session

SkillFiles & storage

Session state that outlives a context reset — `dump` sweeps the live conversation and writes a compact handover doc (goal, decisions + why, lessons, standing instructions, files-touched table, outstanding items, next step), then prints `/clear`; the `session-restore.js` SessionStart hook re-injects it automatically. `park` stashes a diverging idea mid-session without derailing; `sweep` audits the conversation for unlanded work. TRIGGER when: user says "dump the session", "handover before clear", "save state before clearing", "carry this over", "I want to clear but keep the plan", "park this for later", "what did we defer", "anything unfinished before I close". SKIP: surviving auto-compact at 85% (that is the skill contract in `.temp/state/skill-contract.md` per `compaction.md`, written by the running skill); reviving a *finished* conversation (Claude Code native `/resume`).

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the session skill

What this skill tells your AI

The instructions your AI receives, as published by borda/ai-rig in plugins/cc_foundry/skills/session/SKILL.md and read by ahel’s review.

Carry the durable part of a session across /clear, and hold open loops until they land. dump composes a small handover doc from the live conversation, writes it to .claude/state/session/<slug>.md, and ends by printing /clear. The session-restore.js hook (SessionStart, matcher clear) injects that doc into the fresh session, so restore is free. Implementation detail — diffs, tool output, exploration transcript, abandoned approaches — is dropped on purpose; only the decision that settled an approach survives.

Runs inline, never context: fork. The conversation history is the authoritative source for what changed, what was decided, and what never landed; a forked run would see none of it.

NOT for: auto-compact survival (compaction.md skill contract); reviving a finished conversation (native /resume). Boundary table in the notes section below.

  • $ARGUMENTS: required. Six modes:
    • dump [name] — sweep, compose and write the handover doc, set the LATEST pointer, print the /clear next step. name optional; unnamed dumps derive a slug from branch + UTC timestamp.
    • recall [name] — print a stored handover into context and mark it consumed. Named, or LATEST when omitted. Manual path for the dumps the hook skips.
    • list — table of stored handovers (slug, age, consumed) plus the open parked items.
    • park <idea> — append one open-loop item to .claude/state/session/PARKED.md. No dump needed; works any time.
    • sweep — read the conversation for unlanded ideas, unanswered questions and pending tasks; report them and offer to park.
    • drop <item> — fuzzy-match a parked item and remove it, logging the closure.
  • Store dir: .claude/state/session/ (project-local; .claude/state/ is gitignored, same as session-context.md)
  • Item store: .claude/state/session/PARKED.md — one bullet per open item; survives every dump, never auto-deleted
  • Pointer file: .claude/state/session/LATEST — plain text, the slug of the most recent unconsumed dump. Blank or missing = nothing pending.
  • Closure log: .claude/state/session/dropped.jsonl — one JSON line per drop
  • Doc size cap: ~1.5K tokens (~75 lines). Compress prose, never drop a decision.
  • Hook auto-restore window: unconsumed and created within 30 min. Outside it, recall is the manual path.
  • Hook truncation threshold: ~8000 chars — above it the hook injects ## Goal + files table + ## Next step + a → /foundry:session recall <slug> pointer instead of the whole doc.
  • Stale threshold: 14 days (⚠ stale prefix when listing) — applies to handover docs and parked items
  • Delete threshold: 30 days — handover docs only, swept during list. Parked items are never auto-deleted; only drop removes one.
  • Files-table row cap: 25 (over that, group by directory and state the elided count)

Task hygiene: load and follow the protocol below.

# audit-skip: resilience-replication
python "${CLAUDE_PLUGIN_ROOT:-plugins/cc_foundry}/bin/load_shared_doc.py" foundry skills/_shared task-hygiene.md  # timeout: 5000

Step 0: Validate and dispatch mode

Extract first word of $ARGUMENTS as MODE.

If MODE matches:

  • dump (alias: save) → Mode: dump
  • recall (aliases: restore, load) → Mode: recall
  • listMode: list
  • parkMode: park
  • sweepMode: sweep
  • drop (alias: archive) → Mode: drop

Unsupported flag check — after extracting the mode token, scan $ARGUMENTS for remaining --<token> patterns. If found: print ! Unknown flag(s): `--<token>`. Supported modes: dump, recall, list, park, sweep, drop. then invoke AskUserQuestion — (a) Abort (stop, re-invoke correctly) · (b) Continue ignoring (skip unknown flags, proceed with recognized mode).

