session
SkillFiles & storageSession 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.
No other account needed.
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 theLATESTpointer, print the/clearnext step.nameoptional; unnamed dumps derive a slug from branch + UTC timestamp.recall [name]— print a stored handover into context and mark it consumed. Named, orLATESTwhen 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 assession-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 perdrop - Doc size cap: ~1.5K tokens (~75 lines). Compress prose, never drop a decision.
- Hook auto-restore window: unconsumed and
createdwithin 30 min. Outside it,recallis 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 (
⚠ staleprefix 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; onlydropremoves 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: dumprecall(aliases:restore,load) → Mode: recalllist→ Mode: listpark→ Mode: parksweep→ Mode: sweepdrop(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
dateflag split needed.
Sourcing precedence for the files table — union of four sources, in this order:
- Conversation history — authoritative. Every
Edit/Writeof 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). git diff --stat HEAD→ theRefcolumn (+N/-M). A file with no diff entry and no commit isRef: uncommitted-new, or dropped when it was temp scratch.git log --name-only --since=…→ files already committed this session, which no longer appear ingit diff HEAD..claude/state/session-context.md→## Files Modified This Session— fallback only.task-log.jswrites 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). State ∈ done / 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):
.claude/state/session/<slug>.md— the composed doc. A slug that already exists is overwritten only afterAskUserQuestionconfirms; otherwise append-2,-3, … ..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
- Edit tool on the doc:
consumed: false→consumed: true(frontmatter only). - 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.mdexcluded by name — parked items have no TTL, onlydropremoves 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:
| Type | Trigger |
|---|---|
| Unanswered question | Claude 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 idea | feature/design idea raised while solving something else |
| Unfinished task | TaskList entry still pending / in_progress |
| Stated-but-unlanded | a 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.
| Mechanism | Survives | Trigger | Store |
|---|---|---|---|
Skill contract (compaction.md) | in-flight skill phase state | auto-compact at 85% | .temp/state/skill-contract.md |
| Session handover (this skill) | plan, decisions, lessons, files table, open loops | explicit 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