@marcomigozzi/trailhead 0.12.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.it.md +38 -13
  2. package/README.md +48 -13
  3. package/bin/lib/codex-projection.js +18 -11
  4. package/bin/lib/codex-projection.test.js +39 -11
  5. package/bin/lib/install-verify.js +10 -5
  6. package/bin/lib/install-verify.test.js +51 -0
  7. package/bin/trailhead.js +37 -25
  8. package/bin/trailhead.test.js +162 -5
  9. package/package.json +4 -1
  10. package/plugins/trailhead/.claude-plugin/plugin.json +3 -2
  11. package/plugins/trailhead/commands/panel.md +10 -0
  12. package/plugins/trailhead/hooks/hooks.json +3 -18
  13. package/plugins/trailhead/hooks/lib/body-sources.js +795 -0
  14. package/plugins/trailhead/hooks/lib/gh-subcommand.js +92 -10
  15. package/plugins/trailhead/hooks/lib/gh-subcommand.test.js +55 -0
  16. package/plugins/trailhead/hooks/lib/mockup-link.js +21 -5
  17. package/plugins/trailhead/hooks/lib/shell-scan.js +496 -64
  18. package/plugins/trailhead/hooks/lib/shell-scan.test.js +420 -0
  19. package/plugins/trailhead/hooks/lib/unmodelled.js +345 -0
  20. package/plugins/trailhead/hooks/lib/unmodelled.test.js +173 -0
  21. package/plugins/trailhead/hooks/lib/work-state.test.js +252 -0
  22. package/plugins/trailhead/hooks/mod/README.md +187 -0
  23. package/plugins/trailhead/hooks/mod/map-pane-view.ts +453 -0
  24. package/plugins/trailhead/hooks/mod/map-pane.tsx +663 -0
  25. package/plugins/trailhead/hooks/mod/register.tsx +280 -0
  26. package/plugins/trailhead/hooks/mod/work-state-prompt.ts +69 -0
  27. package/plugins/trailhead/hooks/mod/work-state-view.ts +93 -0
  28. package/plugins/trailhead/hooks/trailhead-bash-guards.js +140 -0
  29. package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +233 -0
  30. package/plugins/trailhead/hooks/trailhead-body-guard.js +145 -61
  31. package/plugins/trailhead/hooks/trailhead-body-guard.test.js +224 -3
  32. package/plugins/trailhead/hooks/trailhead-commit-guard.js +41 -14
  33. package/plugins/trailhead/hooks/trailhead-commit-guard.test.js +2 -1
  34. package/plugins/trailhead/hooks/trailhead-install-guard.js +22 -12
  35. package/plugins/trailhead/hooks/trailhead-install-guard.test.js +2 -1
  36. package/plugins/trailhead/hooks/trailhead-mockup-link-guard.js +48 -11
  37. package/plugins/trailhead/hooks/trailhead-mockup-link-guard.test.js +79 -7
  38. package/plugins/trailhead/hooks/trailhead-mockup-link-stop.test.js +1 -1
  39. package/plugins/trailhead/hooks/trailhead-search-guard.js +19 -9
  40. package/plugins/trailhead/hooks/trailhead-search-guard.test.js +2 -1
  41. package/plugins/trailhead/hooks/trailhead-secret-guard.js +541 -57
  42. package/plugins/trailhead/hooks/trailhead-secret-guard.test.js +1000 -4
  43. package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +171 -6
  44. package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +170 -0
  45. package/plugins/trailhead/skills/_shared/choices.md +3 -1
  46. package/plugins/trailhead/skills/_shared/configuration-reference.md +5 -4
  47. package/plugins/trailhead/skills/_shared/configuration.md +1 -1
  48. package/plugins/trailhead/skills/_shared/multi-map.md +1 -1
  49. package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
  50. package/plugins/trailhead/skills/_shared/teamwork.md +4 -2
  51. package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +1 -1
  52. package/plugins/trailhead/skills/_shared/techniques/prototype.md +15 -16
  53. package/plugins/trailhead/skills/_shared/techniques.md +1 -1
  54. package/plugins/trailhead/skills/trailhead/SKILL.md +2 -2
  55. package/plugins/trailhead/skills/trailhead-work/SKILL.md +3 -2
  56. package/plugins/trailhead/skills/trailhead-work/references/auto.md +2 -0
  57. package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +3 -1
  58. package/plugins/trailhead/templates/trailhead-work-state.js +199 -0
  59. package/plugins/trailhead/tests/map-pane-ui.test.ts +1374 -0
  60. package/plugins/trailhead/tests/map-pane-view.test.ts +565 -0
  61. package/plugins/trailhead/tests/mod.test.ts +14 -0
  62. package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +207 -0
  63. package/plugins/trailhead/tests/work-state-prompt.test.ts +109 -0
  64. package/plugins/trailhead/tests/work-state-ui.test.ts +365 -0
  65. package/plugins/trailhead/tests/work-state-view.test.ts +86 -0
  66. package/plugins/trailhead/types/index.d.ts +69 -0
@@ -8,36 +8,35 @@ If genuinely ambiguous and the user's unreachable, default by context (backend m
8
8
 
