Repository navigation
Description
Destination
A released trailhead version whose Claude Code plugin ships a mod (a function-hooks module, Claude Code's early-access plugin API) inside plugins/trailhead, alongside the existing command hooks. On Claude Code it adds: an on-screen view of the work in play (status, pipeline band, live map pane, toasts, compact subagent rows, rendered map/dashboard output, next-command suggestion), active-ticket context that survives compaction, native engine plumbing (model enforcement at subagent spawn, native gh-backed tools, an auto loop driven by turn events), and the guardrails ported in-process. Codex and any host without mods keep working unchanged on the command hooks. Documented in both READMEs and /docs, released by command.
Notes
Parent-repo anchors: conventions #13. Codebase: 🧱 Codebase map.
Branch: this map's work commits straight to main (conventional commits + Refs:), per the conventions' git: main. The former feat/mod integration branch was merged into main and retired on 2026-10-13.
Settled at chart (the fixed frame):
- Destination = released, not a local prototype: the mod ships to users inside trailhead.
- Packaging = inside the trailhead plugin (one install), not a separate opt-in plugin. This rests on two assumptions verified first by research: a plugin can declare command hooks and a function-hooks module together, and a Claude Code build without mods ignores the module without breaking the plugin.
- All four areas are in scope: on-screen UI, context and compaction, native engine plumbing, in-process guards.
- Command hooks stay the floor. Codex has no mods, so the core must remain self-sufficient on command hooks; the mod adds or replaces behaviour only where it is loaded. The API is early access, so reversible work (UI, context) lands before the guards.
- Develop and try locally with hot reload (
--plugin-dir/ the session mods folder),claude plugin validateandclaude plugin test.
Glossary
- mod: a function-hooks module (
hooks/hooks.json->modules, exportingregister(on)), Claude Code only. Avoid: "function hook plugin" as a separate product. - command hook: the existing
nodescripts registered underhooksinhooks.json(PreToolUse / PostToolUse / SessionStart); the host-neutral floor. - band: the row drawn above the prompt (
AbovePrompt). pane: a docked side view (Pane). status: the status-line entry ($.ui.status). - work state: what the session is doing in trailhead terms (active map, ticket, step); the single fact the UI and context features read.
Decisions so far
- Can the core plugin ship a mod alongside its command hooks?: yes, one
hooks/hooks.jsonholdshooks+modules; builds without mods skip only the module; Claude Code 2.1.287+. Correction to chart decision 2: the npm installer needs its own mod plugin dir under<configDir>/skills/(see fog 13). - Where does the mod read the work state from?: the skills write
.trailhead/work-state.json(map, ticket, title, step, auto, session, updatedAt) through a template helper; the mod reads it on events, observations only flag staleness; a differentsessionmarks it orphaned (shown dimmed, never auto-deleted, no TTL);session-ticketunchanged. Implemented by Write the work state from the skills. - How do in-process guards coexist with the command-hook guards?: per guard (a guard moves to the mod only once ported and tested); the mod signals via
TRAILHEAD_MOD_GUARDS=<guards>@<epoch-ms>refreshed on everytool.call, command hooks skip only listed guards with a fresh stamp; every ported guard fails closed via.catch; search-guard rewrites only safecd <abs> && <read>shapes, denies the rest. Speed win split out as Merge the Bash guards into one hook process. - Scaffold the mod in the plugin with its dev and test loop:
hooks/mod/register.tsdeclared undermodulesbeside the command hooks,$.statecontracttrailhead.workStateintypes/index.d.ts, no visible behaviour;npm testnow runs every suite plusclaude plugin validate/claude plugin test; dev loop inhooks/mod/README.md. Installer and Codex unchanged (pinned by tests). - Write the work state from the skills: template helper
trailhead-work-state.js(set/clear) writes.trailhead/work-state.jsonatomically with whole-state validation and thesessionstamp; the skills call it at work-start (after workspace setup), each step, pause/resume,auto, andclearat Resolve;researchexcluded, UAT follow-ups map toverify; Codex carries helper and calls (pinned by tests). - Status entry and pipeline band showing the work state: the mod draws a status entry (
map #n · #t title · step, engine-prefixed⚠ trailhead:) and anAbovePromptband (pipeline with the current step,auto · n done, dim when orphaned), refreshed on session/turn events and after a helper call; the helper gainedautoDone(--auto-done); module nowregister.tsx, view logic inwork-state-view.ts; verified live on the terminal. - Inject the work state into the system prompt: a
prompt.composehook appends a session-scopedtrailhead:work-statesection (map, ticket with quoted title and link, step), latched in thepromptSnapshotatom and rebuilt only on a ticket change, at session start and after a real main-loop compaction, so step changes keep the prompt cache; absent when orphaned or--bare, capped at 1024 bytes; verified headless across a/compact. - Merge the Bash guards into one hook process:
hooks/trailhead-bash-guards.jsruns commit, secret, install, search, body in one process (first block wins, per-guard failure isolation, same verdicts), the only BashPreToolUseregistration on Claude Code and Codex (Codex--guards=commit,secret,install,body), 7 -> 3 spawns per Bash call; each guard exportsevaluate(cmd, { env }); a freshTRAILHEAD_MOD_GUARDS=<names>@<epoch-ms>(5 s,MOD_GUARDS_FRESH_MS) skips exactly those guards, ready for the in-process ports (fog 10). - Live map pane listing the active map's tickets:
hooks/mod/map-pane.tsxdraws aPaneof the active map's open tickets (frontier / in progress / blocked, holders, in-play marker, unverified only counted) fromghvia$.process.run, only while open, 15 s floor + one trailing refresh, stale results dropped, last list kept on failure; per-rowworkbutton runs/trailhead:work <n>via$.command.run; toggled by/trailhead:panel(acommands/panel.mdthe mod answers, since mod-registered commands cannot carrytrailhead:; left out of Codex), auto-opens once in fullscreen;FORCE_HYPERLINK=1for terminals Claude Code cannot identify. - Burn-down view of the active map's progress: a Burn-down section inside the Map pane (not a pane or band of its own): open tickets over time rebuilt from the map's issues'
createdAt/closedAtvia one moregh issue list --state all(limit 5000, a dim capped line beyond), superseded/out-of-scope closes counted as removed scope; terminal draws a 3-rowRaster, other surfaces a text sparkline; a failed read keeps the last curve (same map only) labelled with its age; verified live on map 198. - Switch maps from the panel by clicking one: the Map pane lists every open map (
Maps · n, shown only with more than one; active marked●), a click only views another map (pane-localviewedMapatom,viewing #n · active #m+back to active), the pane never writes.trailhead/active-map; the switch happens on use, since a named map ticket makes its map active (rule in_shared/multi-map.md, pointed to byworkandquick); with no active map the open maps are listed to view; verified live (guided UAT). - Keep the map panel through /clear and clear before work: the band and the Map pane re-read their state on
classic.SessionStart(sourceclear), the engine's new-session event that fires after/clearhas reset the mod's atoms (verified live on 2.1.296, firmer than a timer); the pane carries the auto-open flag and the viewed map across through module variables; theworkbutton runs/clearthen/trailhead:work <n>via$.command.run, skipping the verb when the session id did not change (a denied clear resolves, it does not reject); verified live (guided UAT). - Add a close button to the map panel footer: the Map pane footer holds
refresh+ status on the left andcloseat the right edge (space-betweenover the pane width);closecloses the pane (itsui.closehook stops the timers) and marks the auto-open done, like a close by the person; a status too long beside the buttons (footerStatusinmap-pane-view.ts) moves whole to a row of its own under them, so aghfailure keeps its hint; verified live (guided UAT). - Add mockup screenshots of the new panels to the docs and website: the canvas map-pane and burn-down artboards were brought up to the shipped panel (maps list, viewing, footer
close) and approved; 2x crops (map-panel,map-panel-viewing,burndown,work-state-band) inassets/screenshots/(README EN/IT) andsite/src/assets/screenshots/(Astro-optimised); README section On screen in Claude Code,/trailhead:paneldocs text updated to the shipped panel, new landing section Live in Claude Code after Know where you stand; pre-existing 375px landing overflow split out as Landing scrolls sideways at phone width (map 98). - Panel: mark this session's ticket and show paused tickets: new
trailhead:pausedlabel (pause adds after thePAUSEDcheckpoint, resume removes after the claim check, awork/quickclaim drops it; nineteen labels at first-use setup); the panel lists paused tickets only underPaused · n(holder, nowork), map and dashboard views; a per-sessionownTicketatom keeps▸+this sessionon the session's own row even when another session overwrites the shared work-state file; verified by guided UAT. Other surfaces still count a released pause as Frontier: #263. - Panel: show open/total ticket counts per map and the whiteboard's assigned tickets: maps list shows
N open / Mper map from one GraphQLsubIssuesSummaryread (name-only fallback on failure); dashboard whiteboard heading shows open and assigned counts. - Panel: a Whiteboard row above the Maps list, with its counts, in place of the header link: the header
dashboardbutton became aWhiteboardrow above the Maps list (map, loading and no-active-map views; not drawn in the whiteboard view, whosemapbutton goes back) showingN open / Mfrom two aliasedtotalCountconnections in the one maps GraphQL call (parseMapsRead,MapPaneData.whiteboard); a failed or unreadable read draws it name-only, never stale counts; verified by guided UAT. - Treat paused tickets the same on every surface:
trailhead:pausedis off the frontier on every surface (-label:trailhead:pausedin every frontier query, rule insubstrate.md); map/whiteboard renders and the dashboard show a Paused group with the panel's precedence (unverified > paused > blocked > in progress > frontier); taken back only by/trailhead:resumeor a namedwork/quick,autovia its resume step. - Map panel loses its repo when the session cwd moves:
hooks/mod/repo-pin.tspins the session checkout (toplevel + a remote gh can serve, Enterprise viagh auth status); every paneghruns withcwd= pinned root, pane and band read.trailhead/*under it; a cwd outside any repo keeps the pin, a move to another served repo re-pins and resets the pane; never pinned → "Not in a GitHub repository", noghcall;uat.setup: []for this repo; work button and prompt links left to Panel work button and prompt links still follow the session cwd #269. - Panel work button and prompt links still follow the session cwd: the work button refuses (short toast) unless the session is in the pinned repo; prompt links use the pin's host and owner/name;
remoteIdis the single remote parser.
Work-state prompt section missing when the work state is set mid-session (Work-state prompt section missing when the work state is set mid-session #271): the engine keeps the first composed system prompt, so a mid-session work-state change reaches the model as prompt.submit context once per change (stale note when cleared); only the first compose after a reset writes the latch.
Work-state band hidden when the ticket runs in an isolated worktree: the mod finds this session's work state in the repo's other checkouts (worktrees, sibling<repo>-t<n>clones) when the own checkout has none or another session's - Refresh the map panel when a ticket is opened or closed: open-ticket lists read through GraphQL (not the lagging search index), one follow-up read after a write within the floor, wider write matcher
Cancel a queued ticket from the map panel (Cancel a queued ticket from the map panel #304): a panel press during a running turn is held by the panel (nothing sent to the host) and runs at turn end;canceldrops it, conversation untouched. - Panel says no GitHub repository after /trailhead:new created one mid-session: the host's
session.repo()stays null for a repo created mid-session (Claude Code 2.1.306), socurrentRepo($)inrepo-pin.tsfalls back to git in the cwd (rev-parse --show-toplevel, thenremote get-url origin) when it answers null; the pin refresh (pane and band) and the work-press check share it; a host read that throws stays "checkout unreadable"; verified live in tmux. - Panel work press moves into the pinned repo instead of refusing: outside the pinned repo a
work/resumepress sends the verb with a trailing--in=<base64url(root)>; the work cluster moves there first throughtemplates/trailhead-checkout-in.js(validates a checkout root, POSIX-quoted lonecd, post-movehererequired, fails closed) before the load and the Preflight; no pin, an unreadable or removed checkout still refuse before the/clear;quickout of scope. - Map panel does not follow the whiteboard ticket in play or a newly created one: the panel switches to the whiteboard once per own whiteboard ticket in play and stays there after Resolve; a whiteboard ticket created from a map raises a toast hint, no switch.
- Show the total open ticket count across all maps and the whiteboard: the Map pane draws a dim
N open across all maps + whiteboardline (under the Whiteboard row, and the Dashboard header) from one aliased GraphQLtotalCountquery over the dashboard's label scopes (trailhead:map-<n>per open map +trailhead:whiteboard);at least Nwhen the maps read is cut; a failed count keeps the last total only over the same maps;/trailhead:mapstates the same total via the dashboard'sgh issue listcounts and limits. - Map panel keeps a closed ticket as held and its dependents blocked: bug fixed. A dropped trailing refresh timer left the pane on the pre-close list; the trailing timer is now tracked and replaced, and a turn end after a tracker write always reads.
- Map panel drops a read in flight when the pin moves within the same repo (Map panel drops a read in flight when the pin moves within the same repo #315): a same-repo pin move no longer drops the read in flight; it is read again (generation bumped only for another repo, plus a pin-move counter in the stale check).
- Map panel fails with no-surface on a fresh session (Map panel fails with no-surface on a fresh session #321): with no surface attached yet the panel command keeps the pane pending and answers a plain message, never closes it or leaks the SDK reason.
- 🐛 Map panel says "gh not installed" when the followed checkout was deleted: a deleted followed checkout now re-pins to a live checkout of the same repo, else reports
checkout not found: <path>.
Map pane does not refresh after a multi-line Bash tracker write: not reproducible; measured, every write starts a read within 0.3s or at the next trailing read inside the 15s floor. Regression tests added, no mod change. - Map pane write matcher counts a heredoc body line as a tracker write: the pane's write check strips heredoc bodies before the regex, through an import-free leaf held equal to shell-scan.js by a parity test
Not yet specified
- Ticket-closed feedback: toast + optional
$.audio.speakwhen a ticket resolves or verify passes; opt-in surface (config key?) open. - Compact subagent rows:
ui.renderonUserMessagetask notifications fortrailhead-*agents (name, step, duration), full row on ctrl+o. - Rendered map / dashboard output:
/trailhead:mapand/trailhead:dashboarddrawn asMarkdownwith clickable ticket links (onLinkPress). - Next-command suggestion:
prompt.suggestproposing/trailhead:work <n>after a resolve; must respect the/clear-first handoff rule. - Checkpoint before compaction:
session.compacttriggers a pause checkpoint of the ticket in play; what it writes (aPAUSEDcomment vs a local note) and when it is too noisy open. - Model enforcement at spawn:
agent.spawnsets the model fortrailhead-*agents fromconfig.models.*(and themodel-defaults.jsontiers) instead of relying on the prose dispatch. - Native gh-backed tools:
$.tool.registertools (read ticket, frontier, map) returning typed data, fewer Bash calls and permission prompts; which tools, and how the skills prefer them only when present. - auto loop on turn events: moved to 🗺 trailhead auto as fog.
- Guards in-process: port the guards one by one to
tool.call/tool.checkand the injection scanner tosession.append, per the frame settled in How do in-process guards coexist with the command-hook guards? #201 (per-guard replacement, stamped env signal, fail-closed.catch, safe-shape rewrite); builds on Merge the Bash guards into one hook process #207. Open: porting order and the stamp threshold. - Bash statusline fate: whether
~/.claude/statusline-trailhead.shis retired, kept for the terminal, or reads the same work state. - Release and docs: both READMEs,
/docs, badges (hook counts change), CHANGELOG, a minimum Claude Code version note (2.1.287, tested build pinned), the Codex projection verified to skip the module, and a mod-only off switch via auserConfigboolean read inregister. - npm delivery of the mod:
bin/trailhead.jsplaces a dedicated mod plugin dir under<configDir>/skills/(outsideplugins/trailhead/skills), plus uninstall,install-verify, and a dedupe when both the plugin and the npm copy load a mod.
Out of scope
- A mod for Codex or any other host (no mod API there).
Activity
ToRvaLDz commentedon Oct 12, 2026
Autonomous run summary (2026-10-12)
9 remaining items