@marcomigozzi/trailhead 0.12.0 → 0.13.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.
- package/README.it.md +11 -11
- package/README.md +11 -11
- package/bin/lib/codex-projection.js +2 -2
- package/bin/trailhead.test.js +27 -6
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/hooks/lib/fixtures/label-guard-0.12.0.yml +55 -0
- package/plugins/trailhead/hooks/lib/label-guard-sync.test.js +156 -0
- package/plugins/trailhead/hooks/trailhead-check-update.js +25 -1
- package/plugins/trailhead/hooks/trailhead-mockup-link-guard.test.js +4 -4
- package/plugins/trailhead/hooks/trailhead-mockup-link-stop.test.js +1 -1
- package/plugins/trailhead/skills/_shared/configuration-reference.md +5 -4
- package/plugins/trailhead/skills/_shared/configuration.md +1 -1
- package/plugins/trailhead/skills/_shared/load-first.md +2 -0
- package/plugins/trailhead/skills/_shared/substrate-commands.md +1 -1
- package/plugins/trailhead/skills/_shared/teamwork.md +2 -0
- package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/prototype.md +15 -16
- package/plugins/trailhead/skills/_shared/techniques.md +1 -1
- package/plugins/trailhead/skills/trailhead-manage/references/updating.md +4 -0
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +1 -1
- package/plugins/trailhead/templates/trailhead-label-guard-sync.js +212 -0
- package/plugins/trailhead/templates/trailhead-label-guard.yml +5 -0
|
@@ -7,8 +7,9 @@ Effective config = built-in defaults ← global `~/.claude/trailhead/config.json
|
|
|
7
7
|
| `models.{plan,execute,research,review,debug,verify,codebase-map}` | a model id (**Balanced-profile default**) | model for that activity; an unset key resolves to its default **Balanced** tier, see note |
|
|
8
8
|
| `models.codex.{plan,execute,research,review,debug,verify,codebase-map}` | an OpenAI model id (**inherit session**) | per-activity subagent model on **Codex** (OpenAI ids), the honoured namespace there; independent of `models.*` (Claude ids); projected into `~/.codex/agents/trailhead-*.toml` and dispatched by `agent_type`. See note |
|
|
9
9
|
| `ticket.language` | an ISO 639-1 code (**`en`**) | the language trailhead **writes** its GitHub prose in (ticket titles/bodies, engine comments, map body & commit descriptions), see note. Independent of the language the agent converses in |
|
|
10
|
-
| `design` | **`disk`** \| `
|
|
11
|
-
| `design.surface` | **`canvas`** \| `design-system` |
|
|
10
|
+
| `design` | **`disk`** \| `artifacts` \| `stitch` | mode for UI mockups (**Prototype**). `disk` = throwaway local HTML (any host); `artifacts` = build the mockup as a **Design canvas Artifact** on claude.ai through Claude Code's built-in `Artifact` tool (no MCP, no login step), **Claude Code signed in with a claude.ai account only**; `stitch` = create/edit the mockup via the **Google Stitch MCP** (`claude mcp add --scope user --transport http stitch https://stitch.googleapis.com/mcp`), the **cross-host** hosted option (Claude Code **and** Codex). A hosted mode's canvas/project is created or reused at execution and cached in `design.project`. **Legacy:** `claude.ai/design` (the standalone Claude Design site, closing 2026-12-14) is read as `artifacts`, with a one-time notice and an offer to rewrite the key. Where the `Artifact` tool is missing (Codex, API-key sessions) Prototype **offers to switch to `stitch`** before degrading to `disk`; `stitch` and `disk` run on Codex unchanged |
|
|
11
|
+
| `design.surface` | **`canvas`** \| `design-system` | **`stitch` only**: which surface the mockup lives on. `canvas` = a Stitch project of screens to react to; `design-system` = push components into a shared Stitch design system. Ignored under `artifacts` (always a Design canvas; a design system is installed on it via `design.system`, never written to) and under `disk`. See Prototype |
|
|
12
|
+
| `design.system` | Design System Artifact URL (**unset**) | **`artifacts` only**: the design system installed on the mockup canvas (its tokens and, when it ships them, its components). Unset at the first screen: the account's default design system if one is marked, else trailhead lists them and asks (or none), then caches the pick here |
|
|
12
13
|
| `design.approval` | **`explicit`** \| `auto` | must a mockup be explicitly approved before the build proceeds to UI code? |
|
|
13
14
|
| `tdd` | **`seams`** \| `on` \| `off` | how the `build` engine tests |
|
|
14
15
|
| `acceptance.browser` | **`auto`** \| `on` \| `off` | how **Acceptance testing** runs |
|
|
@@ -62,7 +63,7 @@ If a value is unset, it resolves to its **Balanced**-profile default for that ac
|
|
|
62
63
|
|
|
63
64
|
**acceptance.browser.** The drive-mode ask (AI-driven vs human-driven, see the Acceptance testing technique) **always comes first, before any step runs**: no value of this key pre-answers it. This key only governs whether and how an AI-driven step uses the browser once that ask is answered AI-driven: `auto` (default) = browser-drive when `testing.webapp` + `testing.url` are set, or (when `testing.webapp` is set but `testing.url` is unset) first try to auto-detect the target URL (running dev server, package scripts, dev-server/framework config, project config) and propose it, driving against the accepted URL; when no URL can be determined, fall back per the Acceptance testing technique (ask the human for the URL, or the guided UAT for that step); `on` = always attempt browser-driving, auto-detecting the URL the same way when `testing.url` is unset; `off` = never drive a browser. **`off` disables *browser-driving*, not acceptance, and not AI-driven checking**: AI-driven still runs its non-browser checks via commands / the test suite for whatever steps allow it; only a step that genuinely needs the human (or the whole UAT, under human-driven) gets the guided UAT walkthrough. The Verify step honours this.
|
|
64
65
|
|
|
65
|
-
**design.surface.** Under `design:
|
|
66
|
+
**design.surface / design.system.** Under `design: stitch`, `design.surface` picks the surface: `canvas` (default) = a Stitch project with generated screens, `design-system` = a Stitch design system; the install offer and disk fallback are in Prototype (the Stitch MCP either connects or falls to disk). Under `design: artifacts` there is one surface, a **Design canvas Artifact** (`design.project` = its URL), and `design.surface` is ignored; `design.system` names the **Design System Artifact** installed on that canvas, a read-only reference: screens are never pushed into it. **Back-compat:** a `design.project` left from the standalone claude.ai/design (a bare project id or a `claude.ai/design/p/<id>` URL) is not reusable; Prototype says so once and attaches a new canvas. Ignored under `design: disk`.
|
|
66
67
|
|
|
67
68
|
**design.approval.** `explicit` (default) = a UI mockup must be **explicitly approved by the user** before the `build` engine writes any real UI code; surface the mockup and wait for a clear go-ahead. `auto` = surface the mockup and proceed without blocking on a confirmation (the user can still object). The Prototype technique honours this.
|
|
68
69
|
|
|
@@ -104,7 +105,7 @@ Example `config.json` (same shape for the project `.trailhead/config.json` and t
|
|
|
104
105
|
- **The per-key questions (Manual path only): always two separate questions, one for `plan` and one for `execute`; never merge them into a single model pick.** Ask each as its own `AskUserQuestion` menu (current value pre-selected), and offer the **same versioned lineup** to both. Each option is a **full model id with its version**; never a bare family alias (`opus`/`sonnet`/`haiku`), which can't say *which* live version. Build the list from the models actually reachable **at setup time** (verify, don't trust this file verbatim), offering **at least the two most recent versions per family**, e.g. `claude-fable-5`, `claude-opus-5-5` / `claude-opus-5`, `claude-sonnet-5-5` / `claude-sonnet-5`, `claude-haiku-5-5` / `claude-haiku-4-5`, plus **inherit session** (the no-override option, the session model). If that lineup exceeds the menu's option cap, drill **family → version** or include a free-id "Other" so every recent version stays reachable. The rarer `research`/`review`/`debug`/`verify`/`codebase-map` sit behind an "Advanced" option, each its own separate choice, same versioned rule.
|
|
105
106
|
- **Anti-pattern, never do this (in the Manual path):** a single screen whose options are *combined* plan+execute presets, e.g. `🧠 Plan 4.8 / Exec Sonnet 5`, `🚀 Tutto Opus 5`, `Plan Opus 5 / Exec Sonnet 5`. That bundles the two decisions into one pick. Forbidden. In Manual, Plan is asked, answered, and only then is Execute asked, as a second menu: two questions, two answers, always. (A **named tier profile** applied whole is **not** this anti-pattern: it is a coherent per-key tier assignment chosen deliberately, not an ad-hoc plan+execute combo, which is why picking a profile is terminal.)
|
|
106
107
|
- **On Codex (host-detected), the Manual path asks `models.codex.*` instead of the Claude lineup.** Same discipline: `plan` and `execute` as two separate `AskUserQuestion` menus, never merged; the rarer `research`/`review`/`debug`/`verify`/`codebase-map` behind Advanced, each its own choice. Offer OpenAI model ids verified reachable at setup time (do not trust this list verbatim): `gpt-5.6-sol` (flagship) / `gpt-5.6-terra` (balanced) / `gpt-5.6-luna` (fast), plus **inherit session** (default). Avoid the retiring `gpt-5.4` / `gpt-5.4-mini` and the deprecated `gpt-5.3-codex`. Write the answers to `models.codex.*`; the Claude `models.*` lineup is not asked on Codex (those ids cannot run there). After writing, tell the user to **re-run the installer** (the same `--host=codex` install command) so the pins are re-projected into `~/.codex/agents/trailhead-*.toml`: the menu updates `.trailhead/config.json`, the installer is what regenerates the TOMLs and enables `features.multi_agent_v2`.
|
|
107
|
-
5. **🎨 Design**: 💾 Local disk · 🖼️ claude.ai
|
|
108
|
+
5. **🎨 Design**: 💾 Local disk · 🖼️ Artifacts (a Design canvas on claude.ai, built with Claude Code's `Artifact` tool; Claude Code only) · 🧵 Stitch (Google Stitch MCP, cross-host: Claude Code + Codex). *Mode only, don't ask for a URL here; the canvas/project is created or reused at execution time and cached in `design.project` (see Prototype).* If Stitch is picked, ask the **🖼️ Design surface**: 🖌️ Canvas (a Stitch project of screens to react to; default) · 🧩 Design system (push components into a shared Stitch design system) → `design.surface`. If Artifacts is picked, optionally ask the **🧩 Design system** to install on the canvas (list the account's Design System Artifacts, or "decide at the first screen") → `design.system`. Then **✅ Mockup approval**: 🖐️ Explicit (wait for a go-ahead before UI code) · ⏩ Auto (proceed without blocking).
|
|
108
109
|
6. **🧪 TDD**: 🎯 Seams only · ✅ On (all definable behaviour) · ⛔ Off.
|
|
109
110
|
7. **⚖️ Effort**: how hard the `build`/`bug` engine works each change (`effort`). 🛠️ **Standard** (default; every step runs) · 🍃 **Lean** (size-triage small changes and offer to skip heavy steps, on an explicit yes only: the plan pass, TDD, cross-AI review, the full code review, the goal-backward verify; correctness checks always stay). Governs `build`/`bug` only: `quick` always size-triages regardless.
|
|
110
111
|
8. **🖥️ Acceptance testing**: 🤖 Auto · ▶️ Always browser · 🚫 No browser (acceptance still runs, starting with the drive-mode ask; only browser-driving is off, see `acceptance.browser` above). If browser is enabled, ask **🔗 base URL** and confirm it's a web app.
|
|
@@ -5,4 +5,4 @@ Three layers, **nearest wins**, key by key; a key unset at one layer inherits th
|
|
|
5
5
|
- **Global**: `~/.claude/trailhead/config.json` (honour `$CLAUDE_CONFIG_DIR`): your standing defaults across every project.
|
|
6
6
|
- **Defaults**: the built-in values documented in `configuration-reference.md`.
|
|
7
7
|
|
|
8
|
-
Load the effective config at the start of a work session, **from the map's project root** (`.trailhead/config.json` at the superproject repo root) plus the global file, **before** any isolation workspace is set up. **Config is a property of the project/map, not the working directory**: an `isolation: worktree`/`clone` workspace **inherits** the config loaded here and must **never re-resolve it from the isolated copy's own root**. A per-ticket clone is always a clone of the **superproject** (never of a submodule alone), so its `.trailhead/config.json` is present when the project commits one and absent when it doesn't; either way re-resolving there is wrong, because the workspace must reuse the config already resolved from the original checkout, not load it afresh (an absent file would silently fall back to defaults, which is exactly how a `design:
|
|
8
|
+
Load the effective config at the start of a work session, **from the map's project root** (`.trailhead/config.json` at the superproject repo root) plus the global file, **before** any isolation workspace is set up. **Config is a property of the project/map, not the working directory**: an `isolation: worktree`/`clone` workspace **inherits** the config loaded here and must **never re-resolve it from the isolated copy's own root**. A per-ticket clone is always a clone of the **superproject** (never of a submodule alone), so its `.trailhead/config.json` is present when the project commits one and absent when it doesn't; either way re-resolving there is wrong, because the workspace must reuse the config already resolved from the original checkout, not load it afresh (an absent file would silently fall back to defaults, which is exactly how a `design: artifacts` project ends up wrongly using `disk` in a clone). Config **writes** too (e.g. caching `design.project`) target the project's `.trailhead/config.json` in the original checkout, never the clone. **The main session orchestrates and holds the HITL moments** (the `build`/`bug` Discuss's "stop and ask", Verify's acceptance/UAT); **every model key runs as a subagent** (`plan`, `execute` (an *executor subagent* implementing with atomic commits on `models.execute`), `research`, `review`, `debug`, `verify`, `codebase-map`) so the whole per-activity model split applies in one session whatever model it runs on. Run Execute inline only when `models.execute` is unset or equals the session model. **On a subagent-less host** (`subagentToolkit == none`) there is nothing to fan out into: every activity runs inline. **Separately, on a host that can't honour the model keys** (`honorsModelKeys == false`, e.g. Codex, whose subagents run on its own OpenAI models) every `models.*` key collapses to the single session model even though the activities still fan out; when any `models.*` is set there the engine surfaces a **one-time** session-start notice that the split is not taking effect (the keys stay in `.trailhead/config.json` for when the project next runs on Claude Code). On Codex the per-activity split is available through a parallel `models.codex.*` namespace (OpenAI model ids), the honoured namespace there (projected into per-technique `~/.codex/agents/*.toml` and dispatched at runtime by `agent_type`), while `models.*` (Claude ids) still collapse to the single session model; the collapse notice points at `models.codex.*` and goes silent once it is set. A host without a hook bus (`hookBus == none`) also runs a lightweight inline update check at session start (no SessionStart hook, no statusline). After an update changes the tier defaults, a pinned-model review offer surfaces too, once per session and until the user answers it (see `../_shared/load-first.md` for the check/ack sequence and `configuration-reference.md`'s Pinned model review). See `configuration-reference.md`. The **keys, their values, the model/tdd/acceptance semantics, and the guided menu setup live in `configuration-reference.md`**: read it when running `/trailhead:config` or when you need a key's exact meaning. `/trailhead:config` with no args runs the guided setup; `config get` prints the merged config; `config set <key> <value>` writes one key.
|
|
@@ -18,3 +18,5 @@ Before doing anything, read the shared core, in order:
|
|
|
18
18
|
Load the **effective config** (per `../_shared/configuration.md`) at session start, from the map's project root (`.trailhead/config.json` plus the global file), before any isolation workspace is set up.
|
|
19
19
|
|
|
20
20
|
**Pinned model review, once per session.** After loading the effective config, run the read-only check: `node <lib> check <project root>`, where `<lib>` is the first of these two paths (relative to this `_shared/` dir) that exists: `../../hooks/lib/model-defaults-review.js` (Claude plugin, Claude npm copy, dev-symlink install), else `../trailhead/hooks/lib/model-defaults-review.js` (Codex). If neither exists, skip silently. If it prints a notice, offer it to the user without blocking the current request; only after the user answers any of its three choices, run `node <lib> ack <project root>`. Do this at most once per session: skip it if the SessionStart hook (or an earlier config load this session) already surfaced the same offer. Repeats across sessions are expected until the user answers.
|
|
21
|
+
|
|
22
|
+
**Label guard check, once per session.** Same pattern for the repo's label guard: run `node <templates>/trailhead-label-guard-sync.js --repo <project root> --notice`, with `<templates>` resolved as for the commit-msg sync script (see `teamwork.md`, Trust & provenance → Repo-side enforcement; skip silently if none of the template dirs has the script). It prints an offer only when the committed guard is the outdated one (no job-level `if:`, so every label event bills a runner) and the user has not answered for that file yet. Offer it without blocking the current request and follow its instructions; only after the user picks either choice, run the `--ack` command it names. At most once per session: skip it if the SessionStart hook already surfaced it.
|
|
@@ -200,5 +200,5 @@ gh api repos/{owner}/{repo}/issues/<map> --jq '.sub_issues_summary.total'
|
|
|
200
200
|
|
|
201
201
|
## First-use repo setup (do ALL THREE, every chart or adopt, never skip any)
|
|
202
202
|
1. Create any missing labels with `gh label create` (all eighteen: `trailhead:map`, `trailhead:codebase`, `trailhead:conventions`, `trailhead:dashboard`, `trailhead:ticket`, the six type labels, `trailhead:blocked`, `trailhead:seed`, `trailhead:out-of-scope`, `trailhead:superseded`, `trailhead:unverified`, `trailhead:fog`, `trailhead:whiteboard`).
|
|
203
|
-
2. **Check the label guard is installed**: `gh api repos/{owner}/{repo}/contents/.github/workflows/trailhead-label-guard.yml`; if it's absent (404), install it: **read `teamwork.md` (Trust & provenance → Repo-side enforcement) for the exact steps**. This is part of standing up trailhead in a repo, not an optional extra: *check every time*, so a repo can never end up with the labels but no guard.
|
|
203
|
+
2. **Check the label guard is installed and current**: `gh api repos/{owner}/{repo}/contents/.github/workflows/trailhead-label-guard.yml`; if it's absent (404), install it; if it's present, run the label guard sync script (`trailhead-label-guard-sync.js`, check-only) on the working root and, when it reports `outdated` (a pre-0.12.1 guard with no job-level `if:`, which starts a billed runner on every `issues: labeled` event), offer the upgrade: **read `teamwork.md` (Trust & provenance → Repo-side enforcement) for the exact steps**. This is part of standing up trailhead in a repo, not an optional extra: *check every time*, so a repo can never end up with the labels but no guard, or with a guard that burns Actions minutes.
|
|
204
204
|
3. **Sync the commit-msg git hook**: run the commit-msg hook sync script (`trailhead-commit-msg-sync.js`; installs it if absent, upgrades an outdated trailhead hook in place, never touches a foreign one) so trailhead's commit discipline (Conventional Commits subject, no `Co-Authored-By`, and a `Refs: #<n>` trailer while a `.trailhead/session-ticket` marker is active) runs on **every** `git commit` regardless of host (the Claude-Code `PreToolUse` guard stays too, defence in depth): **read `teamwork.md` (Trust & provenance → Repo-side enforcement) for the exact steps**. If the script is missing or node errors (missing means none of the three template dirs listed in `teamwork.md` has it: try them all first), fall back to inspecting the hooks dir (`git rev-parse --git-path hooks`) per `teamwork.md`, never a blind "not enforced". Like the label guard, *check every time*; unlike it, `.git/hooks/` is per-clone local state (untracked), so it is installed, never committed.
|
|
@@ -94,4 +94,6 @@ The map issue's **body** is a shared mutable document: two sessions appending to
|
|
|
94
94
|
|
|
95
95
|
**Repo-side enforcement (part of first-use setup, not an afterthought).** trailhead **installs the label guard for you**; don't tell the user to copy files. **Every chart or adopt, *check* whether `.github/workflows/trailhead-label-guard.yml` exists** (the first-use repo setup, in SKILL.md's Substrate section); if it's absent, install it, by default on a repo with other collaborators, and as a quick offer on a clearly solo/private one (cheap insurance), but **never silently skip the check**: that's exactly how a repo ends up with trailhead labels and no guard. On go-ahead, write the shipped label-guard template (`${CLAUDE_PLUGIN_ROOT}/templates/trailhead-label-guard.yml` for a plugin install, or `~/.claude/trailhead/templates/trailhead-label-guard.yml` for an npm install, take whichever exists) to the repo's `.github/workflows/trailhead-label-guard.yml` and commit it (conventional message; the commit-guard hook keeps it clean). The Action strips any `trailhead:*` label added by a non-write actor and comments why; set an optional `TRAILHEAD_LABEL_ALLOWLIST` repo variable to whitelist extra logins (e.g. a bot trailhead runs as). **Caveat:** pushing a workflow file needs a token with the `workflow` scope; if the push is rejected, run `gh auth refresh -s workflow` and retry (or, as a last resort, tell the user to add the file by hand).
|
|
96
96
|
|
|
97
|
+
**Keeping the guard current (migration).** The guard filters at **job level** (`jobs.guard.if`: a `trailhead:` label, a sender other than the repo owner, not in `TRAILHEAD_LABEL_ALLOWLIST`), because a skipped job is not billed while a job that starts and returns early from its script costs at least one Actions minute; the in-script permission check stays the real defence. Guards installed before 0.12.1 lack that `if:` and start a runner on **every** label event, which with trailhead labelling all the time runs into thousands of billed runs a month. So whenever the guard is present (every chart/adopt, and after `/trailhead:update`), run `node <templates>/trailhead-label-guard-sync.js --repo <working-root>` (same `<templates>` lookup as the commit-msg sync script below). It prints one JSON status line: `current` (nothing to do), `outdated` (offer the upgrade and always explain why in the question, in plain words: today every label added to an issue starts a run billed at least one minute even when it does nothing, trailhead labels constantly so that adds up to thousands of billed runs a month and can exhaust the Actions minutes, and the updated guard skips irrelevant events before a runner starts with the same protection), `custom` (has the job-level filter but differs from the template: leave it, mention it only if asked), `foreign` (not a trailhead workflow: never touched), `absent` (install as above). On go-ahead for `outdated`, re-run it with `--apply` (it rewrites the file from the template in the working tree only, never a foreign or customised guard), then commit it (`fix: skip trailhead label guard job for irrelevant label events`) and push, with the same `workflow`-scope caveat. The SessionStart hook (or, on a host with no hook bus, the load-first check) also runs it with `--notice` at every session start and surfaces the same upgrade offer until the user answers it; `--ack` records the answer in the gitignored `.trailhead/label-guard-ack` (the guard's hash), so a declined offer comes back only if the file changes. If the script is missing (an outdated install), fall back to reading the file and checking `jobs.guard` for an `if:` containing `startsWith(github.event.label.name, 'trailhead:')`.
|
|
98
|
+
|
|
97
99
|
**The commit-msg git hook (host-independent commit guard).** On every host trailhead ALSO installs a git **`commit-msg`** hook as a host-independent backstop (defence in depth alongside the host's own `PreToolUse` guard on Claude Code and Codex alike), enforcing the two rules (a Conventional Commits subject of at most 72 chars; never a `Co-Authored-By` trailer) plus, while a `.trailhead/session-ticket` marker is present at the worktree root, a `Refs: #<n>` trailer matching its number, on every `git commit`, whatever host drives it (decision #16). **Every chart or adopt, and at every `work`/`quick` ticket start, run the sync script**: `node <templates>/trailhead-commit-msg-sync.js --repo <working-root>`, where `<templates>` is the first of `${CLAUDE_PLUGIN_ROOT}/templates/`, `~/.claude/trailhead/templates/`, `~/.codex/skills/trailhead/templates/` that contains `trailhead-commit-msg-sync.js`: try all three in order, skipping the first when `CLAUDE_PLUGIN_ROOT` is unset or empty (it is often absent from the Bash environment), and never stop at the first miss. It prints one JSON status line and handles every case itself: absent -> installs it; an older trailhead hook (recognised by its own header line, not by trusting the filename) -> upgraded in place; already current -> left alone; a foreign (non-trailhead) hook -> never touched, only reported (offer to chain it); under a repo's `core.hooksPath`, an existing trailhead hook there is still upgraded in place, but nothing is newly added to that directory. Unlike the label guard, `.git/hooks/` is not version-controlled, so this is a per-clone local install with nothing to commit. **If the sync script is missing (none of the three dirs has it: an outdated install) or node errors, never claim git enforces nothing before looking**: resolve the effective hooks dir with `git -C <working-root> rev-parse --git-path hooks` (resolve a relative output against the working root; it honours `core.hooksPath` and the git common dir shared by linked worktrees), read `<hooks dir>/commit-msg`, and classify it by its header line (a trailhead hook has `// trailhead commit-msg hook (git).` among its first 5 lines, trimmed; never trust the filename alone; a symlinked `commit-msg` counts as foreign, as the script treats it). Report one result-oriented line: (a) trailhead hook present and executable (`test -x`): git does enforce trailhead's rules here, but its freshness could not be checked or upgraded, so suggest updating the trailhead install; a trailhead hook that is NOT executable: git skips it, so it does not enforce trailhead's rules here (suggest updating the install so the sync script can fix the mode); (b) a foreign hook (or a symlink): left untouched, git does not enforce trailhead's rules here; (c) no hook: git does not enforce trailhead's rules here (suggest updating the install so the sync script can install it). Never blocks the work.
|
|
@@ -17,7 +17,7 @@ Automated tests prove the code does what the *code* intended; acceptance testing
|
|
|
17
17
|
- **When the human can only run it later** (on-device, hardware you don't have right now), record the full numbered checklist in the `VERIFY` comment as the durable artifact and mark UAT **explicitly pending**, but the walkthrough stays conversational **when they come to run it**: never shrink it to a vague one-liner ("just check it on the device"), and never let "automated passed" quietly stand in for it. Deferring *where/when* the human runs it is fine; replacing the live step-by-step with a self-serve checklist is not.
|
|
18
18
|
- **Pending UAT must stay tracked: a closed ticket is not tracking.** Don't resolve/close a user-facing ticket on unconfirmed acceptance and leave the checklist as a comment on the closed issue, where nobody will look. The default is to **keep the ticket open** until the human confirms the UAT passes (closing on green you didn't see is a premature close). **When UAT hasn't been run at Resolve, pose the choice to the human and wait for an explicit answer, never decide it for them:** (a) **run the UAT now**, in this session, with no ticket (opens with the drive-mode ask above, then the guided conversational walkthrough or AI-driven checks, resolving 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 failing-step and partial-run rules above, never a silent close), or (b) **open a UAT ticket** to run it later. **Never open a UAT ticket without asking, and never let silence or an ambiguous reply default to either path.** Only when the human picks (b) do you **open a follow-up ticket before that close** that carries the pending UAT and links back to the parent with a `UAT of: <parent name>(link)` pointer in the body. Route it the way `../choices.md` routes a deferred choice: a **`trailhead:seed`** (kept blocked) when the run is gated on a future external trigger not yet true (hardware you don't have, a device out of reach), or a follow-up **`task`** on the frontier when it's just scheduled or batched work you could pick up any time. Either way the follow-up **carries the full numbered checklist and, when it comes to resolution, opens with the drive-mode ask above before running the guided conversational walkthrough**, never a self-serve check-off. **Never close a user-facing ticket on a chosen-deferred UAT with nothing opened**: the parent may close as agreed, but the owed UAT stays visible on the tracker as its own ticket, linked to the parent, until it is actually performed. The pending check must be visible on the tracker, not buried.
|
|
19
19
|
|
|
20
|
-
**Mockup-fidelity check (when the ticket had an approved mockup).** If the change was preceded by an approved mockup (the **Prototype** technique produced one and it was approved and recorded at Execute), acceptance testing MUST check the built screen **against that approved mockup**, not merely against "what the user wanted" in the abstract. Pull the mockup's current state to compare against (re-fetch it in `
|
|
20
|
+
**Mockup-fidelity check (when the ticket had an approved mockup).** If the change was preceded by an approved mockup (the **Prototype** technique produced one and it was approved and recorded at Execute), acceptance testing MUST check the built screen **against that approved mockup**, not merely against "what the user wanted" in the abstract. Pull the mockup's current state to compare against (re-fetch it in `artifacts` / `stitch` mode via the technique's re-fetch rule, or open the linked `disk` mockup); never compare from memory. Walk it **element by element and state by state**, layout, spacing, sizing, colour, typography, copy, and every state it shows (empty, loading, error, hover, active, disabled, and the like), enumerating from the approved mockup itself (the spec) and cross-checking the executor's recorded Execute-step fidelity list and any element it flagged infeasible, and confirm each is reproduced. Record the per-element and per-state result in the `VERIFY` comment. Any **unforced** deviation is an acceptance finding, never waved through as "close enough"; a genuinely infeasible part must already have been raised with the user at Execute (per the Prototype technique), not surface here as a silent drift.
|
|
21
21
|
|
|
22
22
|
Acceptance failures are findings like any other: a `bug` follow-up or a fix on this ticket before it resolves.
|
|
23
23
|
|
|
@@ -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 (`
|
|
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
|
-
- **`
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
19
|
-
-
|
|
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
|
|
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
|
-
**
|
|
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
|
-
**
|
|
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
|
|
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; 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
|
|
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 /
|
|
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 |
|
|
@@ -51,4 +51,8 @@ After the cache refresh, run a **lightweight integrity check** (fs existence plu
|
|
|
51
51
|
|
|
52
52
|
The check is lightweight and degrades silently: a probe that can't run (an unreadable path, a host detail absent) is reported as unchecked, never a hard failure of the update.
|
|
53
53
|
|
|
54
|
+
## 6. Realign this repo's label guard
|
|
55
|
+
|
|
56
|
+
An update can change the shipped label guard, but a guard already committed to a repo is not touched by the install. If the current directory is a repo with `.github/workflows/trailhead-label-guard.yml`, run the label guard sync script check-only (see `../../_shared/teamwork.md`, Trust & provenance → Keeping the guard current) and, on `outdated`, offer the upgrade (`--apply`, then commit and push); `current`, `custom` and `foreign` need no action. Skip silently when there is no guard or no repo.
|
|
57
|
+
|
|
54
58
|
Never edit `package.json`/`plugin.json` versions here: this command **consumes** releases, it does not cut them.
|
|
@@ -30,7 +30,7 @@ The ticket that *builds*. Lean cycle, all in **ticket comments**:
|
|
|
30
30
|
|
|
31
31
|
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
32
|
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 /
|
|
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 / 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
34
|
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
35
|
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
36
|
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// trailhead label guard sync (self-contained, ships in templates/ so it
|
|
4
|
+
// reaches every install channel). Classifies the repo's
|
|
5
|
+
// .github/workflows/trailhead-label-guard.yml against the sibling template
|
|
6
|
+
// and, with --apply, installs it when absent or upgrades an OUTDATED trailhead
|
|
7
|
+
// guard (one without the job-level `if:` filter, so every `issues: labeled`
|
|
8
|
+
// event started a billed runner). Never touches a foreign or customised guard.
|
|
9
|
+
//
|
|
10
|
+
// CLI: node trailhead-label-guard-sync.js [--repo <dir>] [--apply | --notice | --ack]
|
|
11
|
+
// Default and --apply print exactly one JSON line: {"status","file","message"}.
|
|
12
|
+
// --notice prints the session-start upgrade offer (plain text) when the guard
|
|
13
|
+
// is outdated and the user has not declined it for this exact file, else
|
|
14
|
+
// nothing. --ack records that the user answered the offer: it stores the
|
|
15
|
+
// guard's hash in the gitignored .trailhead/label-guard-ack, so the offer
|
|
16
|
+
// comes back only if the file changes.
|
|
17
|
+
// status (check): absent | current | outdated | custom | foreign | skipped
|
|
18
|
+
// status (--apply): installed | upgraded, or the check status when nothing
|
|
19
|
+
// was written
|
|
20
|
+
// Writing only changes the working tree: committing (and pushing, which needs
|
|
21
|
+
// the `workflow` token scope) stays with the caller.
|
|
22
|
+
// Exit 0 on every normal outcome (including unexpected errors: this must never
|
|
23
|
+
// block the caller); exit 2 only on a usage error.
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
const crypto = require('crypto');
|
|
28
|
+
|
|
29
|
+
const GUARD_REL = path.join('.github', 'workflows', 'trailhead-label-guard.yml');
|
|
30
|
+
// Prima riga di ogni versione del guard trailhead (tra le prime 5, trim).
|
|
31
|
+
const TRAILHEAD_MARK = '# trailhead label guard:';
|
|
32
|
+
const ACK_REL = path.join('.trailhead', 'label-guard-ack');
|
|
33
|
+
// Il filtro che deve stare nell'`if:` a livello di job.
|
|
34
|
+
const JOB_FILTER = "startsWith(github.event.label.name, 'trailhead:')";
|
|
35
|
+
|
|
36
|
+
function normalize(text) {
|
|
37
|
+
return String(text == null ? '' : text).replace(/\r\n/g, '\n');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isTrailheadGuard(text) {
|
|
41
|
+
return normalize(text).split('\n').slice(0, 5).some((line) => line.trim().startsWith(TRAILHEAD_MARK));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Estrae il testo dell'`if:` diretto di jobs.guard (indentazione 4, prima del
|
|
45
|
+
// successivo `steps:`/chiave), incluse le righe di continuazione di `>-`.
|
|
46
|
+
// null se jobs.guard non ha un `if:` a livello di job.
|
|
47
|
+
function jobGuardIf(text) {
|
|
48
|
+
const lines = normalize(text).split('\n');
|
|
49
|
+
const start = lines.findIndex((l) => /^ {2}guard:\s*$/.test(l));
|
|
50
|
+
if (start < 0) return null;
|
|
51
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
52
|
+
const line = lines[i];
|
|
53
|
+
if (/^\S/.test(line) || /^ {2}\S/.test(line)) return null; // fine del job
|
|
54
|
+
const m = /^ {4}if:\s*(.*)$/.exec(line);
|
|
55
|
+
if (!m) continue;
|
|
56
|
+
const parts = [m[1]];
|
|
57
|
+
for (let j = i + 1; j < lines.length && /^ {6,}\S/.test(lines[j]); j++) parts.push(lines[j].trim());
|
|
58
|
+
return parts.join(' ');
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function hasJobLevelFilter(text) {
|
|
64
|
+
const cond = jobGuardIf(text);
|
|
65
|
+
return cond !== null && cond.includes(JOB_FILTER);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function classify({ existingText, templateText }) {
|
|
69
|
+
if (existingText == null) return 'absent';
|
|
70
|
+
if (!isTrailheadGuard(existingText)) return 'foreign';
|
|
71
|
+
if (normalize(existingText) === normalize(templateText)) return 'current';
|
|
72
|
+
return hasJobLevelFilter(existingText) ? 'custom' : 'outdated';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const MESSAGES = {
|
|
76
|
+
absent: 'no trailhead label guard in this repo.',
|
|
77
|
+
current: 'the trailhead label guard is already current.',
|
|
78
|
+
outdated: 'the trailhead label guard is outdated: no job-level filter, so every label event starts a billed runner.',
|
|
79
|
+
custom: 'the trailhead label guard has the job-level filter but differs from the template; left untouched.',
|
|
80
|
+
foreign: 'a non-trailhead workflow sits at the label guard path; left untouched.',
|
|
81
|
+
installed: 'installed the trailhead label guard (commit it).',
|
|
82
|
+
upgraded: 'upgraded the trailhead label guard to the current template (commit it).',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
function writeAtomic(filePath, text, fsMod) {
|
|
86
|
+
const dir = path.dirname(filePath);
|
|
87
|
+
fsMod.mkdirSync(dir, { recursive: true });
|
|
88
|
+
const tmpPath = path.join(dir, `.trailhead-label-guard-${process.pid}-${crypto.randomBytes(4).toString('hex')}`);
|
|
89
|
+
try {
|
|
90
|
+
fsMod.writeFileSync(tmpPath, text);
|
|
91
|
+
fsMod.renameSync(tmpPath, filePath);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
try { fsMod.unlinkSync(tmpPath); } catch { /* best effort cleanup */ }
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function syncLabelGuard({ repoDir, templateText, apply = false, fs: fsMod = fs }) {
|
|
99
|
+
const file = path.join(repoDir, GUARD_REL);
|
|
100
|
+
const existingText = fsMod.existsSync(file) ? fsMod.readFileSync(file, 'utf8') : null;
|
|
101
|
+
const status = classify({ existingText, templateText });
|
|
102
|
+
if (apply && (status === 'absent' || status === 'outdated')) {
|
|
103
|
+
writeAtomic(file, templateText, fsMod);
|
|
104
|
+
const done = status === 'absent' ? 'installed' : 'upgraded';
|
|
105
|
+
return { status: done, file, message: MESSAGES[done] };
|
|
106
|
+
}
|
|
107
|
+
return { status, file, message: MESSAGES[status] };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function sha256(text) {
|
|
111
|
+
return crypto.createHash('sha256').update(normalize(text)).digest('hex');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function readGuard(repoDir, fsMod) {
|
|
115
|
+
const file = path.join(repoDir, GUARD_REL);
|
|
116
|
+
return fsMod.existsSync(file) ? fsMod.readFileSync(file, 'utf8') : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Registra la risposta all'offerta: hash del guard attuale, cosi' l'offerta
|
|
120
|
+
// torna solo se il file cambia. false se non c'e' un guard da ackare.
|
|
121
|
+
function ackGuard({ repoDir, fs: fsMod = fs }) {
|
|
122
|
+
const existingText = readGuard(repoDir, fsMod);
|
|
123
|
+
if (existingText == null) return false;
|
|
124
|
+
const file = path.join(repoDir, ACK_REL);
|
|
125
|
+
fsMod.mkdirSync(path.dirname(file), { recursive: true });
|
|
126
|
+
fsMod.writeFileSync(file, sha256(existingText) + '\n');
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isAcked(repoDir, existingText, fsMod) {
|
|
131
|
+
try {
|
|
132
|
+
return fsMod.readFileSync(path.join(repoDir, ACK_REL), 'utf8').trim() === sha256(existingText);
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Offerta di sessione (SessionStart o load-first inline): solo per un guard
|
|
139
|
+
// trailhead `outdated` non ancora ackato. Sola lettura, mai eccezioni.
|
|
140
|
+
function sessionNotice({ repoDir, templateText, scriptPath, fs: fsMod = fs }) {
|
|
141
|
+
try {
|
|
142
|
+
const existingText = readGuard(repoDir, fsMod);
|
|
143
|
+
if (classify({ existingText, templateText }) !== 'outdated') return null;
|
|
144
|
+
if (isAcked(repoDir, existingText, fsMod)) return null;
|
|
145
|
+
const cmd = `node "${scriptPath}" --repo "${repoDir}"`;
|
|
146
|
+
return [
|
|
147
|
+
`trailhead's label guard in this repo (${GUARD_REL}) is outdated: it has no job-level \`if:\`, so every \`issues: labeled\` event starts a GitHub Actions runner billed at least one minute, even for labels it ignores.`,
|
|
148
|
+
'Offer the user, once, without blocking their current request, two choices, and put the reason in the question itself, in plain words, not just the choices. Say that the workflow currently starts a GitHub Actions run every time any label is added to an issue, that GitHub bills each run at least one minute even when the workflow then does nothing, that trailhead adds labels all the time so this can reach thousands of billed runs a month and use up the Actions minutes, and that the updated version skips irrelevant label events before a runner starts (skipped runs are free) while keeping the same protection.',
|
|
149
|
+
`1) upgrade it: run \`${cmd} --apply\`, then commit the file (\`fix: skip trailhead label guard job for irrelevant label events\`) and push (a workflow file needs the \`workflow\` token scope: run \`gh auth refresh -s workflow\` if the push is rejected);`,
|
|
150
|
+
'2) keep it as it is.',
|
|
151
|
+
`This offer reappears every session until answered: once the user has picked either choice, run: ${cmd} --ack`,
|
|
152
|
+
].join('\n');
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function parseArgs(argv) {
|
|
159
|
+
let repo = null;
|
|
160
|
+
let apply = false;
|
|
161
|
+
let mode = 'sync';
|
|
162
|
+
for (let i = 0; i < argv.length; i++) {
|
|
163
|
+
const arg = argv[i];
|
|
164
|
+
if (arg === '--repo') {
|
|
165
|
+
i++;
|
|
166
|
+
if (i >= argv.length) return { error: 'usage: --repo requires a value' };
|
|
167
|
+
repo = argv[i];
|
|
168
|
+
} else if (arg === '--apply') {
|
|
169
|
+
apply = true;
|
|
170
|
+
} else if (arg === '--notice' || arg === '--ack') {
|
|
171
|
+
if (mode !== 'sync') return { error: 'usage: --notice and --ack are exclusive' };
|
|
172
|
+
mode = arg.slice(2);
|
|
173
|
+
} else {
|
|
174
|
+
return { error: `usage: unknown argument: ${arg}` };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (apply && mode !== 'sync') return { error: `usage: --apply cannot be combined with --${mode}` };
|
|
178
|
+
return { repo, apply, mode };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function main(argv, { cwd, stdout } = {}) {
|
|
182
|
+
const out = stdout || process.stdout;
|
|
183
|
+
const parsed = parseArgs(argv || []);
|
|
184
|
+
if (parsed.error) {
|
|
185
|
+
process.stderr.write(parsed.error + '\n');
|
|
186
|
+
return 2;
|
|
187
|
+
}
|
|
188
|
+
const repoDir = path.resolve(cwd || process.cwd(), parsed.repo || '.');
|
|
189
|
+
try {
|
|
190
|
+
const templateText = fs.readFileSync(path.join(__dirname, 'trailhead-label-guard.yml'), 'utf8');
|
|
191
|
+
if (parsed.mode === 'notice') {
|
|
192
|
+
const notice = sessionNotice({ repoDir, templateText, scriptPath: __filename });
|
|
193
|
+
if (notice) out.write(notice + '\n');
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
if (parsed.mode === 'ack') {
|
|
197
|
+
ackGuard({ repoDir });
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
out.write(JSON.stringify(syncLabelGuard({ repoDir, templateText, apply: parsed.apply })) + '\n');
|
|
201
|
+
} catch (err) {
|
|
202
|
+
if (parsed.mode !== 'sync') return 0; // notice/ack: mai rumore
|
|
203
|
+
out.write(JSON.stringify({ status: 'skipped', file: path.join(repoDir, GUARD_REL), message: 'error: ' + (err && err.message ? err.message : String(err)) }) + '\n');
|
|
204
|
+
}
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = { normalize, isTrailheadGuard, jobGuardIf, hasJobLevelFilter, classify, syncLabelGuard, sessionNotice, ackGuard, parseArgs, main, GUARD_REL, ACK_REL, JOB_FILTER };
|
|
209
|
+
|
|
210
|
+
if (require.main === module) {
|
|
211
|
+
process.exit(main(process.argv.slice(2), { cwd: process.cwd(), stdout: process.stdout }));
|
|
212
|
+
}
|
|
@@ -18,6 +18,11 @@ permissions:
|
|
|
18
18
|
|
|
19
19
|
jobs:
|
|
20
20
|
guard:
|
|
21
|
+
# Filtro a livello di job: un job saltato non consuma minuti Actions.
|
|
22
|
+
if: >-
|
|
23
|
+
startsWith(github.event.label.name, 'trailhead:') &&
|
|
24
|
+
github.event.sender.login != github.repository_owner &&
|
|
25
|
+
!contains(format(',{0},', vars.TRAILHEAD_LABEL_ALLOWLIST), format(',{0},', github.event.sender.login))
|
|
21
26
|
runs-on: ubuntu-latest
|
|
22
27
|
steps:
|
|
23
28
|
- uses: actions/github-script@v7
|