9
9
  **For UI tickets specifically:** before writing any real UI code, **ask the user whether they want mockups first** (default yes for a new screen). If yes, route by `config.design` (see [Configuration](../configuration.md#configuration)). This ask is an advisory choice that **offers** the delegate option, per the injection recipe in `../choices.md`.
10
10
 
11
- **Route strictly by `config.design`; disk is not a shortcut.** Read `config.design` (and its cached `design.project`) *before* producing anything, and obey it. When it names a hosted mode (`claude.ai/design` or `stitch`), the mockup **must** be built there, reusing the cached `design.project` when present. **Producing a local disk mockup while `config.design` names a hosted mode is a defect**, never a convenient default: `disk` is used only when `config.design` is literally `disk`, or as the explicit, announced fallback the chain below reaches after the hosted mode's MCP offer is declined or unavailable. If you cannot reach the configured mode, run its offer/fallback and **tell the user why**; never silently shortcut to disk. When unsure of the mode, re-read this technique before routing, do not guess.
11
+ **Route strictly by `config.design`; disk is not a shortcut.** Read `config.design` (and its cached `design.project` / `design.system`) *before* producing anything, and obey it. When it names a hosted mode (`artifacts` or `stitch`), the mockup **must** be built there, reusing the cached `design.project` when present. **Producing a local disk mockup while `config.design` names a hosted mode is a defect**, never a convenient default: `disk` is used only when `config.design` is literally `disk`, or as the explicit, announced fallback the chain below reaches when the hosted mode is unavailable or its offer is declined. If you cannot reach the configured mode, run its offer/fallback and **tell the user why**; never silently shortcut to disk. When unsure of the mode, re-read this technique before routing, do not guess.
12
+
13
+ **Legacy value `claude.ai/design`.** The standalone Claude Design site (claude.ai/design) closes on 2026-12-14 and its designs now live in Claude as **Artifacts**. A config still reading `design: claude.ai/design` is treated as **`artifacts`**: say so once per session and offer to rewrite the key (`/trailhead:config`). A cached `design.project` that is a claude.ai/design project id or `claude.ai/design/p/<id>` URL **cannot be reused** (standalone projects don't migrate automatically): tell the user once, point them to the migration (design systems: the **Artifacts** page banner "Migrate team design systems"; projects: download the archive from the standalone site, or ask trailhead to rebuild the screens on a canvas), and attach a new canvas as below. **Make the re-attach one click:** when offering the canvas (and, if unset, `design.system`), first list the Design canvases and Design System Artifacts (`list` with `type: "Design"` / `"Design System"`) whose title matches the repo's or the product's name (e.g. `ZipTv2 — Mockup`, `ZipTv2`), as the pre-selected options, before the generic pick / paste / create; on the user's pick write `design.project` / `design.system` and drop the legacy `design.surface` in the same config write. Never call the `claude-design` MCP or DesignSync to make a mockup: DesignSync is only for the user-started `/design-sync` skill.
12
14
 
13
15
  - **`disk`** (the default config *value*, not a fallback to pick at will): a throwaway static HTML mockup next to the code, linked from the ticket as an asset (on a throwaway branch, out of main). Fast, self-contained.
14
- - **`claude.ai/design` mode**: build the mockup on claude.ai/design, where the user edits it visually. `config.design.surface` picks which surface (unset, a cached `design.project` infers `design-system`, so an existing project keeps its surface; see [Configuration](../configuration.md#configuration)). The two surfaces are reached by **different mechanisms** (see below): `design-system` is driven by the DesignSync MCP, while `canvas` is **not** MCP-driven (no MCP call creates or drives a regular claude.ai/design canvas project). On the **first** UI screen for this map you attach to a canvas or a project; **cache its id/URL in the project `.trailhead/config.json` under `design.project`** and **later screens reuse** it. **Link the specific screen** back from the ticket: the canvas **Artifact** URL (or a hand-made web-app project's `claude.ai/design/p/<id>`), or the design-system project's `claude.ai/design/p/<projectId>/…` URL, not merely the project. **With several screens, link each screen separately** (its own artboard/file URL), not only the index screen.
15
- - **`canvas` (default)**: a one-off screen to react to, editable visually. **No MCP creates or drives a claude.ai/design canvas project** (the `claude-design` MCP only creates `PROJECT_TYPE_DESIGN_SYSTEM` projects), so a canvas is produced one of two real ways: (a) the built-in **`/design` skill** → a canvas published as an **Artifact** (`claude.ai/code/artifact/<id>`), a preview of the Claude Design editor inside Claude Code, editable and exportable there; this is the default and the only in-session path. (b) a regular claude.ai/design project **created by hand in the web app**, whose `claude.ai/design/p/<id>` URL you then **paste in** to attach. Cache the Artifact URL / pasted `/p/<id>` in `design.project`; later screens are added to the same Artifact/project. **When the canvas holds more than one screen, maintain an index (overview) screen and keep it in sync:** every time you produce or add a screen, regenerate the index from the **current** screen set so it lists the new one too (never leave a stale index behind that misses later screens), size its per-screen tiles/thumbnails **large enough to read comfortably** (a legible preview, not a postage stamp), and give each screen its **own standalone artboard/file, linkable on its own outside the index** (openable directly, not only through an index tile). **Do not** try to create a canvas project via the MCP: no such call exists.
16
- - **`design-system`**: push the mockup as a component into a **design-system project** via the **`/design-sync` skill + `claude-design` MCP (DesignSync)**, the design-system slice of the same official MCP, for a shared component library kept in sync. On the first screen, three paths: (a) **pick an existing one**, listing the **10 most-recently-updated** design systems from DesignSync `list_projects` (sorted `updatedAt` desc); (b) **paste a URL**, taking the projectId from a `claude.ai/design/p/<id>` link and confirming with `get_project` that it's a `PROJECT_TYPE_DESIGN_SYSTEM`; (c) **create a new one** (name from the user), then `create_project`. Later screens reuse it (`finalize_plan` → `write_files`). **DesignSync only targets `PROJECT_TYPE_DESIGN_SYSTEM` projects**: verify with `get_project` before pushing; a regular `PROJECT_TYPE_PROJECT` **cannot** be driven by DesignSync (nor by any MCP); reach a regular claude.ai/design project through the `canvas` surface instead (the `/design` skill's Artifact, or a hand-made web-app project).
17
- - **Missing MCP (`design-system` only): offer to install it once, don't degrade silently.** When `design: claude.ai/design` is configured with the **`design-system`** surface but its MCP (DesignSync) isn't connected, **surface the enable steps once** before falling back: `claude mcp add --scope user --transport http claude-design https://api.anthropic.com/v1/design/mcp`, then `/design-login`. Let the user **install-and-retry or decline**; never auto-install. If they install and it connects, use the surface. If they decline (or it still doesn't connect), fall to `disk` and **don't re-offer for the rest of the session**. This is a one-time **infrastructure** prompt, **orthogonal to `design.approval`**: `auto` only skips the wait on *mockup* approval, it never silences the install offer (an MCP can't be auto-installed), though a decline degrades without blocking, in keeping with `auto`. The **`canvas`** surface needs **no MCP** (the `/design` skill is built into Claude Code; the web-app path needs only a browser), so there is nothing to install or offer for it.
18
- - **Fallback chain** once the offer is declined or doesn't apply: the **`design-system`** surface needs DesignSync connected; if it isn't, fall to `disk`, and say so. The **`canvas`** surface needs no MCP, so on Claude Code it is always available (the `/design` skill) and doesn't fall back for a missing MCP. **On a host without the `/design` skill and the `claude-design` MCP (e.g. Codex), `claude.ai/design` mode isn't available at all** (neither surface). Because **`stitch` mode is cross-host** (below), don't degrade silently there: **offer once to switch this project to `design: stitch`** (with the Stitch install offer if its MCP isn't connected), and only on decline fall back to `disk`. Offer this once per session; on decline, don't re-ask. (On Claude the three modes stay independent explicit choices, no auto-redirect between them.)
19
- - **`stitch` mode**: create/edit the mockup via the **Google Stitch MCP** (`https://stitch.googleapis.com/mcp`), Google's Gemini-powered UI generator. Unlike `claude.ai/design`, the Stitch MCP is a standard HTTP MCP that runs on **both Claude Code and Codex**, so it's the **cross-host** hosted option. `config.design.surface` picks the surface (the same key as claude.ai/design), and on the **first** UI screen for this map you attach to a Stitch project; **cache the chosen projectId in the project `.trailhead/config.json` under `design.project`** and **later screens reuse** it. Because `design.project` is shared with `claude.ai/design`, **verify the cached id belongs to Stitch (`get_project`) before reusing it**, and re-attach if it doesn't. **Link the specific screen** back from the ticket.
16
+ - **`artifacts` mode**: build the mockup as a **Design canvas Artifact** on claude.ai (the `Artifact` tool, built into Claude Code: no MCP to install, no login step), where the user edits it visually, comments on it and exports it. The canvas follows its Artifact type's own instructions (the `SKILL.md` the create/read result carries): read them and obey them; the essentials are below.
17
+ - **Attach a canvas once per map.** On the **first** UI screen for this map: a cached `design.project` that is an Artifact URL (`claude.ai/artifact/<id>` or `claude.ai/code/artifact/<id>`) → reuse it. Otherwise offer three paths: (a) **pick an existing one** (Artifact `list` with `type: "Design"`, the 10 most recent); (b) **paste a URL**; (c) **create a new one**: `quickstart` with `intent: "design"`, then `publish` with its `type_url`, a `title` (the map's or product's name, from the user, don't invent it) and `auto_open: "after_first_write"`, nothing else. **Cache the canvas URL in the project `.trailhead/config.json` under `design.project`**; later screens are added to the same canvas.
18
+ - **Design system.** `design.system` (a Design System Artifact URL) is installed on the canvas: its `project/tokens.json` copied to `project/ds/<folder>/tokens.json` plus a record in the index's `designSystems`, the way the type's `design-system-components.md` says (and its components mounted when its README names a bundle global). Unset on the first screen: use the account's default design system when `quickstart` marks one; otherwise name the listed systems and ask which (or none), and cache the choice in `design.system`. **Screens never get pushed into a design system**: a DS is a read-only reference the canvas installs.
19
+ - **One artboard per screen (and per variant).** Each screen is its own `project/<Name>.dc.html` (names: letters, digits, `_ . -`, no spaces; the format rules in the type's `format.md`, above all the exact `<script src="./support.js"></script>` head line, all UI as markup, never script-built); structurally different variants sit side by side as separate artboards. Group a ticket's screens in a canvas **page** (`pages` entry `t<issue#>`, named after the ticket) so each ticket's mockups are browsable on their own; the canvas itself is the overview, no hand-made index screen. App screens get a fixed root (`w`×`h`, e.g. phone 390×844); web pages and dashboards are fluid with `expand: "fill"`. An artboard is at most 8000px tall: split a longer screen into consecutive artboards.
20
+ - **Publishing.** Write the files under one scratchpad folder at their canvas paths and publish with `url`, `root` = the folder holding `project/`, `file_path` = `project/canvas.json` (sent only when boards, pages or notes change, after re-reading it), `files` = the changed artboards. **Before publishing to a canvas this session didn't create, `read` its whole URL first** (a publish to an unread artifact is refused). Upload images with `asset: true` and use the returned `/_blob/<id>` url; no other network than Google Fonts.
21
+ - **Host availability and fallback.** The `Artifact` tool exists only on Claude Code signed in with a claude.ai account. When it isn't available (Codex, or an API-key session), `artifacts` mode can't run: because **`stitch` mode is cross-host** (below), **offer once to switch this project to `design: stitch`** (with the Stitch install offer if its MCP isn't connected), and only on decline fall back to `disk`, saying so. Offer once per session; on decline, don't re-ask.
22
+ - **`stitch` mode**: create/edit the mockup via the **Google Stitch MCP** (`https://stitch.googleapis.com/mcp`), Google's Gemini-powered UI generator. The Stitch MCP is a standard HTTP MCP that runs on **both Claude Code and Codex**, so it's the **cross-host** hosted option. `config.design.surface` picks the surface, and on the **first** UI screen for this map you attach to a Stitch project; **cache the chosen projectId in the project `.trailhead/config.json` under `design.project`** and **later screens reuse** it. Because `design.project` is shared with `artifacts`, **verify the cached id belongs to Stitch (`get_project`) before reusing it**, and re-attach if it doesn't. **Link the specific screen** back from the ticket.
20
23
  - **`canvas` (default)**: a Stitch project with generated screens to react to: `generate_screen_from_text` for the first screen (or `generate_variants` for structurally different alternatives), `edit_screens` to refine, `get_screen` / `list_screens` to read back. On the first screen ask which Stitch project to attach to: (a) **pick an existing one** (`list_projects`); (b) **paste a project URL**; (c) **create a new one** (`create_project`, name from the user, don't invent it). Later screens are added to the same project.
21
24
  - **`design-system`**: push the mockup into a **Stitch design system** (`create_design_system` / `create_design_system_from_design_md` / `apply_design_system`, read/update via `list_design_systems` / `update_design_system`) for a shared component library. On the first screen: (a) **pick an existing one** (`list_design_systems`); (b) **paste a URL**; (c) **create a new one** (name from the user). Later screens reuse it.
22
- - **Missing MCP: offer to install it once** (the same rule as claude.ai/design): if the Stitch MCP isn't connected, surface the enable step once, `claude mcp add --scope user --transport http stitch https://stitch.googleapis.com/mcp` (authenticate if prompted), and let the user install-and-retry or decline; never auto-install. On decline (or a still-missing MCP), fall back to `disk`, and say so. There's no intra-provider chain: the Stitch MCP either connects or falls to `disk`. One-time infrastructure prompt, **orthogonal to `design.approval`** (see above).
23
-
24
- **Publishing a screen to a `design-system` surface is not done until its index is updated and no loose copy is left (either provider).** Every push of a screen into a design system (DesignSync `finalize_plan` → `write_files`, or Stitch `apply_design_system` / `update_design_system`) must, as part of the *same* publish: (a) **update the design-system index** to list the new screen, regenerating it from the current screen set so nothing is missed, so the design system stays the single browsable source of truth; and (b) **treat the indexed screen as the sole artifact**, never also leaving behind or handing back a separate loose `.html` (a scratchpad copy) that can drift from the indexed screen. This index update is a **mandatory, verified** step of the publish, not an optional afterthought: confirm the index lists the new screen before treating the publish as done. (The `canvas` surface states the equivalent index rule inline above; this makes the `design-system` branch match it.)
25
+ - **Missing MCP: offer to install it once**: if the Stitch MCP isn't connected, surface the enable step once, `claude mcp add --scope user --transport http stitch https://stitch.googleapis.com/mcp` (authenticate if prompted), and let the user install-and-retry or decline; never auto-install. On decline (or a still-missing MCP), fall back to `disk`, and say so. One-time infrastructure prompt, **orthogonal to `design.approval`**: `auto` only skips the wait on *mockup* approval, it never silences the install offer.
25
26
 
26
- **Author full-viewport mockups with an intrinsic height, not `100vh` (any card-rendered surface).** A design-system pane renders each screen as a card with no real viewport, so a full-viewport app shell sized `height: 100vh` collapses to a thin strip (only the titlebar shows). Give the app shell an intrinsic fixed height instead (e.g. `height: 800px`); a flowing or scrolling page (like an overview index) renders fine because its content gives it real height, but a single-viewport app does not.
27
+ **Publishing a screen to a Stitch `design-system` is not done until its index is updated and no loose copy is left.** Every push of a screen into a Stitch design system (`apply_design_system` / `update_design_system`) must, as part of the *same* publish: (a) **update the design-system index** to list the new screen, regenerating it from the current screen set so nothing is missed; and (b) **treat the indexed screen as the sole artifact**, never also leaving behind a separate loose `.html` that can drift from it. Confirm the index lists the new screen before treating the publish as done.
27
28
 
28
- **Two DesignSync mechanics the publish must get right (DesignSync surface).** Specific to a DesignSync design system, alongside the shared index rule above:
29
- - **Screen files at project ROOT, not in subfolders, even when a twin already lives in one.** The Design System pane lists only root-level `.html` as openable Pages; a screen in a subfolder (`chat-states/loading.html`) opens **blank** and is never offered in the Pages menu. The manifest / `@dsCard` still embeds such a screen as a thumbnail in `index.html`, but that is the index, not the Pages menu, and there is no way today to surface a subfolder screen in Pages. So **always publish each screen as a flat root file**, and do **not** mirror an existing project's subfolder grouping: if a sibling screen already sits under a folder (`chat-states/`), that placement is the anomaly to avoid (its own Page is unreachable too), not a convention to inherit. The legitimate grouping need (browse the "Chat states" screens together) is served by the **index**, which groups the thumbnails via the manifest / `@dsCard`: grouping is an index/browsing concern, never a filesystem-folder one. Every openable Page stays a flat root `.html`.
30
- - **Verify the index against the real `_ds_manifest.json`.** `register_assets` does **not** rewrite an existing manifest, so after any screen add, move, rename, or delete the manifest goes **stale** (keeps listing old or deleted paths). After such a change, read `_ds_manifest.json` and confirm it lists the current flat screen paths, rewriting it directly if not. This is the concrete, verified form of the index-update step above.
29
+ **Author full-viewport mockups with an intrinsic height, not `100vh`.** Canvas artboards and design-system cards render each screen with no real viewport, so a full-viewport app shell sized `height: 100vh` collapses or clips. Give the app shell an intrinsic fixed height instead (e.g. `height: 800px`); a flowing or scrolling page renders fine because its content gives it real height.
31
30
 
32
31
  Either way the rough screen is **reacted to before real UI code**.
33
32
 
34
- **"Surface the mockup" means: put its link in the same message that asks for the reaction.** The user can't react to what they can't open. Every message that asks to approve, react to, or pick between mockups **must carry, in that same message and right before the question**, the direct clickable location of each screen it refers to: the file path (`disk`), the Artifact / claude.ai/design project URL (`canvas`), the design-system project URL plus the screen's page (`design-system`), or the Stitch project/screen URL (`stitch`), one line per screen or variant. A link given earlier in the conversation, only in a ticket comment, or implied ("the mockup above") does **not** count: scrollback scrolls away. **Write the link(s) in the plain prose of the message, never inside a structured prompt**: an options picker renders its question and options as plain text, so a link put there is not clickable (above all on mobile). When the approval then goes through a structured prompt, keep every link in the prose just before it and let the picker ask only a plain yes/no: approve the mockup (yes) or request changes (no); for a pick between variants, name each variant in the options and keep their links in the prose. If you don't have a link to show yet (publish failed, URL not returned), **do not ask for approval**: fix the publish or report the failure first. The project's or design system's **name is not a link** ("the mockup is ready on MyApp" without the URL fails this rule), and the link must sit in the reply **text**, not only in your reasoning. A hook enforces this: on Claude Code the **mockup-link guard** blocks a mockup-approval `AskUserQuestion` whose preceding reply text carries no URL or file path; on Codex **mockup-link-stop** continues a final reply that asks for the approval without one. When either fires, write the link(s) in the prose and ask again.
33
+ **"Surface the mockup" means: put its link in the same message that asks for the reaction.** The user can't react to what they can't open. Every message that asks to approve, react to, or pick between mockups **must carry, in that same message and right before the question**, the direct clickable location of each screen it refers to: the file path (`disk`), the canvas Artifact URL plus the **artboard title** of each screen (`artifacts`: one canvas URL, then one line per screen or variant naming its artboard and page, since artboards have no URL of their own), or the Stitch project/screen URL (`stitch`). A link given earlier in the conversation, only in a ticket comment, or implied ("the mockup above") does **not** count: scrollback scrolls away. **Write the link(s) in the plain prose of the message, never inside a structured prompt**: an options picker renders its question and options as plain text, so a link put there is not clickable (above all on mobile). When the approval then goes through a structured prompt, keep every link in the prose just before it and let the picker ask only a plain yes/no: approve the mockup (yes) or request changes (no); for a pick between variants, name each variant in the options and keep their links in the prose. If you don't have a link to show yet (publish failed, URL not returned), **do not ask for approval**: fix the publish or report the failure first. The project's or canvas's **name is not a link** ("the mockup is ready on MyApp" without the URL fails this rule), and the link must sit in the reply **text**, not only in your reasoning. A hook enforces this: on Claude Code the **mockup-link guard** blocks a mockup-approval `AskUserQuestion` whose preceding reply text carries no URL or file path (it also accepts a link carried by the approval question itself, but the prose stays the preferred form since a picker's text is not clickable); on Codex **mockup-link-stop** continues a final reply that asks for the approval without one. When either fires, write the link(s) in the prose and ask again.
35
34
 
36
35
  How approval gates the build depends on `config.design.approval`:
37
36
  - **`explicit` (default)**: surface the mockup and **wait for the user's clear go-ahead**; do not write UI code until they approve. Record the approval on the ticket, then proceed.
38
37
  - **`auto`**: surface the mockup and **proceed without blocking** on a confirmation (the user can still object). Record the direction on the ticket.
39
38
 
40
- **In `claude.ai/design` mode, re-fetch the design before you build on it.** Between surfacing the mockup and writing the UI code the user may have edited it directly on claude.ai/design, so once it's approved (or, in `auto`, right before you consume it), pull the **current** state before building from it: on the **`design-system`** surface via DesignSync (`list_files` → `get_file`); on the **`canvas`** surface re-read the Artifact (or the web-app project) directly, there is no MCP fetch. Build from *that*, never from the version you last pushed. Note on the ticket that the design was re-fetched at that point.
39
+ **In a hosted mode, re-fetch the design before you build on it.** Between surfacing the mockup and writing the UI code the user may have edited it directly (on the canvas or in Stitch), so once it's approved (or, in `auto`, right before you consume it), pull the **current** state before building from it: in `artifacts` mode `read` the canvas's `project/canvas.json` and each approved artboard's `project/<Name>.dc.html`; in `stitch` mode `get_screen`. Build from *that*, never from the version you last published. Note on the ticket that the design was re-fetched at that point.
41
40
 
42
41
  **Once a mockup is approved, reproduce it with 100% fidelity, not an approximation.** The approved mockup is the spec, not a mood board: match its layout, spacing, sizing, colour, typography, copy, and **every state it shows** (empty, loading, error, hover, active, disabled, and the like). "In the spirit of" the mockup is not the bar; faithful to the pixel is. This is not licence to improve or reinterpret it while building: if something in the approved mockup is genuinely infeasible, wrong, or missing a state, **stop and raise it with the user** and get the mockup (or the decision) updated, never silently deviate. An unforced departure from an approved mockup is a defect, and Verify checks the built screen against the approved mockup, not against a loose retelling of it: **Acceptance testing** runs the element-by-element and state-by-state mockup-fidelity check, and **Goal-backward verification** carries fidelity as an explicit goal claim (see their technique files).
43
42
 
@@ -10,7 +10,7 @@ The ticket engines call these by name. Each technique's full protocol lives in i
10
10
  |---|---|---|
11
11
  | **Grilling** | `techniques/grilling.md` | interrogate the human, one question at a time, to converge on a decision |
12
12
  | **Domain vocabulary** | `techniques/domain-vocabulary.md` | build a precise shared glossary so each term means one thing |
13
- | **Prototype** | `techniques/prototype.md` | throwaway artifact for "how should it look/behave"; routes UI mockups (disk / claude.ai/design / stitch) |
13
+ | **Prototype** | `techniques/prototype.md` | throwaway artifact for "how should it look/behave"; routes UI mockups (disk / artifacts / stitch) |
14
14
  | **Research** | `techniques/research.md` | a focused subagent gathers a decision-ready fact from primary sources |
15
15
  | **TDD** | `techniques/tdd.md` | RED → GREEN → REFACTOR at the seams; no implementation before a failing test |
16
16
  | **Codebase map** | `techniques/codebase-map.md` | one-time fan-out of 5 read-only readers, distilled into the repo's `trailhead:codebase` issue |
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: trailhead
3
3
  description: "Start and drive a large project (more than one agent session can hold) as a map of decision tickets on GitHub Issues, resolving one at a time until the way to the destination is clear. Self-contained (zero external-plugin dependencies: trailhead ships its own `trailhead-*` engine agents and needs only an authenticated gh CLI): map-based onboarding and a discuss→plan→execute→verify engine with atomic commits, with everything (map, discussion, plan, verification) living on the Issues. Invoke only when the user explicitly runs `/trailhead` (or a `/trailhead:<verb>` command) or asks to chart, adopt, or work a trailhead map, and do not auto-invoke for generic planning, project, or task-tracking requests."
4
- argument-hint: "[new|adopt|work|quick|whiteboard|ticket|inbox|map|dashboard|config|grill|split|pause|resume|auto|update|prune|audit|bug|todo|idea|seed|note] [text | ticket]"
4
+ argument-hint: "[new|adopt|work|quick|whiteboard|ticket|inbox|map|dashboard|config|grill|split|pause|resume|auto|update|prune|audit|panel|bug|todo|idea|seed|note] [text | ticket]"
5
5
  ---
6
6
 
7
7
  `trailhead` charts a large project as a **shared map on GitHub Issues**, then works its **tickets** one at a time until the way to the destination is clear. This skill is the **entry point**: bare `/trailhead` runs **smart entry** (detect the repo state and propose the next move); a verb runs that verb's engine. Everything lives on the Issues; the repo holds code only.
@@ -15,7 +15,7 @@ Before doing anything, read `../_shared/load-first.md` and follow it: the shared
15
15
  The **first word** of the arguments is the verb; the rest is the text or a ticket number. `/trailhead <verb>` and `/trailhead:<verb>` are equivalent.
16
16
 
17
17
  - **No verb** to **smart entry** (below): this skill handles it directly.
18
- - **A verb** to hand off to the skill that owns it, passing the whole argument string. **The skill-split migration ([map #53](https://github.com/ToRvaLDz/trailhead/issues/53)) lands the cluster skills one at a time.** The **chart cluster is built (#59)**: `new`, `adopt`, `ticket`, `inbox`, and `grill` route to the **`trailhead-chart`** skill (call the Skill tool with skill name `trailhead-chart` and arguments `<verb> <rest>`). The **work cluster is built (#60)**: `work`, `quick`, `pause`, `resume`, `split`, and `auto` route to the **`trailhead-work`** skill (call the Skill tool with skill name `trailhead-work` and arguments `<verb> <rest>`). The **view cluster is built (#61)**: `map`, `dashboard`, and `whiteboard` route to the **`trailhead-view`** skill (call the Skill tool with skill name `trailhead-view` and arguments `<verb> <rest>`). The **capture cluster is built (#62)**: `bug`, `todo`, `idea`, `seed`, and `note` route to the **`trailhead-capture`** skill (call the Skill tool with skill name `trailhead-capture` and arguments `<verb> <rest>`). The **manage cluster is built (#63)**: `config`, `update`, `prune`, and `audit` route to the **`trailhead-manage`** skill (call the Skill tool with skill name `trailhead-manage` and arguments `<verb> <rest>`). Then carry out the target skill's instructions.
18
+ - **A verb** to hand off to the skill that owns it, passing the whole argument string. **The skill-split migration ([map #53](https://github.com/ToRvaLDz/trailhead/issues/53)) lands the cluster skills one at a time.** The **chart cluster is built (#59)**: `new`, `adopt`, `ticket`, `inbox`, and `grill` route to the **`trailhead-chart`** skill (call the Skill tool with skill name `trailhead-chart` and arguments `<verb> <rest>`). The **work cluster is built (#60)**: `work`, `quick`, `pause`, `resume`, `split`, and `auto` route to the **`trailhead-work`** skill (call the Skill tool with skill name `trailhead-work` and arguments `<verb> <rest>`). The **view cluster is built (#61)**: `map`, `dashboard`, and `whiteboard` route to the **`trailhead-view`** skill (call the Skill tool with skill name `trailhead-view` and arguments `<verb> <rest>`). The **capture cluster is built (#62)**: `bug`, `todo`, `idea`, `seed`, and `note` route to the **`trailhead-capture`** skill (call the Skill tool with skill name `trailhead-capture` and arguments `<verb> <rest>`). The **manage cluster is built (#63)**: `config`, `update`, `prune`, and `audit` route to the **`trailhead-manage`** skill (call the Skill tool with skill name `trailhead-manage` and arguments `<verb> <rest>`). The **`panel`** verb routes to no skill: it is answered by the trailhead mod on Claude Code 2.1.287+ (`/trailhead:panel` opens or closes the live map panel), and where no mod answers, say in one line that the panel is not available in this session. Then carry out the target skill's instructions.
19
19
  - **Unrecognised verb** to treat the whole string as `idea` (route to `trailhead-capture` as `idea <text>`).
20
20
 
21
21
  The namespaced `/trailhead:<verb>` commands reach their target skill directly through their own command wrapper; this routing table only covers the bare `/trailhead <verb>` form.
@@ -37,13 +37,14 @@ This guard is the **explicit-number paths only**. Bare `work` selects from the l
37
37
 
38
38
  The user invokes with a map (URL or number). A ticket is optional: without one, you pick the next decision.
39
39
 
40
- 1. Load the **map** (the low-res view, not every ticket body). **Pick which map** if the repo has several open: use the one the user named, else the active map (`.trailhead/active-map`); if neither and more than one is open, list them and ask. Set/refresh the active-map marker to the one you're working. See **Multiple maps on one repo** in `../_shared/multi-map.md`. **Codebase issue check (greenfield, offer once):** if the repo has **no `trailhead:codebase` issue** (a greenfield map never got one) and now holds **substantial code**, **don't interrupt the work to offer now**: mark the offer **pending** and surface it **at the handoff**, just before the `/clear`-first next-step block (not as a line inside it: see the **pending codebase offer** rule in `../_shared/session-handoff.md`), exactly once. On the user's yes, generate it via the **Codebase map** technique, then pin it (one of the three permanent repo pins: dashboard, codebase, conventions) and link it from the map's Notes; if the user declines, don't nag again this session. A repo that already has the issue, or is still too thin to map, skips this.
40
+ 1. Load the **map** (the low-res view, not every ticket body). **Pick which map** if the repo has several open: use the one the user named, else the active map (`.trailhead/active-map`); if neither and more than one is open, list them and ask. Set/refresh the active-map marker to the one you're working. **Named ticket:** its map becomes the active map, a whiteboard ticket leaves it alone (see **Active map** in `../_shared/multi-map.md`, under **Multiple maps on one repo**). **Codebase issue check (greenfield, offer once):** if the repo has **no `trailhead:codebase` issue** (a greenfield map never got one) and now holds **substantial code**, **don't interrupt the work to offer now**: mark the offer **pending** and surface it **at the handoff**, just before the `/clear`-first next-step block (not as a line inside it: see the **pending codebase offer** rule in `../_shared/session-handoff.md`), exactly once. On the user's yes, generate it via the **Codebase map** technique, then pin it (one of the three permanent repo pins: dashboard, codebase, conventions) and link it from the map's Notes; if the user declines, don't nag again this session. A repo that already has the issue, or is still too thin to map, skips this.
41
41
  2. Choose the ticket. **If the user named a ticket, the Preflight above already confirmed it is open** (a closed, superseded, out-of-scope, or nonexistent number stopped there, before any setup ran). If the user names one, use it (a **named** ticket may be a `trailhead:whiteboard` one: `work <n>` works it whole, off the map, with the normal cycle and split allowed; use `quick <n>` for the no-split variant). Otherwise take a frontier ticket to act on, **scoped to this map** (add its `trailhead:map-<n>` label to the frontier query when several maps are live) and **never a whiteboard ticket** (bare `work` stays on the map; the whiteboard frontier is reached via `/trailhead:whiteboard` and `quick`). Bare `work` may act on the first ticket the query returns, but that order is `gh`'s newest-first, not a priority: present several independent tickets as a **set to choose from**, not the graph's natural next step (see **Frontier order carries no priority** in `../_shared/substrate.md`). **Claim** it: assign it to yourself before any work, then re-read the assignee before starting; on a collision, stop and ask the user (see `../_shared/teamwork.md`). If the ticket proves too big once you're in it, propose a **split** rather than grind, but never split on your own: present the children you would create and wait for the user's go-ahead before creating anything (same file).
42
42
  - **First, resolve the effective isolation mode.** The `isolation:` header defaults to **`auto`** (an absent key means `auto`; the engine never writes the key). Under **`auto`**, at work-start **before any file edit or diagnostic probe**, first run the read-only **assigned-ticket pre-check** (full rule in `../_shared/teamwork.md`): other open tickets already assigned to you **pre-bias** the ask toward `worktree`/`clone` (still asking, easy neutral), and the ask is **skipped** only when the session is already inside this ticket's own linked worktree (`trailhead/t<N>`, `N` the current ticket). Then ask the user once whether they will work more than one ticket at once on this machine this session. **No** (a lone ticket): the mode resolves to the current checkout, exactly like `none` (the scope-free check below applies). **Yes**: resolve by repo shape, a plain repo -> `worktree`, a **superrepo** (a `.gitmodules` at the checkout root declaring at least one submodule) -> `clone`; that "yes" **is** the clone gate's answer, so the resolved `clone` path proceeds without re-asking. Because trailhead resolves one ticket per session, this asks at most once per ticket work-start (so once per session in normal work; a within-session ticket switch re-asks at the new ticket's work-start), and an autonomous run (`/trailhead:auto`) never asks and never runs the pre-check (sequential work, no concurrency) and resolves to the current checkout. If the header carries an **explicit** `none`/`worktree`/`clone`, honour it as-is (never subject it to auto's ask): explicit `clone` keeps its own gate, explicit `worktree` is eager. See `../_shared/teamwork.md`.
43
43
  - **When the resolved mode is the current checkout (`isolation: none`, or `auto` answered no), check the scope is free before starting** (this is how a submodule/package gets serialised when worktrees don't fit): scan the other in-progress tickets' `Scope:` lines, and if one **overlaps** this ticket's scope, **stop and tell the user** the scope is busy (name the ticket + holder) and offer a disjoint-scope frontier ticket or to wait. See `../_shared/teamwork.md`.
44
44
  - **Write the session-ticket marker**: at the **working root** (the checkout, worktree, or clone you'll edit in), write `.trailhead/session-ticket` as **one line** `#<n> <ticket title>`. **Keep it gitignored** (add `.trailhead/session-ticket` to that repo's `.gitignore` if absent): it's per-session local state, **never committed**. It lets an external tool show "what am I on" offline (the tracker stays the source of truth), but it is not merely that cheap hint: while it is present, the git commit-msg hook also requires every commit to carry a `Refs: #<n>` trailer matching it, so a stale marker left behind would wrongly demand that trailer on unrelated commits. Remove it at Resolve/handoff (see `../_shared/session-handoff.md`); on **Pause** it may stay (you'll return) or be cleared if you release the claim. The **`.trailhead/active-map`** marker (which map this session works) follows the same rules: gitignored, per-session, never committed; see **Multiple maps on one repo** in `../_shared/multi-map.md`.
45
45
  - **Sync the commit-msg hook**: at the working root, run `node <templates>/trailhead-commit-msg-sync.js --repo <working-root>` (where `<templates>` lives and what each status means: the commit-msg hook paragraph in `../_shared/teamwork.md`). Stay silent when it reports installed or current. On **upgraded**, give the user one result-oriented line: this clone's commit-msg hook was updated to the current rules. On **foreign**, one line that the user's own hook was left untouched and git does not enforce trailhead's rules here. On **skipped**, one line that nothing was set up here (a `core.hooksPath` without a trailhead hook, or a working root that is not a git repo) and git does not enforce trailhead's rules. On a missing script or a node error (missing means none of the three template dirs has it: try them all before concluding that), inspect the hooks dir (`git rev-parse --git-path hooks`) and report per the fallback in that same paragraph of `../_shared/teamwork.md` (trailhead hook present and executable: enforced but freshness unchecked, update the install; present but not executable: git skips it, not enforced; foreign hook: untouched, not enforced; no hook: not enforced, update the install), never a blind "not enforced"; it never blocks the work.
46
46
  - **When the resolved mode is `worktree` or `clone` (explicit, or `auto` answered yes), set up the isolated workspace NOW, before any file edit or diagnostic probe** (not later at Execute/Fix): **if the repo has submodules (a `.gitmodules`), `isolation: worktree` is escalated to `clone`** (a superproject worktree cannot safely carry submodule state; see `../_shared/teamwork.md`). For `worktree` (no submodules), create/enter this ticket's `git worktree` on a `trailhead/t<n>` branch for the package/path the ticket's `Scope:` names (a submodule-bearing repo would have escalated to `clone` above), e.g. `git worktree add ../<repo>-t<n> -b trailhead/t<n>`; for `clone`, make an independent working copy at `../<repo>-t<n>` (branch `trailhead/t<n>`) by whichever is faster: `git clone` (add `--recurse-submodules` when the repo has submodules) + the install step, or a **folder copy that brings `node_modules`** (and the submodule working trees) to skip the reinstall (then verify the copy's git is independent, `git -C <copy> rev-parse --absolute-git-dir` resolves inside it, and the same inside every submodule, else commits leak back to the original); a clone with submodules is provisioned via `git submodule update --init --recursive` (nested included), each scoped submodule pinned at the superproject's recorded commit. **If this `clone` was reached via `auto`'s work-start ask (answered yes), that ask already served as the clone gate: do not re-ask, proceed to set up the clone. Under an explicit `isolation: clone`, run the gate as follows.** **Because `clone` is heavy (a full clone + an install), gate it, every time, before cloning: ask the user whether they expect to work more than one ticket at once on this machine this session.** If **no**, **skip the clone** and work this ticket in the current checkout (isolation only exists to stop concurrent workspaces from colliding; a lone ticket has nothing to collide with, so the shared-checkout caveats apply and that is fine, and `git:` still decides the branch as usual); if **yes**, clone as above. The gate is **`clone`-only**: `worktree` is cheap, so set it up without asking. Do every subsequent step in whatever workspace you ended up in. This is the whole point of isolation: if you diagnose and edit in the shared checkout "just to start" while a concurrent workspace exists, you are already colliding. Set the workspace up silently (the `clone` gate-question is the one exception you voice); it's undone at Resolve when the branch integrates and the worktree/clone is removed. See `../_shared/teamwork.md`.
47
+ - **Write the work state, last** (the working root is final only now): `node <templates>/trailhead-work-state.js set --repo <working-root> --map <n> --ticket <n> --title "<title>" --step discuss`, and add `.trailhead/work-state.json` to that repo's `.gitignore` if absent. It is updated at every step change and cleared at Resolve; the canonical description, the `<templates>` lookup, and the silent-on-failure rule are the **Work state file** paragraph in `../_shared/teamwork.md`, and the step mapping per type is in `references/ticket-engines.md`.
47
48
  3. **Brief the ticket and confirm scope, before the engine runs.** With the ticket claimed, read it and give the user a **brief, result-oriented summary of what resolving it will do** (its goal and the shape of the work, not a plan yet), then **ask whether they want to change anything**. This is a HITL confirm gate: on assent, proceed to the engine. **If the user asks for a change, hold it against the ticket.** A change **consistent** with the ticket is folded in and the engine proceeds. A change that **diverges** from the ticket is surfaced as a mismatch, and the user is **asked, never forced**, whether to (a) work the ticket **as-is**, or (b) open a **separate new ticket** for the divergent request. Offer both and decide neither: open the separate ticket **only if the user chooses it** (via **Scope that surfaces while working a ticket**, served by the capture cluster, the same capture-or-split path used for scope that surfaces mid-work), never automatically. **If the user defers the choice rather than settling it here, mark the divergent follow-up pending for the Pending follow-up guard (`../_shared/session-handoff.md`), so it is re-posed at the handoff rather than lost.** **Under an autonomous run (`auto`) this is a process confirm gate**, so it follows the advisory boundary in `references/auto.md`: `auto` proceeds (takes the delegate option) without stopping to brief.
48
49
  4. Resolve it with its type's engine in **`references/ticket-engines.md`**, **zoom as needed**: fetch the full body of related/closed tickets on demand. If in doubt on a `decision` ticket, run **Grilling** + **Domain vocabulary**. If in doubt on a `build`, **stop and ask** (see the Discuss step): never auto-grill.
49
50
  5. Record the resolution: a comment with the answer, `gh issue close`, add the pointer to *Decisions so far*. Then **unblock dependents**: for every ticket this one was blocking, if it was the last open blocker, remove its `trailhead:blocked` label so it graduates onto the frontier.
@@ -59,7 +60,7 @@ It runs the **full engine** (the `build`/`bug` cycle in `references/ticket-engin
59
60
  - **Grill only if needed** (Discuss): never auto-grill; start from Plan and stop to ask only if a blocking ambiguity surfaces, exactly as the `build` Discuss. The user may ask to grill at any time.
60
61
  - **Never splits.** If the ticket turns out large, work it whole anyway: that is the point of `quick`. Do not `split` it and do not spin children. (New scope that surfaces mid-work is still captured out, never folded in, see **Scope that surfaces while working a ticket**, served by the capture cluster.)
61
62
  - **No map narrative book-keeping.** A whiteboard ticket has no map, so there is no `Decisions so far` update, no fog to graduate, no frontier re-scan. Everything else holds: atomic commits with `Refs: #<n>`, TDD / Code review / Acceptance testing per the cycle, the resolution comment + `gh issue close`, and the **Session handoff** (`../_shared/session-handoff.md`). **Exception, `quick <n>` on a *map* ticket: dependent-unblocking still runs** (remove `trailhead:blocked` from any dependent whose last blocker just closed, **Work the map** step 5 above); skipping it would strand those dependents off the frontier for good. Only the *narrative* book-keeping above and the split are skipped, so prefer `work <n>` for a map ticket that has dependents or belongs in the map record.
62
- - **Same isolation setup as `work`.** `quick` **writes the `.trailhead/session-ticket` marker at the working root in every isolation mode** (exactly as **Work the map** step 2, so tooling like the statusline shows what's in play; no active-map marker, though: a whiteboard ticket has no map). Under `auto` it runs the same work-start concurrency ask **before any edit** and resolves to current-checkout / worktree / clone by the same rule; under an explicit `isolation: worktree`/`clone` it sets up the ticket's `trailhead/t<n>` workspace before any edit (including the submodule-forces-`clone` escalation and submodule provisioning) and re-checks the claim; under `isolation: none` (or `auto` answered no), the scope-free check applies too. `quick` also runs the same commit-msg hook sync at the working root, exactly as **Work the map** step 2.
63
+ - **Same isolation setup as `work`.** `quick` **writes the `.trailhead/session-ticket` marker at the working root in every isolation mode** (exactly as **Work the map** step 2, so tooling like the statusline shows what's in play; the active-map marker follows the same rule as `work`: a named ticket's map becomes the active map, a whiteboard ticket leaves it alone, see **Active map** in `../_shared/multi-map.md`). Under `auto` it runs the same work-start concurrency ask **before any edit** and resolves to current-checkout / worktree / clone by the same rule; under an explicit `isolation: worktree`/`clone` it sets up the ticket's `trailhead/t<n>` workspace before any edit (including the submodule-forces-`clone` escalation and submodule provisioning) and re-checks the claim; under `isolation: none` (or `auto` answered no), the scope-free check applies too. `quick` also runs the same commit-msg hook sync at the working root, exactly as **Work the map** step 2. It also writes the work state last, exactly as **Work the map** step 2, with `--map none` for a whiteboard ticket.
63
64
  - **Refreshes the dashboard at the handoff, not mid-work.** A `quick "<text>"` whiteboard ticket is born and resolved in the **same** session, so its dashboard refresh rides with the session's end, not with ticket creation: **do not refresh right after the ticket exists** (that lands a structural pinned-issue write ahead of the work `quick` exists to just-get-done, and only duplicates the resolution refresh a moment later). Refresh at **Resolve** (the handoff), as a **full regeneration from the live tracker** (never a partial patch that leaves other sections knowingly stale), so the whiteboard ticket lands on the dashboard as the session closes. **If the session pauses before Resolve** (`/trailhead:pause`), refresh there instead, so a deliberately-unfinished quick still shows (the read-only renders never rewrite the dashboard body, so nothing else would surface it). `quick <n>` on a *map* ticket does **not** refresh (map ticket progress shows via the native bar). See **The dashboard** and its freshness rule in `../_shared/substrate.md`.
64
65
  - **Always size-triages, independent of `config.effort`.** `quick` is the home of the cost-aware path: reaching for `quick` is itself the signal the piece is small, so it runs the **same size triage and ask-to-skip offer defined canonically under effort in `../_shared/configuration-reference.md`** (the same one the `build`/`bug` engine runs, `references/ticket-engines.md`) **every time, at the start before planning, whatever `config.effort` is set to**. `quick` does not wait for `effort: lean`, and an explicit `effort: standard` does not suppress it: `effort` governs `work` / the map cycle, not `quick`. On a trivial/small ticket it surfaces the one consolidated offer (the plan pass, TDD, cross-AI review, the full code review, the goal-backward verify) and goes lean **only on an explicit yes**; on a large ticket there is no offer and the full cycle runs. The correctness spine (atomic `Refs:` commits, tests/criterion, post-commit self-check, Resolve) is never in the offer, whatever the size or the answer.
65
66
 
@@ -53,4 +53,6 @@ Whatever ends the run (frontier drained or human interrupt), auto emits **one ru
53
53
  - **what resolved** (the tickets auto closed this run);
54
54
  - **what remains**: the **set-aside** tickets (fog, `decision` convergences, out-of-scope rulings, HITL `task` checklists, human-only UAT steps, and rail actions including any **ready-to-integrate branch / PR** under `git: pr`) and **why each needs the human**, the **paused / stuck** tickets from the failure budget, and any **blocked dependents** still waiting.
55
55
 
56
+ Before handing back, if a work state file remains (an interrupted or paused ticket), run `node <templates>/trailhead-work-state.js set --repo <working-root> --auto false`. During the run every work-state `set` adds `--auto true --auto-done <n>`, `n` being the tickets resolved so far in this run (`autoDone` in the file).
57
+
56
58
  Then hand back to the human with the normal session handoff framing (`../../_shared/session-handoff.md`): the human decides what to take next (dissolve fog, make a held decision, authorise a rail action, then re-invoke `/trailhead:auto` to drain more).
@@ -6,6 +6,8 @@ Every ticket is **HITL** (human in the loop, speaking for themselves) or **AFK**
6
6
 
7
7
  Technique names below (**Grilling**, **Domain vocabulary**, **Prototype**, **Research**, **TDD**, **Code review**, **Acceptance testing**, **Debug**) are indexed in `../../_shared/techniques.md`; their full protocols live in `_shared/`, read from `../../_shared/techniques/<name>.md`.
8
8
 
9
+ **Work state (every type).** Keep `.trailhead/work-state.json` current with the `set --step` call in the **Work state file** paragraph of `../../_shared/teamwork.md`: `build` Discuss/Plan/Execute/Verify/Resolve set `discuss`/`plan`/`execute`/`verify`/`resolve`; `bug` Repro and Diagnose set `discuss`, Fix `execute`, Verify `verify`, Resolve `resolve`; `decision`, `prototype` and `task` stay `discuss` until `resolve`. A loop back from Verify to Execute (or Fix) sets `execute` again, and a UAT follow-up (`UAT of:`) sets `verify`. `research` tickets never write it: they run AFK and possibly in parallel, and the single file tracks only the one ticket in play.
10
+
9
11
  ## UAT follow-ups (any type)
10
12
  A ticket whose body carries `UAT of: <parent>` is a UAT follow-up, **whatever its type label**, including a graduated seed (a `trailhead:seed` that dropped `trailhead:seed` + `trailhead:blocked` on graduating, keeping whatever type label it had, or none). `work`, `quick`, and `auto` all dispatch through this file, so this check runs **before** the per-type engines below. It resolves via the **Acceptance testing** technique (`../../_shared/techniques/acceptance-testing.md`), starting with the drive-mode ask; the record is a `VERIFY` comment on the follow-up itself, never on the parent. It closes only on a confirmed pass, with a resolution comment linking back to the parent.
11
13
 
@@ -30,7 +32,7 @@ The ticket that *builds*. Lean cycle, all in **ticket comments**:
30
32
 
31
33
  1. **Discuss**: **never auto-grill.** Start from Plan. If reading the ticket surfaces blocking ambiguity, **stop and ask the user** whether they want a **Grilling** round: don't launch it on your own initiative. Only on their assent run the short round and post a `DISCUSS` comment with the closed choices. The user may also ask for it themselves at any time. Advisory choices raised here **offer** the delegate/defer options, per the injection recipe in `../../_shared/choices.md`.
32
34
  2. **Plan**: a `PLAN` comment: the steps, the seams, the files touched, the verification criteria. Apply **TDD** per `config.tdd` (`seams`/`on`/`off`). Produce this plan via a *planner subagent* on `config.models.plan` (dispatch the **`trailhead-plan`** agent, read-only, passing `config.models.plan` as the model override; inline when the model is unset or equals the session model; Execute then runs as its own subagent, see below). If `config.plan_review` is on, run **Cross-AI plan review** before Execute: send the plan to external AI CLIs and converge on their concerns. Now that the files/seam are known, run the **undeclared-coupling check** (`../../_shared/teamwork.md`) against in-progress tickets. **Plan to sufficiency:** stop once you can name the steps, seams, and files with confidence, and never re-read a file already read in this planning pass; act when you have enough to plan, rather than re-deriving what the pass already established. **The single exception:** under an accepted `lean` skip (see the Size triage above), produce a short inline plan here instead of the planner subagent, and skip Cross-AI plan review; absent that accepted skip, this step runs in full as described.
33
- 3. **Execute**: **if this ticket changes user-facing UI (a new screen, a redesign, a changed layout), run the Prototype technique FIRST, before any real UI code**: surface a mockup to react to (its clickable link always in the same message as the approval question, per the Prototype technique), routed by `config.design` (disk / claude.ai/design / stitch) and gated by `config.design.approval` (`explicit` waits for a go-ahead, `auto` proceeds after surfacing). **Route by `config.design` and never shortcut a hosted mode to a local disk mockup**: when `config.design` is `claude.ai/design` or `stitch`, the mockup is built there (reusing a cached `design.project`); `disk` is only for `config.design: disk` or the technique's explicit, announced fallback, never a convenience default. Read `config.design` before producing anything. For a redesign or new screen the default is to offer mockups; don't jump straight to code on a UI ticket. **Once the mockup is approved, reproduce it with 100% fidelity** (layout, spacing, sizing, colour, type, copy, and every state), never a loose approximation "in the spirit of" it; if part of the approved mockup is genuinely infeasible, stop and raise it rather than silently deviate (full statement in the **Prototype** technique, `../../_shared/techniques/prototype.md`). **Make fidelity verifiable, not a hope:** before finishing Execute, enumerate the approved mockup's elements and states, confirm each is reproduced, and **record that enumerated fidelity list as a ticket comment** (noting any element raised as infeasible with its resolution), so Verify works from a durable checklist instead of reconstructing intent. At Verify, **Acceptance testing** checks the built screen against the approved mockup (element by element, state by state) and **Goal-backward verification** carries fidelity as an explicit goal claim (see `../../_shared/techniques/acceptance-testing.md` and `../../_shared/techniques/verify.md`). Then implement with **atomic commits** (conventional commits, each with a `Refs: #<n>` trailer for this ticket, see **Principles** in `../../_shared/principles.md`; the trailer is also what scopes Verify's review and goal check, per **Ticket diff** in `../../_shared/techniques/code-review.md`), following the repo's `trailhead:conventions` `git:`, straight to `main` by default, or a feature branch + PR when `git: pr`. **Under `isolation: worktree`, do the work in this ticket's `git worktree`** (`trailhead/t<n>` branch), integrating to the trunk at Resolve (see `../../_shared/teamwork.md`). One commit = one verifiable step. Run this via an **executor subagent on `config.models.execute`** (dispatch the **`trailhead-executor`** agent, full tools that can Edit/Write/commit, passing `config.models.execute` as the model override; inline when the model is unset or equals the session model); the commits land on `main`, visible. **Before installing any new dependency, run the verify-before-install gate** (slopsquatting barrier): check the package's legitimacy (name / typosquat similarity to popular packages, age / first-seen date, download counts, repository metadata present and matching) and **stop and ask the user on a suspicious result** (block-and-ask), never installing on your own judgement. On a host with a hook bus the `trailhead-install-guard` `PreToolUse` hook enforces this, blocking a named public-registry install (npm / PyPI / crates.io / Go / RubyGems) until it is vetted; once vetted, proceed by re-running the exact install command prefixed with `TRAILHEAD_VERIFIED_INSTALL=1`. New scope surfacing mid-execute is captured/split, not worked here.
35
+ 3. **Execute**: **if this ticket changes user-facing UI (a new screen, a redesign, a changed layout), run the Prototype technique FIRST, before any real UI code**: surface a mockup to react to (its clickable link always in the same message as the approval question, per the Prototype technique; the guard also accepts a link inside the approval question itself, the prose stays preferred), routed by `config.design` (disk / artifacts / stitch) and gated by `config.design.approval` (`explicit` waits for a go-ahead, `auto` proceeds after surfacing). **Route by `config.design` and never shortcut a hosted mode to a local disk mockup**: when `config.design` is `artifacts` (or the legacy `claude.ai/design`) or `stitch`, the mockup is built there (reusing a cached `design.project`); `disk` is only for `config.design: disk` or the technique's explicit, announced fallback, never a convenience default. Read `config.design` before producing anything. For a redesign or new screen the default is to offer mockups; don't jump straight to code on a UI ticket. **Once the mockup is approved, reproduce it with 100% fidelity** (layout, spacing, sizing, colour, type, copy, and every state), never a loose approximation "in the spirit of" it; if part of the approved mockup is genuinely infeasible, stop and raise it rather than silently deviate (full statement in the **Prototype** technique, `../../_shared/techniques/prototype.md`). **Make fidelity verifiable, not a hope:** before finishing Execute, enumerate the approved mockup's elements and states, confirm each is reproduced, and **record that enumerated fidelity list as a ticket comment** (noting any element raised as infeasible with its resolution), so Verify works from a durable checklist instead of reconstructing intent. At Verify, **Acceptance testing** checks the built screen against the approved mockup (element by element, state by state) and **Goal-backward verification** carries fidelity as an explicit goal claim (see `../../_shared/techniques/acceptance-testing.md` and `../../_shared/techniques/verify.md`). Then implement with **atomic commits** (conventional commits, each with a `Refs: #<n>` trailer for this ticket, see **Principles** in `../../_shared/principles.md`; the trailer is also what scopes Verify's review and goal check, per **Ticket diff** in `../../_shared/techniques/code-review.md`), following the repo's `trailhead:conventions` `git:`, straight to `main` by default, or a feature branch + PR when `git: pr`. **Under `isolation: worktree`, do the work in this ticket's `git worktree`** (`trailhead/t<n>` branch), integrating to the trunk at Resolve (see `../../_shared/teamwork.md`). One commit = one verifiable step. Run this via an **executor subagent on `config.models.execute`** (dispatch the **`trailhead-executor`** agent, full tools that can Edit/Write/commit, passing `config.models.execute` as the model override; inline when the model is unset or equals the session model); the commits land on `main`, visible. **Before installing any new dependency, run the verify-before-install gate** (slopsquatting barrier): check the package's legitimacy (name / typosquat similarity to popular packages, age / first-seen date, download counts, repository metadata present and matching) and **stop and ask the user on a suspicious result** (block-and-ask), never installing on your own judgement. On a host with a hook bus the `trailhead-install-guard` `PreToolUse` hook enforces this, blocking a named public-registry install (npm / PyPI / crates.io / Go / RubyGems) until it is vetted; once vetted, proceed by re-running the exact install command prefixed with `TRAILHEAD_VERIFIED_INSTALL=1`. New scope surfacing mid-execute is captured/split, not worked here.
34
36
  4. **Verify**: a `VERIFY` comment: run the tests / the plan's criterion; then the **Code review** technique, scoped to the ticket's own commits per its **Ticket diff** rule (`../../_shared/techniques/code-review.md`: the `Refs: #<n>` set, read per commit, recomputed each round, an empty set surfaced as an error, never a `<base>...HEAD` range) (its subagent reviews *adversarially*: it doesn't defer to a `CLAUDE.md` convention or a "settled decision" memory), and carries the review's counts line and findings ledger, per code-review.md, **run in the background and collected by polling, never a blind blocking wait** (a hard engine default on every host, see *Reviews never block the session* in `../../_shared/techniques.md`); **read the review's leading `status:` tag to branch deterministically: `status: clean` -> proceed to Resolve, `status: issues_found` -> iterate the fix-and-re-review loop (or run the HITL checkpoint) before resolving**; then the **Goal-backward verification** technique on `config.models.verify` (does the change deliver what the ticket promised, backward from its `Question` and the `PLAN`'s criteria; the read-only `trailhead-verify` subagent, on the same **Ticket diff** set), **run in the background and collected by polling** like Code review, branching on its leading `status:` tag (`status: met` -> proceed, `status: gaps_found` -> loop back to Execute for the gap or run the HITL checkpoint before resolving, `status: inconclusive` as the leading tag (all gaps cleared) -> a per-reason-class HITL checkpoint with no auto-retry, never auto-resolving over the unevaluated goal); then **Acceptance testing** if the change is user-facing, opening with the drive-mode ask (`../../_shared/techniques/acceptance-testing.md`): AI-driven, or **walk the user through a guided UAT conversationally, one step at a time**, never a checklist dumped in a comment: interactive by design, so it runs inline, not backgrounded. Report the outcome honestly (if a test fails, say so). **The single exception:** under an accepted `lean` skip, run a lighter inline review pass instead of the full Code-review subagent and a quick inline goal check instead of the full Goal-backward verification subagent (or skip either only when the change is truly cosmetic); the lighter inline review still uses the same finding IDs, counts line, and ledger, and the same no-`open` gate, per code-review.md; the tests / criterion and the honest outcome report still run regardless. At a HITL checkpoint here, an advisory sub-choice (which fix, how to proceed) **offers** the delegate option, per the injection recipe in `../../_shared/choices.md`; the accept/resolve decision stays the human's.
35
37
  5. **Resolve**: a resolution comment with what was done → `gh issue close` → update `Decisions so far` on the map (a body read-modify-write: obey the **Fail-safe body rewrites** rule in `../../_shared/substrate.md`). **Disposition gate (before the close)**: the latest `VERIFY` comment's findings ledger covers every finding the counts line counts and none of them is `open`; the resolution comment links that ledger rather than duplicating it; otherwise do not `gh issue close` (per code-review.md's Disposition gate). **Pending-UAT guard (before the close).** If Verify left acceptance testing **not yet run** (it would otherwise be deferred), **pose the two-way choice to the user and wait for an explicit answer, never open a ticket unilaterally**: (a) run the UAT now, in this session, with no ticket (opens with the drive-mode ask, `../../_shared/techniques/acceptance-testing.md`), or (b) open a UAT follow-up ticket to run it later. On (a), run the guided walkthrough now and resolve **only on a confirmed pass** (a failing step becomes a fix or a `bug` follow-up and an incomplete/blocked step is marked pending and tracked, per the guided-UAT rules in `../../_shared/techniques/acceptance-testing.md`, not a silent close); on (b), **open the follow-up ticket carrying the owed UAT first**, linked back to this parent (a `task` on the frontier or a `trailhead:seed` gated on the run, per *Pending UAT must stay tracked* in `../../_shared/techniques/acceptance-testing.md`). **Never open a UAT ticket without asking, never `gh issue close` on a chosen-deferred UAT with nothing opened, and never let silence or an ambiguous reply default to either path.** When acceptance was already completed at Verify, this guard doesn't fire. **Post-commit self-check (before the comment goes out).** For each commit the comment cites, confirm the hash resolves (`git cat-file -e <hash>^{commit}`, or `git show --stat <hash>`) and that every file the comment names as touched actually appears in that commit's diff (`git -c core.quotepath=false show --name-only --format= <hash>`, with `core.quotepath=false` so a non-ASCII filename compares plainly instead of in git's C-quoted form); correct or drop any citation that fails the check, so a hallucinated or mis-stated commit never lands in the tracker record. **If this change materially altered the codebase** (a new module/layer, dependency, seam, or build/test command), **patch the affected facet of the `trailhead:codebase` issue** (see **Codebase** in `../../_shared/substrate.md`) in place (a line or two, not a re-map). Then close the session with the **Session handoff** ritual (`../../_shared/session-handoff.md`): `/clear` + next command.
36
38
 
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // trailhead work state (self-contained, ships in templates/ so it reaches every
4
+ // install channel: bin/trailhead.js copies templates/ wholesale for Claude/npm
5
+ // and Codex). Writes and removes <repo>/.trailhead/work-state.json, the state
6
+ // of the ticket in play that the skills update at every step. #205.
7
+ //
8
+ // CLI:
9
+ // node trailhead-work-state.js set [--repo <dir>] [--map <n>|none] [--ticket <n>]
10
+ // [--title <t>] [--step <step>] [--auto true|false] [--auto-done <n>]
11
+ // node trailhead-work-state.js clear [--repo <dir>]
12
+ // --repo defaults to cwd. `set` merges the given fields onto the existing file
13
+ // (only --step at each step change); a --ticket other than the one on file
14
+ // starts from an empty state. File shape:
15
+ // { map, ticket, title, step, auto, autoDone, session, updatedAt }
16
+ // autoDone: tickets resolved so far in the autonomous run (non-negative
17
+ // integer, default 0; reset to 0 whenever auto is false).
18
+ // Prints one JSON line {"status","path","state"?,"message"?}.
19
+ // status: written | cleared | absent | skipped
20
+ // Exit 0 on every normal outcome and on any unexpected I/O error (it must
21
+ // never block the caller: status skipped with the message); exit 2 only on a
22
+ // usage or validation error (nothing is written).
23
+
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+ const crypto = require('crypto');
27
+
28
+ const STATE_DIR = '.trailhead';
29
+ const STATE_FILE = 'work-state.json';
30
+ const STEPS = ['discuss', 'plan', 'execute', 'verify', 'resolve', 'paused'];
31
+ const TITLE_MAX = 200;
32
+
33
+ const isPosInt = (v) => Number.isInteger(v) && v > 0;
34
+
35
+ // Strict parse of a positive integer from a CLI string; null if invalid.
36
+ function parsePosInt(text) {
37
+ return /^[1-9]\d*$/.test(text) ? Number(text) : null;
38
+ }
39
+
40
+ // Strict parse of a non-negative integer from a CLI string; null if invalid.
41
+ function parseNonNegInt(text) {
42
+ return /^(0|[1-9]\d*)$/.test(text) ? Number(text) : null;
43
+ }
44
+
45
+ // Title on a single line: control characters and newlines become spaces,
46
+ // spaces collapsed, trimmed, capped at TITLE_MAX characters.
47
+ function cleanTitle(text) {
48
+ return String(text).replace(/[\u0000-\u001f\u007f\s]+/g, ' ').trim().slice(0, TITLE_MAX);
49
+ }
50
+
51
+ // Keeps only the valid fields of an arbitrary object: an invalid field is
52
+ // dropped (treated as absent), never carried into the new state.
53
+ function sanitizeState(raw) {
54
+ const out = {};
55
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) return out;
56
+ if (raw.map === null || isPosInt(raw.map)) out.map = raw.map;
57
+ if (isPosInt(raw.ticket)) out.ticket = raw.ticket;
58
+ if (typeof raw.title === 'string') out.title = cleanTitle(raw.title);
59
+ if (STEPS.includes(raw.step)) out.step = raw.step;
60
+ if (typeof raw.auto === 'boolean') out.auto = raw.auto;
61
+ if (Number.isInteger(raw.autoDone) && raw.autoDone >= 0) out.autoDone = raw.autoDone;
62
+ return out;
63
+ }
64
+
65
+ // Merges the patch onto the existing state. A different ticket starts from
66
+ // empty (no inherited title/map). Returns the normalised state, or
67
+ // { error } when ticket or step is missing.
68
+ function mergeState(existing, patch, { session = null, now = new Date() } = {}) {
69
+ const base = sanitizeState(existing);
70
+ const start = patch.ticket !== undefined && base.ticket !== undefined && base.ticket !== patch.ticket ? {} : base;
71
+ const merged = { ...start, ...patch };
72
+ if (merged.ticket === undefined) return { error: 'usage: set needs a ticket (pass --ticket)' };
73
+ if (merged.step === undefined) return { error: 'usage: set needs a step (pass --step)' };
74
+ const auto = merged.auto === undefined ? false : merged.auto;
75
+ return {
76
+ map: merged.map === undefined ? null : merged.map,
77
+ ticket: merged.ticket,
78
+ title: merged.title === undefined || merged.title === '' ? null : merged.title,
79
+ step: merged.step,
80
+ auto,
81
+ // A run that is not autonomous has no counter.
82
+ autoDone: auto && merged.autoDone !== undefined ? merged.autoDone : 0,
83
+ session,
84
+ updatedAt: now.toISOString(),
85
+ };
86
+ }
87
+
88
+ function parseArgs(argv) {
89
+ const command = argv[0];
90
+ if (command !== 'set' && command !== 'clear') {
91
+ return { error: `usage: expected "set" or "clear", got: ${command === undefined ? '(nothing)' : command}` };
92
+ }
93
+ let repo = null;
94
+ const patch = {};
95
+ const allowed = command === 'set' ? ['--repo', '--map', '--ticket', '--title', '--step', '--auto', '--auto-done'] : ['--repo'];
96
+ for (let i = 1; i < argv.length; i++) {
97
+ const flag = argv[i];
98
+ if (!allowed.includes(flag)) return { error: `usage: unknown argument: ${flag}` };
99
+ i++;
100
+ if (i >= argv.length) return { error: `usage: ${flag} requires a value` };
101
+ const value = argv[i];
102
+ if (flag === '--repo') repo = value;
103
+ else if (flag === '--map') {
104
+ if (value === 'none') patch.map = null;
105
+ else if (parsePosInt(value) !== null) patch.map = parsePosInt(value);
106
+ else return { error: `usage: --map must be a positive integer or "none", got: ${value}` };
107
+ } else if (flag === '--ticket') {
108
+ if (parsePosInt(value) === null) return { error: `usage: --ticket must be a positive integer, got: ${value}` };
109
+ patch.ticket = parsePosInt(value);
110
+ } else if (flag === '--title') patch.title = cleanTitle(value);
111
+ else if (flag === '--step') {
112
+ if (!STEPS.includes(value)) return { error: `usage: --step must be one of ${STEPS.join(', ')}, got: ${value}` };
113
+ patch.step = value;
114
+ } else if (flag === '--auto') {
115
+ if (value !== 'true' && value !== 'false') return { error: `usage: --auto must be true or false, got: ${value}` };
116
+ patch.auto = value === 'true';
117
+ } else if (flag === '--auto-done') {
118
+ if (parseNonNegInt(value) === null) return { error: `usage: --auto-done must be a non-negative integer, got: ${value}` };
119
+ patch.autoDone = parseNonNegInt(value);
120
+ }
121
+ }
122
+ return { command, repo, patch };
123
+ }
124
+
125
+ // Reads the existing file; missing or corrupt = no state (never a crash).
126
+ function readExisting(file, fsMod) {
127
+ try {
128
+ return JSON.parse(fsMod.readFileSync(file, 'utf8'));
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+
134
+ // Atomic write: a unique temp in the same dir, then rename over the file
135
+ // (never an in-place truncate); the temp is removed if anything fails.
136
+ function writeAtomic(dir, file, text, fsMod) {
137
+ fsMod.mkdirSync(dir, { recursive: true });
138
+ const tmp = path.join(dir, `.${STATE_FILE}.${process.pid}-${crypto.randomBytes(4).toString('hex')}.tmp`);
139
+ try {
140
+ fsMod.writeFileSync(tmp, text);
141
+ fsMod.renameSync(tmp, file);
142
+ } catch (err) {
143
+ try { fsMod.unlinkSync(tmp); } catch { /* pulizia best effort */ }
144
+ throw err;
145
+ }
146
+ }
147
+
148
+ function writeResult(stdout, result) {
149
+ stdout.write(JSON.stringify(result) + '\n');
150
+ }
151
+
152
+ function main(argv, { cwd, stdout, env, fs: fsMod, now } = {}) {
153
+ const cwd_ = cwd || process.cwd();
154
+ const out = stdout || process.stdout;
155
+ const env_ = env || process.env;
156
+ const fs_ = fsMod || fs;
157
+
158
+ const parsed = parseArgs(argv || []);
159
+ if (parsed.error) {
160
+ process.stderr.write(parsed.error + '\n');
161
+ return 2;
162
+ }
163
+
164
+ const repoDir = parsed.repo ? path.resolve(cwd_, parsed.repo) : cwd_;
165
+ const dir = path.join(repoDir, STATE_DIR);
166
+ const file = path.join(dir, STATE_FILE);
167
+ try {
168
+ if (parsed.command === 'clear') {
169
+ if (!fs_.existsSync(file)) {
170
+ writeResult(out, { status: 'absent', path: file });
171
+ return 0;
172
+ }
173
+ fs_.unlinkSync(file);
174
+ writeResult(out, { status: 'cleared', path: file });
175
+ return 0;
176
+ }
177
+
178
+ const sessionEnv = env_.CLAUDE_CODE_SESSION_ID;
179
+ const session = typeof sessionEnv === 'string' && sessionEnv !== '' ? sessionEnv : null;
180
+ const state = mergeState(readExisting(file, fs_), parsed.patch, { session, now: now || new Date() });
181
+ if (state.error) {
182
+ process.stderr.write(state.error + '\n');
183
+ return 2;
184
+ }
185
+ writeAtomic(dir, file, JSON.stringify(state, null, 2) + '\n', fs_);
186
+ writeResult(out, { status: 'written', path: file, state });
187
+ return 0;
188
+ } catch (err) {
189
+ writeResult(out, { status: 'skipped', path: file, message: 'error: ' + (err && err.message ? err.message : String(err)) });
190
+ return 0;
191
+ }
192
+ }
193
+
194
+ module.exports = { STEPS, sanitizeState, mergeState, parseArgs, main };
195
+
196
+ if (require.main === module) {
197
+ const code = main(process.argv.slice(2), { cwd: process.cwd(), stdout: process.stdout, env: process.env });
198
+ process.exit(code);
199
+ }