Otherwise (empty, unrecognized, misspelled): use AskUserQuestion:

"Which session mode did you want?"

Options: (a) dump [name] — write the handover doc now, (b) park <idea> — stash one open loop without derailing, (c) sweep — audit the conversation for unlanded work, (d) list — stored handovers and open items

Step 1 / Mode: dump

Substep 1a: Derive slug and timestamp

git branch --show-current 2>/dev/null; date -u +%Y-%m-%dT%H:%M:%SZ; date -u +%Y-%m-%dT%H-%M-%SZ  # timeout: 5000

Three lines out: branch, ISO created stamp, filesystem-safe stamp. Slug: <name> from $ARGUMENTS when given (lowercase, non-alphanumerics → -); otherwise <branch with / → ->-<filesystem-safe stamp>. Empty branch (detached HEAD) → detached. Reject a name containing / or .. — re-ask via AskUserQuestion rather than writing outside the store dir. LATEST, PARKED, dropped are reserved slugs; re-ask on those too.

Substep 1b: Sweep for unlanded work

Run Step 5 / Mode: sweep internally, plus Read .claude/state/session/PARKED.md (skip silently if absent). The union becomes the doc's ## Outstanding section. Nothing found and no parked items → omit the section.

Substep 1c: Gather file evidence

git diff --stat HEAD 2>/dev/null; echo "--- committed since session start ---"; git log --name-only --pretty=format:'%h %s' --since="8 hours ago" 2>/dev/null | head -60  # timeout: 5000

git parses the relative date itself — no BSD/GNU date flag split needed.

Sourcing precedence for the files table — union of four sources, in this order:

  1. Conversation history — authoritative. Every Edit/Write of this session is visible to this skill; that is the touched-file list, and the only source that also knows what each change was (Change) and whether it landed (State).
  2. git diff --stat HEAD → the Ref column (+N/-M). A file with no diff entry and no commit is Ref: uncommitted-new, or dropped when it was temp scratch.
  3. git log --name-only --since=… → files already committed this session, which no longer appear in git diff HEAD.
  4. .claude/state/session-context.md## Files Modified This Sessionfallback only. task-log.js writes that section from its PreCompact branch, which fires at the 85% auto-compact threshold; a session dumping before any compaction has no such section, or a stale one from a prior session. Read it only when sources 1–3 come up empty.

