Imported from Emasoft/ai-maestro-janitor (
skills/janitor-handoff-and-clear/SKILL.md). Install upstream withnpx skills add Emasoft/ai-maestro-janitor --skill janitor-handoff-and-clear. Copyright stays with the author.
Janitor handoff-and-clear
Overview
/compact keeps a session going across a context ceiling, but it drags a
compaction summary forward in the transcript forever after. /clear resets to
base-context only — no residual summary, cheaper in steady state — but it is
UNRECOVERABLE (no scrollback, no summary). Whether it also destroys the
session-scoped heartbeat cron is build-dependent — /clear keeps the same
process, so the in-memory cron often just survives it; on-session-start.py's
own re-arm nudge is conditional ("if it is missing, run /janitor-arm") for
exactly this reason (#186). Either way this skill's bootstrap re-arms
unconditionally, so it is correct whether or not the cron actually needed it.
This skill makes /clear safe to use as a continuity primitive: it captures
everything the next session needs into durable storage FIRST, fires /clear, then
types a bootstrap into the fresh pane that re-arms the cron and resumes from the
persisted handoff.
The memory system IS the payload store; the handoff is only a short INDEX of
pointers. The resumed session's first read is the tax this primitive pays every
time — it must not become a second bloated context. So the handoff is
concise-but-exhaustive: it mentions ALL material state, but every big chunk is
REPLACED with a LINK (a [[wikimem-page]], an id:ATOM-xxxx-xxxx, a TRDD-<id8>,
a #issue) into already-harvested storage, never inlined. The resumed session pulls
each detail on demand via memgrep recall only when it actually needs it.
When to use
- The in-flight work is already fully captured in TRDD
## STATEblocks, wikimem pages, and files — so a/clearloses nothing that a link can't recover. - You want the cheaper steady-state of base-context (no compaction summary riding forward) and the work is durably written down.
Prefer /janitor-compact-context instead when there is live scratch reasoning not
yet durably written — /compact keeps a summary; /clear does not. If in doubt,
compact. /clear is unrecoverable: once it runs there is no second chance to recover
missed state.
Instructions
1. PRECONDITION — harvest material state into wikimem FIRST
This step is not optional. A link to an atom that does not exist yet is not a pointer, it's a lost fact — the handoff would be concise but no longer exhaustive. Before writing any handoff, ensure every piece of material session state is captured into durable storage:
- Decisions made this session, non-TRDD facts, hard-won mental models, rejected
alternatives + why → capture as wikimem atoms/pages now, via
/janitor-memory-write(or/janitor-memory-record-recentto harvest recent changes). RECALL first (/janitor-memory-recall) so you UPDATE the page that owns the subject rather than duplicate it. - In-flight task state → it belongs in the governing TRDD's
## STATEblock (authoritative, and already surfaced on the next SessionStart byon-session-start-trdd-state.py). If the STATE block is stale, update it now. - Pending questions for the user, open issues → a wikimem note or a GitHub
#issue.
If material state is NOT yet harvested and you cannot harvest it now, STOP and use
/janitor-compact-context instead — /compact preserves a summary, so it is the
safe choice when knowledge isn't durably written yet.
2. Write the CONCISE, LINK-ONLY handoff
Author a short index and Write it to
${CLAUDE_PROJECT_DIR}/.janitor/state/agent-handoff.md. Rules:
- Link, never inline. Every big chunk of information is a LINK to where it durably
lives:
[[wikimem-page]],id:ATOM-xxxx-xxxx,TRDD-<id8>,memgrep recall "<symptom>",#issue. A line like "decided X because Y — see ATOM-xxxx-xxxx" is correct; pasting the full Y reasoning inline is NOT, even if Y is short. - Exhaustive by REFERENCE. Omit nothing material — every open issue, pending decision, and in-flight task gets a line: what it is + a pointer to the detail. The detail is read on demand; the handoff is the table of contents.
- Do NOT duplicate TRDD
## STATEblocks. They are already surfaced on the next SessionStart. Point at theTRDD-<id8>; don't restate its STATE. - Cover, terse: the in-flight TRDD id(s) + one line each; the ONE concrete NEXT ACTION (runnable as written, pointing at durable state); each open issue + its pointer; any decision/fact NOT already in a TRDD/file → its wikimem link.
- Target a few hundred bytes to low KB — never the tens-of-KB a compaction summary
runs.
clear_trigger.pyWARNS on stderr if the handoff is over-budget, has no references, or inlines a large fenced block.
3. Fire the trigger
clear_trigger.py waits briefly for this pane to go idle, then either spawns a
VERIFIED keystroke chain (the common case: types /clear, waits for the fresh
session to come up, then types the re-arm bootstrap) or falls back to a blind
two-phase send on a channel it cannot read back. Give it a --directive whose FIRST
instruction is to run the verification after phase (so the proof completes
automatically on resume), then to read the handoff and resume:
uv run --script --quiet "${CLAUDE_PLUGIN_ROOT}/scripts/clear_trigger.py" \
--directive "run handoff_clear_verify.py --phase after FIRST, then read .janitor/state/agent-handoff.md (link-only handoff) and continue TRDD-<id8> — read its STATE block"
Read the result (#154, #136 — this is the CURRENT contract; older docs describe
USER_PRESENT as an outcome, which no longer exists):
CLEAR_CHAIN_SPAWNED→ the common case (a readable pane — tmux, or an ai-maestro agent). A detached child now types/clear, waits for the fresh session, then types the re-arm bootstrap. The resume state (resume-directive.txt+resume-after-clear.flag) is written by THAT child, immediately before the verified/clearkeystroke — NOT synchronously here. Proceed to step 4.DIRECTIVE_WRITTEN <path>+CLEAR_MARKER_WRITTEN <path>, thenCLEAR_FIRED→ the fallback path (this channel can't be read back — e.g.wtype/xdotool): the resume state IS written synchronously here, then both phases are typed blind. Proceed to step 4.DIRECTIVE_WRITTEN <path>+CLEAR_MARKER_WRITTEN <path>, thenNO_ITERM→ same fallback path, but no automatable terminal was found. Tell the user: "Handoff written — please run/clear, then/janitor-armto re-arm the heartbeat." The resume state IS recorded, so a manual clear + re-arm still auto-resumes.DEFERRED <reason>(stderr) → you are actively typing in this pane and the script gave up waiting for it to go idle (120s). NOTHING was written and/cleardid NOT fire — re-run this step once the pane is idle.HANDOFF_MISSING/HANDOFF_NOT_CONCISE(stderr) → you skipped step 2 or wrote a bloated handoff./clearis unrecoverable — go back and fix the handoff before it runs.
4. Snapshot ground truth for the cross-/clear verification
Run the before phase now, right AFTER firing the trigger — it records the current
cron id, live context size, the handoff's byte size + [[link]] list, and the log
tails into .janitor/state/handoff-clear-verify.json, which SURVIVES /clear. This
is what lets the resumed session PROVE (not infer) that the heartbeat cron is
healthy, the context collapsed, and the handoff is recoverable across the /clear
boundary. Running it after the trigger (not before, as an earlier revision of this
skill did) also gives it a chance to observe resume-after-clear.flag if the
trigger's write already landed — that check reads SKIP, never FAIL, when it hasn't:
uv run --script --quiet "${CLAUDE_PLUGIN_ROOT}/scripts/handoff_clear_verify.py" --phase before
5. END YOUR TURN IMMEDIATELY
This is critical, and it is also an INPUT-SAFETY requirement (wikimem
claude-code-esc-input-semantics): a slash-command typed into a BUSY pane buffers
and floods later, so /clear must land on an IDLE line. The trigger fired a
detached, soft keystroke sender (no ESC, no Ctrl+C) — /clear runs the moment
this turn ends, so stopping now is what makes it land on an idle prompt. Emit one
short line like "Handoff written; clearing now, I'll re-arm and auto-resume." and
stop. Do NOT do more work: the next thing that must run is /clear.
What happens after /clear (the resume, unattended)
/clearstarts a fresh session;SessionStartfires (source: "clear"), whose re-arm nudge tells the model to verify the cron and arm it if missing (#186 —/clearkeeps the same process, so the cron often just survives it; the nudge is conditional for exactly that reason) — but a shell hook cannot call CronCreate, so on an unattended machine the bootstrap keystroke is what actually re-arms it when needed, not a human. That same hook stampsclear-observed.ts, which is what ARMS the pre-clear marker below (see step 2) —/clearhas no hook of its own, sosource=clearis the only unambiguous observation that it happened.- The bootstrap types
/janitor-arm(idempotent — re-arms the cron if/cleardropped it, no-op if it survived) then/janitor-resume(runs the dispatcher stub →dispatch.py::_phase_clear_resumereadsresume-after-clear.flag→ emits[janitor-resume]+ the handoff directive). The phase requiresclear-observed.tsto be at or newer than the flag: the flag is written BEFORE the clear, so presence alone would let a heartbeat firing in that gap consume a resume for a clear that has not happened yet. - The directive's FIRST instruction runs
handoff_clear_verify.py --phase after, which reads thebeforesnapshot (it survived/clear) and emits a PASS/FAIL table toreports/continuity-build/— proving the heartbeat cron is healthy (survived unchanged or was destroyed+recreated, whichever this build does), the context collapsed, and every handoff link resolves. - The fresh session reads
.janitor/state/agent-handoff.mdand reconstructs the issues/needs by following its wikimem/TRDD links (memgrep recall) on demand.
Scope
ONLY writes the resume state into THIS project's .janitor/state/ and types /clear +
the re-arm/resume bootstrap into THIS session's own pane (matched by
$ITERM_SESSION_ID UUID in iTerm, or $TMUX_PANE in tmux — never other panes, so
concurrent Claude instances are untouched). Does NOT change plugin config, does NOT
disarm the heartbeat, does NOT clear other sessions.
Resources
${CLAUDE_PLUGIN_ROOT}/scripts/clear_trigger.py— backing script (validates the handoff, then either spawns a verified detached/clear+ bootstrap chain that persists the resume marker immediately before the verified/clearkeystroke, or falls back to a synchronous write + blind send on an unreadable channel;--dry-runprints the plan and fires nothing).${CLAUDE_PROJECT_DIR}/.janitor/state/agent-handoff.md— the link-only handoff this skill writes; read FIRST on resume, then follow its links.${CLAUDE_PROJECT_DIR}/.janitor/state/resume-after-clear.flag— the pre-clear resume markerdispatch.py::_phase_clear_resumeconsumes on the re-armed cron's first fire. ONLY that phase may consume it; no other resume phase may treat it as subsumed, since it describes an event that has not happened yet. An UNARMED flag older thanclear_resume_max_age_s(default 24h) is swept as an abandoned clear.${CLAUDE_PROJECT_DIR}/.janitor/state/clear-observed.ts— written bySessionStartonsource=clear; the flag above is inert until this is at or newer than it.${CLAUDE_PLUGIN_ROOT}/scripts/handoff_clear_verify.py— the cross-/clear verification harness (--phase beforesnapshots ground truth;--phase afterproves it and writes the PASS/FAIL report). The snapshot lives at${CLAUDE_PROJECT_DIR}/.janitor/state/handoff-clear-verify.json(survives /clear)./janitor-memory-write//janitor-memory-record-recent//janitor-memory-recall— the harvest surfaces the step-1 precondition uses./janitor-compact-context— the/compactalternative; prefer it when live scratch reasoning isn't durably written down yet.