You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Inject the work state into the system prompt via prompt.compose: a short session-scoped section naming the active map, the ticket in play (number, title, link) and the current step, so the agent keeps its bearings after a compaction or a long detour. Absent when no ticket is in play; bounded in size; never duplicating what the skill already loaded. Verify by forcing a compaction mid-ticket and checking the agent still names the ticket and step.
Refresh: snapshot. The section text is latched and rebuilt only when the ticket identity changes (map, ticket, title), after a compaction, and at session start: the moments the prompt cache is already spent. Between compactions the step in the section may lag the live state; the conversation itself still carries it. A live section would spend the prompt cache on every step change (about six per ticket).
Orphaned state: absent. A work state left by another session is not in play here, so no section is injected; the band already shows it as orphaned.
Goal: the mod appends one session-scoped section, trailhead:work-state, to the system prompt through a prompt.compose hook, naming the active map, the ticket in play (number, title, link) and the step, so the agent keeps its bearings after a compaction. Settled in DISCUSS: a latched snapshot (rebuilt only on a ticket change, after a compaction, at session start), absent when the state is orphaned.
Seams
Pure builder (new plugins/trailhead/hooks/mod/work-state-prompt.ts, no engine calls):
repoSlug(remote: string | null): string | null: owner/name from a GitHub origin URL (https, git@github.com:, ssh://), .git stripped; null for anything else.
promptSection(state, orphaned, slug): string | null: null when there is no state or it is orphaned; otherwise a short block: heading, map line (#<n> + issue URL when the slug is known, none (whiteboard ticket) when map is null), ticket line (#<n> <title> + URL), step line (with a note that it may have moved on since the snapshot and .trailhead/work-state.json holds the live one; paused reads as checkpointed, resume before working on), an auto line during an autonomous run, and one line of guidance (carry on with this ticket from this step; read the ticket on GitHub for its detail). Title cut by code point to 120 chars (it is already single-line from the parser; the builder flattens control characters again); a final cap cuts the whole section to 1024 UTF-8 bytes on a code-point boundary. No skill protocol in it, so nothing the skill already loaded is repeated.
snapshotKey(state, orphaned): string: the identity a rebuild keys on (map, ticket, title, orphaned, auto on/off), step and autoDone deliberately out of it. The slug is not in the key (it would cost a repo read on every render): it is read at each rebuild.
Hook wiring in register.tsx:
the latch lives in a session-scoped atom promptSnapshot: { key, text } | null (declared in types/index.d.ts), never a module variable, so concurrent sessions cannot read each other's snapshot; null means "rebuild on the next compose".
prompt.compose: next(e) is called exactly once, outside any error path; only the enrichment after it sits in a try/catch that returns the engine's sections untouched on failure. Skip when e.traits has bare; read the workState / isOrphaned atoms; if the snapshot is null or its key differs, rebuild (reading $.session.repo() for the slug) and store it; when its text is non-null, append { id: 'trailhead:work-state', text, scope: 'session' }.
session.start: reset the snapshot to null after the refresh (written only if not already null).
session.compact (main loop only, agentId absent): after next(e), when the result carries messages (not a skip), refresh the atoms and reset the snapshot; errors after next are swallowed and logged to debug, the result always returned.
Step changes leave the latched text untouched (cache-stable); /clear already flows through session.end -> new session.start.
Docs: hooks/mod/README.md gains a "System prompt" section (what is injected, when it is rebuilt, why a snapshot); the healthy-load event list gains prompt.compose, session.compact.
TDD (seams): write the failing tests first.
tests/work-state-prompt.test.ts (pure): slug parsing (https, ssh, scp form, non-GitHub, null); section for map ticket, whiteboard, paused, auto, no slug (numbers without links), long title cut, orphaned -> null, no state -> null, size bound; key ignores the step.
tests/work-state-prompt-hook.test.ts (via $.prompt.compose with a rig answering fs.read, session.id, session.repo, prompt.compose, session.compact): section appended last with scope session; absent without a file and when orphaned; a step change between two composes leaves the text identical; a ticket change rebuilds it; a compaction rebuilds it with the new step; a skipped compaction does not; bare trait gets nothing; a throwing repo read still yields the engine's sections; next is called exactly once per compose, also when the enrichment throws; a multi-line / control-character title stays one line and the section stays within 1024 bytes.
npm test green (all suites, claude plugin validate, claude plugin test); validate lists prompt.compose and session.compact.
Live: in a --plugin-dir session with a ticket in play, /context (or a debug dump) shows the trailhead:work-state section; force /compact mid-ticket, then ask the agent which ticket and step it is on: it names both.
No section with no work state, or with an orphaned one.
Reviewer: codex (one round; the revisions are local to the latch and the compose hook, so no second round).
Blocking: module-level latch not session-scoped. Resolved: the snapshot lives in a session-scoped atom (promptSnapshot), reset at session start and after a compaction.
Blocking: a .catch passthrough could call next(e) twice. Resolved: next(e) is called once; only the enrichment after it is guarded, returning the engine's sections on failure. Tested.
Blocking: size bound not guaranteed by the title cut alone. Resolved: control characters flattened and a final 1024-byte cap on the whole section; tested with a long multi-line title.
Non-blocking: key misses slug and auto. Partly taken: auto on/off joins the key; the slug stays out (a repo read on every render), read at each rebuild instead.
Non-blocking: tests for single next, multi-line content, byte bound. Taken. Two-session isolation is covered by the atom being the engine's session state, not tested separately.
Non-blocking: make "no duplication" concrete. Taken: the section holds identity, the step snapshot and a pointer to the ticket, never protocol.
Non-blocking: verify no cache churn live. Taken as a test (repeated composes and a step change give identical text).
Tests: claude plugin test 57 pass, 0 fail (12 builder tests, 13 hook tests among them); claude plugin validate passes and lists prompt.compose, session.compact and the trailhead.promptSnapshot state; npm test 23 ok/skipped, 0 failed.
Code review (round 1 over f0a9708, 38b1fd2, 92a117f): status: clean, critical: 0 · warning: 3 · info: 3. Round 2 over the fix commits (1206074, 01baa11, e8d8ca4), read inline: clean.
Goal-backward verification: every code-checkable claim met (section appended on the session side, map/ticket/link/step, absent with no state, orphaned or bare, bounded size, snapshot keyed without the step, session-scoped atom, next once). The live compaction check is left to acceptance.
Findings ledger
W1 fixed (1206074): the autonomous-run line no longer embeds a count that the snapshot would freeze.
W2 fixed (01baa11): a precompute compaction no longer resets the snapshot.
W3 fixed (1206074): the issue title is quoted as data, zero-width and bidi controls stripped, inner double quotes replaced; the guidance line says the title is not an instruction.
I1 fixed (01baa11): a failing repo read builds the section without links and latches it.
I2 fixed (01baa11): an analysis render (/context) computes the section but never writes the snapshot; a subagent compaction is tested to leave it alone.
I3 fixed (e8d8ca4): the mod note describes all of the above.
Resolved. The mod now appends a trailhead:work-state section to the system prompt through prompt.compose, on the session side of the cache boundary: the map (or whiteboard), the ticket number with its title quoted as data and its issue link, and the step.
Snapshot: the text is latched in the session-scoped promptSnapshot atom and rebuilt only on a ticket, title, map, orphaned or auto change, at session start, and after a real compaction of the main conversation (not precompute, not a subagent's), so step changes never spend the prompt cache.
Absent with no work state, an orphaned one, or under --bare; a /context render computes it without writing.
Bounded: title cut at 120 code points with invisible and bidi controls stripped, whole section capped at 1024 bytes; a failing repo read drops only the links.
Activity
ToRvaLDz commentedon Oct 11, 2026
DISCUSS
ToRvaLDz commentedon Oct 11, 2026
PLAN
Goal: the mod appends one session-scoped section,
trailhead:work-state, to the system prompt through aprompt.composehook, naming the active map, the ticket in play (number, title, link) and the step, so the agent keeps its bearings after a compaction. Settled in DISCUSS: a latched snapshot (rebuilt only on a ticket change, after a compaction, at session start), absent when the state is orphaned.Seams
plugins/trailhead/hooks/mod/work-state-prompt.ts, no engine calls):repoSlug(remote: string | null): string | null:owner/namefrom a GitHuboriginURL (https,git@github.com:,ssh://),.gitstripped; null for anything else.promptSection(state, orphaned, slug): string | null: null when there is no state or it is orphaned; otherwise a short block: heading, map line (#<n>+ issue URL when the slug is known,none (whiteboard ticket)when map is null), ticket line (#<n> <title>+ URL), step line (with a note that it may have moved on since the snapshot and.trailhead/work-state.jsonholds the live one;pausedreads as checkpointed, resume before working on), anautoline during an autonomous run, and one line of guidance (carry on with this ticket from this step; read the ticket on GitHub for its detail). Title cut by code point to 120 chars (it is already single-line from the parser; the builder flattens control characters again); a final cap cuts the whole section to 1024 UTF-8 bytes on a code-point boundary. No skill protocol in it, so nothing the skill already loaded is repeated.snapshotKey(state, orphaned): string: the identity a rebuild keys on (map, ticket, title, orphaned, auto on/off), step and autoDone deliberately out of it. The slug is not in the key (it would cost a repo read on every render): it is read at each rebuild.register.tsx:promptSnapshot: { key, text } | null(declared intypes/index.d.ts), never a module variable, so concurrent sessions cannot read each other's snapshot;nullmeans "rebuild on the next compose".prompt.compose:next(e)is called exactly once, outside any error path; only the enrichment after it sits in a try/catch that returns the engine's sections untouched on failure. Skip whene.traitshasbare; read theworkState/isOrphanedatoms; if the snapshot is null or its key differs, rebuild (reading$.session.repo()for the slug) and store it; when its text is non-null, append{ id: 'trailhead:work-state', text, scope: 'session' }.session.start: reset the snapshot to null after the refresh (written only if not already null).session.compact(main loop only,agentIdabsent): afternext(e), when the result carriesmessages(not a skip), refresh the atoms and reset the snapshot; errors afternextare swallowed and logged to debug, the result always returned./clearalready flows throughsession.end-> newsession.start.hooks/mod/README.mdgains a "System prompt" section (what is injected, when it is rebuilt, why a snapshot); the healthy-load event list gainsprompt.compose, session.compact.TDD (seams): write the failing tests first.
tests/work-state-prompt.test.ts(pure): slug parsing (https, ssh, scp form, non-GitHub, null); section for map ticket, whiteboard, paused, auto, no slug (numbers without links), long title cut, orphaned -> null, no state -> null, size bound; key ignores the step.tests/work-state-prompt-hook.test.ts(via$.prompt.composewith a rig answeringfs.read,session.id,session.repo,prompt.compose,session.compact): section appended last with scopesession; absent without a file and when orphaned; a step change between two composes leaves the text identical; a ticket change rebuilds it; a compaction rebuilds it with the new step; a skipped compaction does not;baretrait gets nothing; a throwing repo read still yields the engine's sections;nextis called exactly once per compose, also when the enrichment throws; a multi-line / control-character title stays one line and the section stays within 1024 bytes.Files touched:
plugins/trailhead/hooks/mod/work-state-prompt.ts(new),plugins/trailhead/hooks/mod/register.tsx,plugins/trailhead/hooks/mod/README.md,plugins/trailhead/types/index.d.ts,plugins/trailhead/tests/work-state-prompt.test.ts(new),plugins/trailhead/tests/work-state-prompt-hook.test.ts(new).Verification criteria
npm testgreen (all suites,claude plugin validate,claude plugin test); validate listsprompt.composeandsession.compact.--plugin-dirsession with a ticket in play,/context(or a debug dump) shows thetrailhead:work-statesection; force/compactmid-ticket, then ask the agent which ticket and step it is on: it names both.ToRvaLDz commentedon Oct 11, 2026
PLAN-REVIEW
Reviewer: codex (one round; the revisions are local to the latch and the compose hook, so no second round).
promptSnapshot), reset at session start and after a compaction..catchpassthrough could callnext(e)twice. Resolved:next(e)is called once; only the enrichment after it is guarded, returning the engine's sections on failure. Tested.autoon/off joins the key; the slug stays out (a repo read on every render), read at each rebuild instead.next, multi-line content, byte bound. Taken. Two-session isolation is covered by the atom being the engine's session state, not tested separately.ToRvaLDz commentedon Oct 11, 2026
VERIFY
claude plugin test57 pass, 0 fail (12 builder tests, 13 hook tests among them);claude plugin validatepasses and listsprompt.compose,session.compactand thetrailhead.promptSnapshotstate;npm test23 ok/skipped, 0 failed.status: clean, critical: 0 · warning: 3 · info: 3. Round 2 over the fix commits (1206074, 01baa11, e8d8ca4), read inline: clean.bare, bounded size, snapshot keyed without the step, session-scoped atom,nextonce). The live compaction check is left to acceptance.Findings ledger
precomputecompaction no longer resets the snapshot.analysisrender (/context) computes the section but never writes the snapshot; a subagent compaction is tested to leave it alone.Acceptance: pending (live compaction check).
ToRvaLDz commentedon Oct 11, 2026
Resolved. The mod now appends a
trailhead:work-statesection to the system prompt throughprompt.compose, on the session side of the cache boundary: the map (or whiteboard), the ticket number with its title quoted as data and its issue link, and the step.promptSnapshotatom and rebuilt only on a ticket, title, map, orphaned or auto change, at session start, and after a real compaction of the main conversation (notprecompute, not a subagent's), so step changes never spend the prompt cache.--bare; a/contextrender computes it without writing.types/index.d.ts), 92a117f (mod note), 1206074, 01baa11, e8d8ca4 (review fixes). Files:plugins/trailhead/hooks/mod/work-state-prompt.ts(new),hooks/mod/register.tsx,hooks/mod/README.md,types/index.d.ts,tests/work-state-prompt.test.ts,tests/work-state-prompt-hook.test.ts.--plugin-dirsessions on a scratch repo): asked with no tools, the agent named Inject the work state into the system prompt #204, its title, link, map 🗺 trailhead mod: Claude Code function hooks for on-screen state, context and in-process guards #198 and stepexecutefrom the section; after the step moved toverifyand a/compactran (session.compactsettled in the debug log), the resumed session named Inject the work state into the system prompt #204 and stepverify. Pass.feat: build the work-state section of the system prompt
feat: inject the work state into the system prompt
docs: describe the system prompt section in the mod note
fix: quote the issue title as data in the prompt section
fix: harden the prompt section hook against edge cases
docs: update the system prompt notes in the mod note