Column rules: Change ≤6 words, what changed, never why (why belongs in ## Decisions). Statedone / wip / needs-test / reverted. Cap 25 rows — over that, group by directory and state the elided count explicitly, never silently truncate (quality-gates.md). Scratchpad and .temp/ paths are never rows; they belong in ## Artifacts.

Substep 1d: Gather artifacts and open tasks

find .temp .reports .experiments .developments -maxdepth 2 -type d -mtime -1 2>/dev/null | head -20  # timeout: 5000

Call TaskList for tasks still in_progress or pending — they feed ## Next step and ## Outstanding, not a section of their own (task state survives on its own).

Substep 1e: Compose the doc

Fill this template. Omit any section with nothing real in it — an empty heading is noise the next session pays for.

---
slug: <slug>
created: <ISO8601-UTC>
consumed: false
branch: <git branch>
---

## Goal
<the task/plan in 1–3 lines>

## Decisions
- <decision> — why: <reason>

## Lessons / corrections
- <correction received> — rule going forward: <rule>

## Standing instructions
- <programmatic-level directive that must keep applying>

## Files touched

| File | Change | State | Ref |
| --- | --- | --- | --- |
| `path/to/a.py` | added retry guard | done | +42/-3 |
| `path/to/b.md` | README sync | wip | +8/-0 |

## Outstanding
- **<slug>** — <one-line summary>. Why: <one sentence>. Next: <what to ask or do>.

## Artifacts

| Path | Kind | Note |
| --- | --- | --- |
| `.reports/<skill>/<ts>/report.md` | report | consolidated findings |
| `.temp/<skill>/<ts>/` | run-dir | agent handover files |

## Next step
<single concrete next action>

## Dropped deliberately
implementation detail, tool output, exploration transcript

Written in ultra-caveman tier (plugins/CLAUDE.md §Writing Style) — this doc is re-injected into a fresh context on every restore, so every word is a recurring cost. Explicit exclusions: no diffs, no code bodies, no command output, no per-file reasoning, no history of abandoned approaches — only the decision that settled them.

Substep 1f: Write the doc and the pointer

Use the Write tool for both (it creates .claude/state/session/ on demand; no mkdir permission gap):

  1. .claude/state/session/<slug>.md — the composed doc. A slug that already exists is overwritten only after AskUserQuestion confirms; otherwise append -2, -3, … .
  2. .claude/state/session/LATEST — the slug alone, one line.

PARKED.md is not cleared by a dump — the doc copies items, it does not consume them. An item leaves only via drop.

Substep 1g: Print the next step

Terminal only, short. Confirm the path, the row count of the files table, then:

→ .claude/state/session/<slug>.md (N files, M decisions, K outstanding)
Next: clear the context. I cannot run it for you — Claude Code exposes no programmatic slash invocation, so the line below is yours to send. The SessionStart hook re-injects this doc automatically on the other side.

/clear

The literal /clear is the last line of the reply — nothing after it, so it is one copy away.

End with a ## Confidence block per quality-gates.md — score on: files table backed by conversation evidence (not guessed), decisions captured with their reasons, sweep run before composing, next step concrete enough to act on cold.

Step 2 / Mode: recall

Substep 2a: Resolve the target

Named → .claude/state/session/<name>.md. Unnamed → read .claude/state/session/LATEST (Read tool) and use the slug it holds; blank or missing → print No pending handover. → /foundry:session list and stop.

Missing target file → print the miss and fall through to a list render so the user sees what does exist.

Substep 2b: Print it into context

Read the doc and print its body verbatim, frontmatter stripped, under a one-line banner naming the slug and its age. Terminal only — never route this to a file: landing it in context is the mechanism, and output-routing to .temp/ would defeat it.

Substep 2c: Mark consumed

  1. Edit tool on the doc: consumed: falseconsumed: true (frontmatter only).
  2. Write tool on .claude/state/session/LATEST: empty content. The hook treats blank and missing alike, so nothing re-injects on the next /clear.

End with a ## Confidence block per quality-gates.md — score on: target resolved unambiguously, doc printed intact, consumed marker written.

Step 3 / Mode: list

Substep 3a: Sweep expired handover docs (≥ 30 days)

find .claude/state/session -maxdepth 1 -name '*.md' ! -name 'PARKED.md' -mtime +30 -delete 2>/dev/null; echo "sweep done"  # timeout: 5000

PARKED.md excluded by name — parked items have no TTL, only drop removes them.

Substep 3b: Collect and render

Glob .claude/state/session/*.md excluding PARKED.md (it holds bullets, not frontmatter), Read each one's frontmatter for slug, created, consumed, branch. Age comes from created, not file mtime — marking a doc consumed rewrites the file and would reset mtime. Read PARKED.md separately for the items table; its ages come from each bullet's Raised: date.

## Session store — <today's date>

### Handovers

| Slug | Age | Branch | Consumed |
| --- | --- | --- | --- |
| `plan-x` | 12 min | main | no |
| `⚠ stale refactor-auth` | 16 d | feat/auth | yes |

### Parked (<N> open)

- [ ] **retry-backoff** — revisit exponential vs linear. Raised: 2026-08-10.
- [ ] ⚠ stale **split-ratio** — 80/20 vs 70/30 never settled. Raised: 2026-07-20.

→ /foundry:session recall <slug> to print a handover back into context
→ /foundry:session drop <item> to close a parked item

⚠ stale prefix at ≥ 14 days, both tables. Nothing in either → No stored handovers, no parked items.

End with a ## Confidence block per quality-gates.md — score on: glob returned a result (even empty), every frontmatter parsed, ages computed from created / Raised:.

Step 4 / Mode: park

Payload is everything after park in $ARGUMENTS. Empty payload → run Mode: sweep instead and offer its findings as parking candidates, rather than asking for text the conversation already holds.

Derive a short kebab slug from the payload, then Read .claude/state/session/PARKED.md (absent → start a fresh file with the # Parked items heading) and Edit/Write one bullet appended at the end:

# Parked items

- **<short slug>** — <one-line summary>. Raised: <YYYY-MM-DD>. Why: <one sentence>. Next: <what to ask or do when revisiting>.

Date from date -u +%Y-%m-%d (fold into any bash call this step already makes). Slug already present → update that bullet rather than adding a near-duplicate, and say so.

Print one line: Parked: <slug> plus the current open count. Terminal only.

Step 5 / Mode: sweep

Read the conversation, not the filesystem. Nothing to run — the history is already in context. Collect, in this order:

TypeTrigger
Unanswered questionClaude asked, user sent a new top-level request instead of answering
Deferred exploration"come back to that", "park this", "later" — idea named, not pursued
Diverging ideafeature/design idea raised while solving something else
Unfinished taskTaskList entry still pending / in_progress
Stated-but-unlandeda change discussed and agreed, with no Edit/Write backing it in this session

Call TaskList for the fourth row. Detection stays behavioural — a new top-level request without an answer to the prior question — never semantic-similarity scoring.

Render:

## Unlanded — <N> items

| # | Item | Type | Why it did not land |
| --- | --- | --- | --- |
| 1 | retry backoff shape | deferred | user said "later, after the bench lands" |

Then AskUserQuestion: (a) park all · (b) park a subset (list the numbers) · (c) skip. Selecting (a) or (b) runs Mode: park for each chosen item in the same turn.

Sweep sees the current conversation only. A finished session's unlanded ideas are unreachable — native /resume revives the conversation itself; this skill never mines transcript JSONL.

End with a ## Confidence block per quality-gates.md — score on: every row traceable to a concrete conversation turn (not inferred), TaskList consulted, no already-landed work listed as outstanding.

Step 6 / Mode: drop

Substep 6a: Fuzzy-match the target

Match everything after drop against the slugs and summaries in .claude/state/session/PARKED.md. Ambiguous (2+ equally close) → list them and AskUserQuestion to disambiguate. No match → render the parked list and stop.

Substep 6b: Remove the bullet and log the closure

Edit tool on PARKED.md — remove only the matched bullet, leave the rest untouched. Then append one audit line:

TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
jq -n --arg ts "$TS" --arg item "<matched slug>" '{"ts":$ts,"item":$item,"action":"dropped"}' >> .claude/state/session/dropped.jsonl  # timeout: 5000

jq escapes the slug — a bullet holding quotes or braces can never break the log line.

Print Dropped: <slug> plus the remaining open count. Terminal only.

End with a ## Confidence block per quality-gates.md — score on: match unambiguous, only the matched bullet removed, audit line appended.

Why not one command. A skill cannot invoke /clear — Claude Code exposes no programmatic slash invocation (that is Agent SDK query() only), and neither keybindings, UserPromptSubmit, nor UserPromptExpansion can trigger one either. So dump + /clear stays two steps; the mechanism buys its keep by making the third step (restore) automatic.

Why inline, not forked. context: fork skills cannot read conversation history. The files table's Change and State columns, the decisions, the lessons and the whole of sweep come from that history — a forked run would have nothing to write.

Two state mechanisms — they do not overlap.

MechanismSurvivesTriggerStore
Skill contract (compaction.md)in-flight skill phase stateauto-compact at 85%.temp/state/skill-contract.md
Session handover (this skill)plan, decisions, lessons, files table, open loopsexplicit dump before /clear.claude/state/session/

Restore is best-effort, dump is not. The hook injects only when the pointer exists, the doc is unconsumed, and it is under 30 minutes old — a deliberately narrow window, so an old dump never ambushes an unrelated session. Everything outside it is reachable by /foundry:session recall <slug>, which has no age gate. The four cases that land there: dumped and walked away (>30 min); doc over ~8K chars (hook injects the head plus a pointer); fresh terminal rather than a /clear (the matcher is clear); wanting the same doc a second time (the first restore set consumed: true).

recall, not resume. Claude Code's native /resume revives a whole past conversation — strictly better recall than any document. This mode name stays clear of it deliberately; the two are not alternatives.

Parking is a command, not a behaviour. The predecessor made parking an automatic behavioural rule documented only inside this file, which loads only on invocation — so it never fired, and the store held zero items for four months. park is now an explicit mode with an explicit store. Nothing writes to PARKED.md unless the user asks for it, and no TTL deletes from it.

Scope: the store is project-local — .claude/state/session/ lives inside the working tree, so nothing leaks across projects.

Signals

GitHub stars
27
Forks
4
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
session-borda
Source
github.com/borda/ai-rig