@marcomigozzi/trailhead 0.16.1 → 0.18.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 +82 -25
- package/README.md +82 -25
- package/bin/base-preflight-docs.test.js +4 -1
- package/bin/bulk-cross-reference-docs.test.js +36 -0
- package/bin/capture-action.test.js +167 -0
- package/bin/capture-docs.test.js +32 -0
- package/bin/engine-route-docs.test.js +155 -0
- package/bin/facet-stamp-docs.test.js +79 -0
- package/bin/forced-mutation-docs.test.js +38 -0
- package/bin/grill-model-docs.test.js +155 -0
- package/bin/grilling-holistic-review-docs.test.js +7 -3
- package/bin/guard-axis-docs.test.js +69 -0
- package/bin/inbox-needs-info-docs.test.js +68 -0
- package/bin/lib/codex-projection.js +28 -19
- package/bin/lib/codex-projection.test.js +77 -20
- package/bin/lib/host-descriptor.js +1 -1
- package/bin/lib/host-descriptor.test.js +6 -0
- package/bin/needs-you-docs.test.js +9 -2
- package/bin/options-in-chat-docs.test.js +65 -0
- package/bin/pick-stress-test-docs.test.js +1 -1
- package/bin/pipeline-review-docs.test.js +118 -0
- package/bin/plan-review-budget-docs.test.js +63 -0
- package/bin/repo-target-docs.test.js +41 -0
- package/bin/report-status-docs.test.js +10 -2
- package/bin/seam-coverage-docs.test.js +46 -0
- package/bin/session-trust-docs.test.js +37 -0
- package/bin/standards-brief-docs.test.js +112 -0
- package/bin/surfaced-scope-docs.test.js +57 -0
- package/bin/test-once-docs.test.js +101 -0
- package/bin/test-run-docs.test.js +47 -0
- package/bin/trailhead.js +15 -8
- package/bin/trailhead.test.js +71 -17
- package/bin/unblock-docs.test.js +56 -0
- package/bin/yes-accepts-recommended-docs.test.js +54 -0
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/agents/trailhead-code-review.md +2 -1
- package/plugins/trailhead/agents/trailhead-codebase-map.md +1 -1
- package/plugins/trailhead/agents/trailhead-grill.md +41 -0
- package/plugins/trailhead/agents/trailhead-plan.md +1 -1
- package/plugins/trailhead/agents/trailhead-verify.md +1 -0
- package/plugins/trailhead/hooks/lib/audit-walk.test.js +62 -0
- package/plugins/trailhead/hooks/lib/capture.test.js +439 -0
- package/plugins/trailhead/hooks/lib/checkout-find.test.js +195 -0
- package/plugins/trailhead/hooks/lib/checkout-in.test.js +83 -7
- package/plugins/trailhead/hooks/lib/dashboard.test.js +19 -0
- package/plugins/trailhead/hooks/lib/gh-auth-preflight.js +89 -0
- package/plugins/trailhead/hooks/lib/gh-auth-preflight.test.js +132 -0
- package/plugins/trailhead/hooks/lib/git-env-mod.test.js +55 -0
- package/plugins/trailhead/hooks/lib/git-env-spawn.test.js +73 -0
- package/plugins/trailhead/hooks/lib/git-env.test.js +51 -0
- package/plugins/trailhead/hooks/lib/global-config-dir.test.js +48 -0
- package/plugins/trailhead/hooks/lib/heredoc-mod.test.js +85 -0
- package/plugins/trailhead/hooks/lib/inbox-needs-info.test.js +179 -0
- package/plugins/trailhead/hooks/lib/inherit-session.js +17 -0
- package/plugins/trailhead/hooks/lib/issue-body.test.js +165 -0
- package/plugins/trailhead/hooks/lib/model-defaults-review.js +22 -1
- package/plugins/trailhead/hooks/lib/model-defaults-review.test.js +64 -1
- package/plugins/trailhead/hooks/lib/model-route.js +207 -0
- package/plugins/trailhead/hooks/lib/model-route.test.js +231 -0
- package/plugins/trailhead/hooks/lib/needs-you.test.js +45 -1
- package/plugins/trailhead/hooks/lib/plan-review-rounds.test.js +272 -0
- package/plugins/trailhead/hooks/lib/repo-target.test.js +256 -0
- package/plugins/trailhead/hooks/lib/report-status.test.js +56 -1
- package/plugins/trailhead/hooks/lib/statusline-git-env.test.js +47 -0
- package/plugins/trailhead/hooks/lib/test-run.test.js +757 -0
- package/plugins/trailhead/hooks/lib/trailhead-git-env.js +40 -0
- package/plugins/trailhead/hooks/mod/README.md +42 -17
- package/plugins/trailhead/hooks/mod/auto-run.ts +2 -1
- package/plugins/trailhead/hooks/mod/git-env.ts +31 -0
- package/plugins/trailhead/hooks/mod/heredoc.ts +79 -0
- package/plugins/trailhead/hooks/mod/map-pane-view.ts +3 -1
- package/plugins/trailhead/hooks/mod/map-pane.tsx +204 -49
- package/plugins/trailhead/hooks/mod/my-tickets.ts +76 -0
- package/plugins/trailhead/hooks/mod/repo-pin.ts +113 -9
- package/plugins/trailhead/hooks/mod/work-state-locate.ts +2 -1
- package/plugins/trailhead/hooks/trailhead-bash-guards.js +8 -8
- package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +28 -14
- package/plugins/trailhead/hooks/trailhead-check-update.js +20 -2
- package/plugins/trailhead/hooks/trailhead-check-update.test.js +60 -0
- package/plugins/trailhead/hooks/trailhead-labels-guard.js +2 -2
- package/plugins/trailhead/hooks/trailhead-labels-guard.test.js +1 -1
- package/plugins/trailhead/hooks/trailhead-test-guard.js +232 -0
- package/plugins/trailhead/hooks/trailhead-test-guard.test.js +172 -0
- package/plugins/trailhead/skills/_shared/capture-core.md +123 -0
- package/plugins/trailhead/skills/_shared/choices.md +2 -2
- package/plugins/trailhead/skills/_shared/configuration-reference.md +30 -16
- package/plugins/trailhead/skills/_shared/configuration.md +1 -1
- package/plugins/trailhead/skills/_shared/load-first.md +2 -0
- package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
- package/plugins/trailhead/skills/_shared/substrate-commands.md +19 -6
- package/plugins/trailhead/skills/_shared/substrate.md +4 -3
- package/plugins/trailhead/skills/_shared/surfaced-scope.md +23 -0
- package/plugins/trailhead/skills/_shared/teamwork.md +11 -5
- package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/code-review-pipeline.md +24 -0
- package/plugins/trailhead/skills/_shared/techniques/code-review.md +22 -6
- package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +6 -3
- package/plugins/trailhead/skills/_shared/techniques/debug.md +5 -2
- package/plugins/trailhead/skills/_shared/techniques/grilling.md +28 -3
- package/plugins/trailhead/skills/_shared/techniques/plan-review.md +16 -4
- package/plugins/trailhead/skills/_shared/techniques/research.md +4 -1
- package/plugins/trailhead/skills/_shared/techniques/tdd.md +4 -2
- package/plugins/trailhead/skills/_shared/techniques/verify.md +5 -1
- package/plugins/trailhead/skills/_shared/techniques.md +8 -6
- package/plugins/trailhead/skills/trailhead-capture/SKILL.md +2 -4
- package/plugins/trailhead/skills/trailhead-capture/references/capture.md +29 -4
- package/plugins/trailhead/skills/trailhead-chart/SKILL.md +2 -2
- package/plugins/trailhead/skills/trailhead-chart/references/charting.md +5 -5
- package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +11 -6
- package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +18 -3
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +12 -9
- package/plugins/trailhead/skills/trailhead-work/references/auto.md +3 -1
- package/plugins/trailhead/skills/trailhead-work/references/executor-brief.md +2 -0
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +13 -13
- package/plugins/trailhead/templates/statusline-trailhead.sh +10 -3
- package/plugins/trailhead/templates/trailhead-audit-walk.js +56 -4
- package/plugins/trailhead/templates/trailhead-auto-merge.js +2 -1
- package/plugins/trailhead/templates/trailhead-auto-queue.js +2 -1
- package/plugins/trailhead/templates/trailhead-auto-slot.js +2 -1
- package/plugins/trailhead/templates/trailhead-base-preflight.js +10 -5
- package/plugins/trailhead/templates/trailhead-capture.js +419 -0
- package/plugins/trailhead/templates/trailhead-checkout-find.js +257 -0
- package/plugins/trailhead/templates/trailhead-checkout-in.js +78 -21
- package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +2 -1
- package/plugins/trailhead/templates/trailhead-dashboard.js +15 -2
- package/plugins/trailhead/templates/trailhead-git-env.js +40 -0
- package/plugins/trailhead/templates/trailhead-inbox-needs-info.js +161 -0
- package/plugins/trailhead/templates/trailhead-isolation-answer.js +2 -1
- package/plugins/trailhead/templates/trailhead-isolation-copy.js +9 -4
- package/plugins/trailhead/templates/trailhead-issue-body.js +161 -12
- package/plugins/trailhead/templates/trailhead-merge-ticket.js +2 -1
- package/plugins/trailhead/templates/trailhead-needs-you.js +38 -4
- package/plugins/trailhead/templates/trailhead-plan-review-rounds.js +261 -7
- package/plugins/trailhead/templates/trailhead-repo-target.js +302 -0
- package/plugins/trailhead/templates/trailhead-report-status.js +57 -9
- package/plugins/trailhead/templates/trailhead-test-run.js +752 -0
- package/plugins/trailhead/templates/trailhead-uat-backfill.js +2 -1
- package/plugins/trailhead/templates/trailhead-uat-followup.js +2 -1
- package/plugins/trailhead/templates/trailhead-uat-setup.js +9 -4
- package/plugins/trailhead/templates/trailhead-unblock.js +2 -1
- package/plugins/trailhead/tests/auto-run-ui.test.ts +2 -1
- package/plugins/trailhead/tests/git-env.test.ts +20 -0
- package/plugins/trailhead/tests/map-pane-ui.test.ts +386 -50
- package/plugins/trailhead/tests/map-pane-view.test.ts +6 -0
- package/plugins/trailhead/tests/map-pane-write.test.ts +57 -1
- package/plugins/trailhead/tests/my-tickets.test.ts +66 -0
- package/plugins/trailhead/tests/repo-pin-late-repo.test.ts +3 -2
- package/plugins/trailhead/tests/repo-pin-relocate.test.ts +175 -0
- package/plugins/trailhead/tests/repo-pin.test.ts +20 -5
- package/plugins/trailhead/tests/unscrub.ts +8 -0
- package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +3 -2
- package/plugins/trailhead/tests/work-state-worktree.test.ts +2 -1
- package/plugins/trailhead/types/index.d.ts +7 -0
package/README.md
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
🇬🇧 **English** · 🇮🇹 [Italiano](README.it.md)
|
|
8
8
|
|
|
9
|
-
[](https://trailhead.marcomigozzi.it) [](https://github.com/ToRvaLDz/trailhead/actions/workflows/site-ci.yml) [](https://www.npmjs.com/package/@marcomigozzi/trailhead) [](LICENSE)   ](https://trailhead.marcomigozzi.it) [](https://github.com/ToRvaLDz/trailhead/actions/workflows/site-ci.yml) [](https://www.npmjs.com/package/@marcomigozzi/trailhead) [](LICENSE)   
|
|
10
10
|
|
|
11
11
|
</div>
|
|
12
12
|
|
|
13
13
|
**Start and drive large projects as a map of tickets on GitHub Issues, resolving one at a time until the way to the destination is clear.**
|
|
14
14
|
|
|
15
|
-
`trailhead` is an orchestrator skill for coding agents, running on [Claude Code](https://
|
|
15
|
+
`trailhead` is an orchestrator skill for coding agents, running on [Claude Code](https://code.claude.com/docs/en/overview) and [Codex CLI](https://developers.openai.com/codex/cli) from a single source. It gives a big, foggy idea a place to begin (a *trailhead*) and a disciplined way to walk it to a working result, with the whole plan living on your issue tracker instead of in scattered local files.
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
@@ -26,7 +26,7 @@ Two approaches to agent-driven project work each nail one half of the problem:
|
|
|
26
26
|
|
|
27
27
|
Working across both, one pattern kept recurring: **use a Wayfinder-style map and onboarding to decide the shape of the work, then a GSD-style engine to build each piece, without ever leaving the issue tracker.** `trailhead` is that pattern made into a set of cohesion-clustered skills over a shared core, its own self-contained take on both ideas rather than a wrapper around either.
|
|
28
28
|
|
|
29
|
-
> **trailhead is self-contained** in the sense that it has **zero external-plugin dependencies**: trailhead-owned agents and the host's built-ins are fine, and no other skill or plugin is required. Wayfinder and GSD are *inspiration*, not dependencies. Every technique those systems package as a separate skill (grilling, TDD, systematic debugging, codebase mapping, code review) is **built in here as an inline protocol**. trailhead's core invokes no other skill; each engine technique dispatches to **trailhead's own `trailhead-*` agent** (`trailhead-executor`, `trailhead-plan`, `trailhead-research`, `trailhead-code-review`, `trailhead-debug`, plus `trailhead-fix` and `trailhead-codebase-map`), authored once as a committed `agents/trailhead-*.md` file and **projected to each host** (Codex gets `~/.codex/agents/trailhead-*.toml` generated from that same source). These agents are **thin** (identity + tool policy + a config-driven model, with the technique protocol injected at spawn) and run on the host's native subagent toolkit (Claude Code's built-in `Agent` tool, or Codex's `multi_agent` toolkit). The only thing to install is trailhead itself. There is one opt-in exception: hosted mockups. `design: artifacts` uses Claude Code's built-in `Artifact` tool (nothing to install), and `design: stitch` uses the Google Stitch MCP (offered as a one-time install). See [Mockups before code](#-mockups-before-code).
|
|
29
|
+
> **trailhead is self-contained** in the sense that it has **zero external-plugin dependencies**: trailhead-owned agents and the host's built-ins are fine, and no other skill or plugin is required. Wayfinder and GSD are *inspiration*, not dependencies. Every technique those systems package as a separate skill (grilling, TDD, systematic debugging, codebase mapping, code review) is **built in here as an inline protocol**. trailhead's core invokes no other skill; each engine technique dispatches to **trailhead's own `trailhead-*` agent** (`trailhead-executor`, `trailhead-plan`, `trailhead-research`, `trailhead-code-review`, `trailhead-debug`, `trailhead-verify`, `trailhead-grill`, plus `trailhead-fix` and `trailhead-codebase-map`), authored once as a committed `agents/trailhead-*.md` file and **projected to each host** (Codex gets `~/.codex/agents/trailhead-*.toml` generated from that same source). These agents are **thin** (identity + tool policy + a config-driven model, with the technique protocol injected at spawn) and run on the host's native subagent toolkit (Claude Code's built-in `Agent` tool, or Codex's `multi_agent` toolkit). The only thing to install is trailhead itself. There is one opt-in exception: hosted mockups. `design: artifacts` uses Claude Code's built-in `Artifact` tool (nothing to install), and `design: stitch` uses the Google Stitch MCP (offered as a one-time install). See [Mockups before code](#-mockups-before-code).
|
|
30
30
|
|
|
31
31
|
The design choices that fall out of it:
|
|
32
32
|
|
|
@@ -51,7 +51,7 @@ The design choices that fall out of it:
|
|
|
51
51
|
|
|
52
52
|
> **Runs on Claude Code and Codex CLI.** trailhead is authored once; the installer projects host-native artifacts. On **Claude Code**: a skill + `/trailhead:*` commands + hooks. On **Codex CLI**: a native skill invoked `$trailhead`, one `$trailhead-<verb>` skill per verb for discoverability, and the guardrails as native Codex hooks. Choose the host with `--claude` / `--codex`, or let the installer auto-detect. More hosts can follow (the installer uses a per-host descriptor).
|
|
53
53
|
|
|
54
|
-
**Prerequisites:** an authenticated [`gh` CLI](https://cli.github.com) (the tracker is GitHub Issues) and a GitHub repo to work in. The npm path also needs Node 18+.
|
|
54
|
+
**Prerequisites:** an authenticated [`gh` CLI](https://cli.github.com) (the tracker is GitHub Issues) and a GitHub repo to work in. The npm path also needs Node 18+. If an invalid `GH_TOKEN`/`GITHUB_TOKEN` in your environment shadows a working `gh` login, a check at session start says so once, with the fix.
|
|
55
55
|
|
|
56
56
|
### As a Claude Code plugin (native, managed by `/plugin`)
|
|
57
57
|
```
|
|
@@ -69,9 +69,9 @@ npx @marcomigozzi/trailhead --symlink # dev install (Claude; symlink to the c
|
|
|
69
69
|
npx @marcomigozzi/trailhead --uninstall # remove everything it added (pair with --claude / --codex)
|
|
70
70
|
npx @marcomigozzi/trailhead --dir=<path> # target a specific config dir
|
|
71
71
|
```
|
|
72
|
-
On **Claude Code** it copies the skill (+ its `references/`), the `/trailhead:*` commands, the hooks (into `hooks/`, registered in `settings.json`) including a SessionStart **update check
|
|
72
|
+
On **Claude Code** it copies the skill (+ its `references/`), the `/trailhead:*` commands, the hooks (into `hooks/`, registered in `settings.json`) including a SessionStart **update check** (which also runs the `gh` token check), and the templates (label-guard workflow, statusline, the git `commit-msg` hook and the helper scripts the engine runs), idempotently.
|
|
73
73
|
|
|
74
|
-
On **Codex CLI** (`--codex`) it projects a native Codex skill invoked as **`$trailhead`** (`$trailhead work`, `$trailhead new "idea"`, …), one thin **`$trailhead-<verb>`** skill per verb for discoverability, the
|
|
74
|
+
On **Codex CLI** (`--codex`) it projects a native Codex skill invoked as **`$trailhead`** (`$trailhead work`, `$trailhead new "idea"`, …), one thin **`$trailhead-<verb>`** skill per verb for discoverability, the nine guardrails as native Codex **hooks** (`~/.codex/hooks.json`, enabling `features.hooks`; the six Bash guards share one process behind a single registration; the search guard is Claude-Code-only and isn't projected to Codex), and per-technique subagent model pins from **`models.codex.*`** into `~/.codex/agents/`. Codex **0.145.0+** is required (the installer gates on `codex --version`). The `artifacts` mockup mode (Claude Code's `Artifact` tool) isn't available on Codex; the cross-host `design: stitch` mode (Google Stitch MCP) is the hosted option there, with local disk as the fallback.
|
|
75
75
|
|
|
76
76
|
Re-run the install to update, or run **`/trailhead:update`** (Claude) / **`$trailhead update`** (Codex) from inside the agent: it detects how trailhead was installed and installs the newer version where that is safe (a `git pull` for a dev-symlink, an `npx` re-run for npm, or `/plugin update` for the plugin). When a newer version exists, the statusline shows a `⬆ trailhead <version>` flag (Claude).
|
|
77
77
|
|
|
@@ -94,7 +94,7 @@ The fog holds only what you cannot phrase yet: a question you can already phrase
|
|
|
94
94
|
|
|
95
95
|
A map is scoped to **one** effort, so knowledge that belongs to the *repo* (not to any single map) lives in **repo-scoped anchor issues**, created once and shared by every map (each map's Notes just links them, so nothing is stranded when a map finishes):
|
|
96
96
|
|
|
97
|
-
- **`trailhead:codebase`**: the distilled codebase map (architecture, stack, conventions, risks, test/build), written once at adopt and refreshed only on major drift.
|
|
97
|
+
- **`trailhead:codebase`**: the distilled codebase map (architecture, stack, conventions, risks, test/build), written once at adopt and refreshed only on major drift. Each facet carries an `_as of <short sha>_` stamp of the commit it was written or last patched against.
|
|
98
98
|
- **`trailhead:conventions`**: the project's **way of working**, readable by everyone: a small machine-read header the engine obeys (`git: main|pr`, `release: command|auto`, `isolation: auto|none|worktree|clone`) over human prose. `/trailhead:adopt` and `:new` ask for it up front. `isolation: auto` (the default) decides per ticket at work-start: it asks whether you'll work more than one ticket at once and, if so, uses a `git worktree` (plain repo) or a `clone` (submodule superrepo); otherwise it works in the current checkout. `isolation: worktree` gives each executing ticket its own `git worktree` + branch (concurrent sessions on one clone never share a working tree); `isolation: clone` gives it a dedicated clone instead, for path-bound apps a worktree can't build and for submodule-bearing superrepos (where `worktree` is escalated to `clone`).
|
|
99
99
|
- **`trailhead:dashboard`**: the pinned index of the whole surface, a link to every open map (with GitHub's native progress bars), the whiteboard, and live counts (inbox, whiteboard frontier). Refreshed when a map is charted or exhausted, when a whiteboard ticket is born or resolved (it has no native progress bar, so the dashboard is the only place it shows; a `quick`-born ticket, worked and resolved in one sitting, refreshes at its handoff or pause, not at creation), and on demand via `/trailhead:dashboard`.
|
|
100
100
|
|
|
@@ -219,7 +219,7 @@ Every verb is also a namespaced command (`/trailhead:new`, `/trailhead:work`, `/
|
|
|
219
219
|
| `/trailhead:ticket <type> <title>` | open a ticket on the fly (diverges briefly first: a micro-charting act) |
|
|
220
220
|
| `/trailhead:split [ticket]` | split an oversized ticket into children, supersede the original |
|
|
221
221
|
| `/trailhead:auto [map]` | run a chosen scope (one map, the whiteboard or everything) in up to 4 parallel slots, until exhaustion or a stop; parks what needs you |
|
|
222
|
-
| `/trailhead:grill [topic]` | run a standalone grilling session on a decision/topic |
|
|
222
|
+
| `/trailhead:grill [topic\|ticket]` | run a standalone grilling session on a decision/topic |
|
|
223
223
|
| `/trailhead:map` | show the low-res map (destination, decisions, frontier, fog) |
|
|
224
224
|
| `/trailhead:dashboard` | show the repo dashboard: the pinned index of every open map, the whiteboard, and live counts |
|
|
225
225
|
| `/trailhead:panel` | open or close the live map panel: the active map's open tickets by state, with a `work` button per frontier ticket (**Claude Code only**: needs the trailhead mod, 2.1.287+) |
|
|
@@ -228,15 +228,15 @@ Every verb is also a namespaced command (`/trailhead:new`, `/trailhead:work`, `/
|
|
|
228
228
|
|
|
229
229
|
The trailhead mod (Claude Code 2.1.287+) ships inside the plugin and draws the work beside the conversation. Codex keeps the command hooks and has no panel.
|
|
230
230
|
|
|
231
|
-
|
|
231
|
+
<img src="assets/screenshots/map-panel.png" width="520" alt="The map panel with each section in its own colour: open maps, in progress, needs you, frontier with a work button per ticket, UAT, paused, blocked, the burn-down, and a footer with refresh and close">
|
|
232
232
|
|
|
233
|
-
|
|
233
|
+
Each section of the map panel has its own glyph and colour, with a coloured gutter along its rows. The panel lists the open maps to view, your tickets in progress right under them (in an `In progress` group, from every map and the whiteboard), the tickets an autonomous run parked for you in a `Needs you` group, the frontier with a `work` button per ticket that clears the context and starts the ticket (in the map's checkout; if that checkout has moved, it is found again by its `origin` remote, and you pick one when several match), who holds what (your own ticket is marked `this session`), the pending UAT follow-ups in a `UAT` group (each naming the ticket it tests), the paused ones in a `Paused` group, and what is blocked. A dim line under the Whiteboard row totals the open tickets across every open map plus the whiteboard, the same number `/trailhead:map` states (`at least N` when more open maps exist than the panel reads). The footer has `refresh` and `close`. `/trailhead:panel` toggles it (asked at session start, before the screen is ready, it opens as soon as it is), and it opens by itself once in fullscreen. In the desktop app a session starts at its first message, so the panel cannot appear on a new session where nothing has been typed; `/trailhead:panel` as that first message opens it.
|
|
234
234
|
|
|
235
|
-
|
|
235
|
+
<img src="assets/screenshots/map-panel-viewing.png" width="520" alt="The panel viewing another map, with a viewing and active header and a back to active button">
|
|
236
236
|
|
|
237
237
|
A click only views a map, and `back to active` returns to the active one. The active map changes when you work one of its tickets. A `Whiteboard` row above the maps list, with its `N open / M` counts, shows the repo index instead: every open map (a click opens it) and the whiteboard's loose tickets by state, each frontier one with its `work` button; `map` goes back.
|
|
238
238
|
|
|
239
|
-
|
|
239
|
+
<img src="assets/screenshots/burndown.png" width="520" alt="The Burn-down section of the panel: counts line and a chart of open tickets over time">
|
|
240
240
|
|
|
241
241
|
The burn-down shows the open tickets over time, from the map's first ticket to now, with done and removed counted apart.
|
|
242
242
|
|
|
@@ -388,7 +388,7 @@ Everything the map needs is expressed as GitHub labels, so state is queryable in
|
|
|
388
388
|
- **Structural:** `trailhead:map`, `trailhead:ticket`
|
|
389
389
|
- **Repo-scoped anchors (one each per repo, pinned):** `trailhead:codebase` 🧱 (the distilled codebase map), `trailhead:conventions` 📜 (the way of working), `trailhead:dashboard` 📊 (the pinned index of maps + whiteboard + counts); each title carries its icon (like the map's 🗺) so the pinned anchors stand apart at a glance
|
|
390
390
|
- **Type (one per ticket):** 🧭 `trailhead:decision` · 🔬 `research` · 🎨 `prototype` · 🔨 `build` · 🐛 `bug` · 🔧 `task`
|
|
391
|
-
- **State:** `trailhead:blocked` (has an open blocker) · `seed` (parked on a trigger) · `out-of-scope` (closed, beyond the destination) · `superseded` (closed, split into children) · `paused` (checkpointed, off the frontier) · `needs-you` (parked by an autonomous run for you: runs skip it, it stays on your frontier) · `unverified` (quarantined until a maintainer adopts it) · `fog` (kept open for clarification)
|
|
391
|
+
- **State:** `trailhead:blocked` (has an open blocker) · `seed` (parked on a trigger) · `out-of-scope` (closed, beyond the destination) · `superseded` (closed, split into children) · `paused` (checkpointed, off the frontier) · `needs-you` (parked by an autonomous run for you: runs skip it, it stays on your frontier) · `unverified` (quarantined until a maintainer adopts it) · `fog` (kept open for clarification) · `needs-info` (inbox issue waiting on its reporter)
|
|
392
392
|
- **Container:** `trailhead:whiteboard` (a loose, map-less ticket, off every map's frontier, on the whiteboard's own)
|
|
393
393
|
- **Marker:** `trailhead:uat` (a pending UAT follow-up; never a frontier filter) · `trailhead:map-<n>` (scopes a ticket to its map)
|
|
394
394
|
|
|
@@ -407,13 +407,60 @@ Many people (and their agent sessions) share one map and work it concurrently:
|
|
|
407
407
|
- **Scope tickets by path on a monorepo.** A ticket can carry a `Scope:` line naming the package/dir(s) it touches: disjoint scopes parallelize safely, and it scopes build/test and the worktree's commits to the affected package. `/trailhead:adopt` detects a monorepo and proposes both `isolation: worktree` and the `Scope:` convention. Under `isolation: none`, `Scope:` also **serializes** a shared area: at claim time a session holds off if another in-progress ticket's scope overlaps, so two tickets scoped to the same submodule/package can't run at once (how you keep two sessions off one path-bound app).
|
|
408
408
|
- **Git submodules are handled as separate repos.** Work that lands inside a submodule is committed *inside the submodule* (on a branch) and published before a gitlink bump in the parent points at it. Because superproject worktrees share one submodule git dir, a **submodule-bearing repo resolves to `clone`** (under the default `auto`, or by escalating an explicit `worktree`), so each ticket gets independent submodule state; the clone provisions its submodules at setup (`--recurse-submodules`) and Verify checks `git status` inside each touched submodule. A ticket spanning two submodules stays atomic via a single parent commit that bumps both gitlinks together; its `Scope:` names both, and the same submodule counts as overlapping scope, so two tickets on it serialise. `/trailhead:adopt` detects `.gitmodules` and writes the rule into the conventions.
|
|
409
409
|
- **State lives on the tickets** (claim, labels, comments; all conflict-free); the map body is a re-readable index, not the source of truth. The durable record of a resolution is the ticket's own comment.
|
|
410
|
-
- **
|
|
411
|
-
- **
|
|
410
|
+
- **Forks write where you mean.** In a fork clone `gh` can resolve the parent repository, so once per session, before the first tracker write, trailhead checks where issue writes would land. When that is ambiguous it asks which repository to write to, makes it the `gh` default and remembers the answer in `.trailhead/repo-target.json`. Only the remote named `origin` on github.com is read as the fork side.
|
|
411
|
+
- **Labels are protected from outsiders.** `trailhead:*` labels drive the map, so trailhead trusts a labelled issue only when every `trailhead:*` label on it was applied by a trusted labeller (a write-access collaborator, `github-actions[bot]`, or a login in the `TRAILHEAD_LABEL_ALLOWLIST` repo variable: the same labellers the label guard never strips) and (for a ticket) it carries a valid `Parent:`. Anything else is quarantined as `trailhead:unverified`, off the frontier. GitHub already blocks non-write users from labelling; against triage users and automations, trailhead can **install the label guard GitHub Action for you** (from `templates/trailhead-label-guard.yml`) into your repo's `.github/workflows/`, where it strips unauthorized `trailhead:*` labels at the source. Installing a workflow needs a token with the `workflow` scope, so trailhead runs `gh auth refresh -s workflow` if the push is rejected. For automations (CI, cron, bots) that open tickets without a chat session, `templates/trailhead-capture.js` captures a bug, todo, seed or idea with the full label wiring and refuses to run as an identity the guard would strip, and on GitHub the [`actions/capture`](#capture-from-ci) composite Action wraps it in a few lines of workflow.
|
|
412
|
+
- **Issues opened by others are triaged, not trusted blindly.** `/trailhead:inbox` lists inbound issues (bug reports, requests, questions that aren't yet trailhead tickets) and integrates the worthwhile ones **in place**: reframing them as tickets, keeping the reporter's authorship, and applying the labels on adoption. The rest routes to fog, out-of-scope, duplicate, or needs-info. A needs-info issue is parked with the question posted first and leaves the queue; it comes back in the inbox's *Waiting on the reporter* section only when the reporter answers, and after `inbox.needs_info_days` (default 14) with no answer the inbox offers to close it, never closing it on its own. Quarantined `trailhead:unverified` issues are listed there too, to adopt or reject.
|
|
413
|
+
|
|
414
|
+
### Capture from CI
|
|
415
|
+
|
|
416
|
+
`ToRvaLDz/trailhead/actions/capture` is a thin composite Action over the capture helper: every rule (trust, destination, dedupe, secret scan, labels, edge, dashboard) stays in the helper. Pin a release tag (the Action ships from 0.17.0).
|
|
417
|
+
|
|
418
|
+
```yaml
|
|
419
|
+
permissions:
|
|
420
|
+
issues: write
|
|
421
|
+
|
|
422
|
+
jobs:
|
|
423
|
+
build:
|
|
424
|
+
runs-on: ubuntu-latest
|
|
425
|
+
steps:
|
|
426
|
+
# ... your build and test steps ...
|
|
427
|
+
- if: failure()
|
|
428
|
+
uses: ToRvaLDz/trailhead/actions/capture@v0.18.0
|
|
429
|
+
with:
|
|
430
|
+
type: bug
|
|
431
|
+
title: "CI failed on ${{ github.ref_name }}"
|
|
432
|
+
body: "Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
|
433
|
+
dedupe-key: ci-failure-${{ github.job }}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
A manual variant works the same way: trigger the job with `workflow_dispatch` and pass `type`, `title` and `body` from `github.event.inputs`.
|
|
437
|
+
|
|
438
|
+
| Input | Required | Meaning |
|
|
439
|
+
|---|---|---|
|
|
440
|
+
| `type` | yes | `bug`, `todo`, `idea` or `seed` |
|
|
441
|
+
| `title` | yes | issue title |
|
|
442
|
+
| `body` / `body-file` | one of | issue body, or a file path (`-` for stdin); not both |
|
|
443
|
+
| `map` / `of` | no | file under map `<n>`, or inherit the map of origin ticket `<n>` (bug only); exclusive |
|
|
444
|
+
| `trigger` | seed only | the `Trigger:` text |
|
|
445
|
+
| `unverified` | no | `true` adds `trailhead:unverified` (not for `idea`) |
|
|
446
|
+
| `dedupe-key` | no | idempotency key, 1 to 100 characters of `A-Za-z0-9._:-` (no spaces or `/`) |
|
|
447
|
+
| `no-dashboard` | no | `true` skips the whiteboard dashboard refresh |
|
|
448
|
+
| `repo` | no | target `owner/name` (default: this repo) |
|
|
449
|
+
| `token` | no | token for the helper (default: `github.token`) |
|
|
450
|
+
|
|
451
|
+
Outputs: `issue`, `url` and `status` (`created`, `existing`, `partial`, `error`, `usage`).
|
|
452
|
+
|
|
453
|
+
- The default token acts as `github-actions[bot]`, which the label guard trusts; the helper verifies it after the create.
|
|
454
|
+
- A re-run with the same `dedupe-key` returns `existing` and creates nothing.
|
|
455
|
+
- `partial` (exit 3, the issue exists but the sub-issue edge failed), `error` and `usage` (exit 2, bad inputs) fail the step; `status` is still set.
|
|
456
|
+
- The runner needs bash, node and gh, all present on GitHub-hosted runners; self-hosted runners must provide them.
|
|
412
457
|
|
|
413
458
|
## ✅ Keeping tickets honest
|
|
414
459
|
|
|
415
460
|
A ticket is one answerable Question, sized to one session, so you never grow it in flight. When new scope surfaces while you're working (testing especially sparks ideas), you decide per idea: small and part of the same Question → just do it; balloons the ticket past one session → **split** it; separate work or a follow-up → **capture** it (`idea`/`todo`/`ticket`/`bug`) and keep going. Extras never become silent lines inside the ticket you're on.
|
|
416
461
|
|
|
462
|
+
A captured ticket lands where its topic belongs: on the current map when it serves that map's destination, on another open map when it plainly serves that one, and otherwise trailhead asks (every open map, then the whiteboard). While a map is open it never falls back to the whiteboard silently, and it tells you in one line what it opened and where.
|
|
463
|
+
|
|
417
464
|
Tickets that spin off from other work carry a lineage pointer in their body, so the map stays traceable:
|
|
418
465
|
|
|
419
466
|
| Pointer | Meaning |
|
|
@@ -432,11 +479,11 @@ Three layers: **nearest wins**, key by key (a key unset at one layer inherits th
|
|
|
432
479
|
- **Global**: `trailhead/config.json` in your host's config directory: `$CLAUDE_CONFIG_DIR` (default `~/.claude`) on Claude Code, `$CODEX_HOME` (default `~/.codex`) on Codex. Your standing defaults across every project.
|
|
433
480
|
- **Defaults**: the built-in values.
|
|
434
481
|
|
|
435
|
-
`/trailhead:config` runs a **guided, menu-driven setup**: pick the scope, then walk each setting (⚙️ way of working (git · release · isolation) · 🌐 ticket language · 🧠 models (a High / Balanced / Low profile, or Manual) · 🎨 design + approval · 🧪 TDD · ⚖️ effort · 🖥️ acceptance testing (browser, UAT setup, a test account) · 🧑⚖️ plan review · 📊 statusline) as an icon-labelled menu; no hand-editing JSON. Every step is asked (none skipped), and **plan and execute models are always two separate, version-pinned choices**. `config get` prints the effective merged config; `config set <key> <value>` writes one key.
|
|
482
|
+
`/trailhead:config` runs a **guided, menu-driven setup**: pick the scope, then walk each setting (⚙️ way of working (git · release · isolation) · 🌐 ticket language · 🧠 models (a High / Balanced / Low profile, or Manual) · 🎨 design + approval · 🧪 TDD (and test suite) · ⚖️ effort · 🖥️ acceptance testing (browser, UAT setup, a test account) · 🧑⚖️ plan review · 📊 statusline) as an icon-labelled menu; no hand-editing JSON. Every step is asked (none skipped), and **plan and execute models are always two separate, version-pinned choices**. `config get` prints the effective merged config; `config set <key> <value>` writes one key.
|
|
436
483
|
|
|
437
484
|
`/trailhead:prune [map]` reclaims a map's sub-issue slots under GitHub's 100-cap (drops closed/orphan edges, keeps every reference). Offered automatically when a map is near or at the cap, and runnable on demand.
|
|
438
485
|
|
|
439
|
-
`/trailhead:audit [map] [--all]` hunts regressions: it checks that the tickets closed since the last audit are still true against the current code. Code a ticket's commits left untouched is classed unchanged with no agent (a fast, deterministic git check that relies on the `Refs: #<n>` commit trailers), decisions, research, tasks and prototypes get a light inline check, and a full verification agent runs only where the code changed or the fast path cannot decide (no `Refs:` commits, for example). Each ticket is classed implemented, unchanged, evolved (a later ticket changed it on purpose, listed with no proposal), implemented wrong, or not implemented. It also flags decisions closed with no build ticket carrying their answer. It then compares the open ones for duplicates and groupable clusters. It is read-only until you confirm: every fix (a regression bug, a reopen, the missing build tickets of a decision, a duplicate close, a supersede-and-merge) is a proposal applied only on an explicit yes. `--all` re-audits every closed ticket.
|
|
486
|
+
`/trailhead:audit [map] [--all]` hunts regressions: it checks that the tickets closed since the last audit are still true against the current code. Code a ticket's commits left untouched is classed unchanged with no agent (a fast, deterministic git check that relies on the `Refs: #<n>` commit trailers), decisions, research, tasks and prototypes get a light inline check, and a full verification agent runs only where the code changed or the fast path cannot decide (no `Refs:` commits, for example). A whole-repo audit also flags codebase facets whose cited files changed after their `_as of <short sha>_` stamp (report-only). Each ticket is classed implemented, unchanged, evolved (a later ticket changed it on purpose, listed with no proposal), implemented wrong, or not implemented. It also flags decisions closed with no build ticket carrying their answer. It then compares the open ones for duplicates and groupable clusters. It is read-only until you confirm: every fix (a regression bug, a reopen, the missing build tickets of a decision, a duplicate close, a supersede-and-merge) is a proposal applied only on an explicit yes. `--all` re-audits every closed ticket.
|
|
440
487
|
|
|
441
488
|
The **📊 statusline** step offers to install trailhead's Claude Code status bar: one line with **model · project · branch · plan usage (`5h %` · reset · `7d %`) · a context-window bar**, plus a **second line with the active ticket** (`▸ #N Title`) whenever you're working one, and a `⬆ trailhead <version>` flag when a newer trailhead is available (run `/trailhead:update`). The project is always the main repo's name even from an isolated checkout, and the branch carries a `(WT)` tag in a worktree or `(C)` in a per-ticket clone (nothing on the original checkout). It's a global Claude Code setting; if you already run a statusline (e.g. `ccstatusline`) the setup asks before replacing it, and the script also exposes `--ticket-only` / `--context-only` / `--usage-only` segments to slot into an existing tool.
|
|
442
489
|
|
|
@@ -445,8 +492,8 @@ The **📊 statusline** step offers to install trailhead's Claude Code status ba
|
|
|
445
492
|
| Key | Values (default **bold**) | Effect |
|
|
446
493
|
|---|---|---|
|
|
447
494
|
| `ticket.language` | an ISO 639-1 code (**`en`**) | the language trailhead **writes** its GitHub prose & commit descriptions in, decoupled from the language it converses in |
|
|
448
|
-
| `models.{plan,execute,research,review,debug,verify,codebase-map}` | a full **versioned** model id (**Balanced-profile default**; `inherit session` is an explicit choice) | which model runs each activity; `plan` and `execute` are always set separately |
|
|
449
|
-
| `models.codex.{plan,execute,research,review,debug,verify,codebase-map}` | an OpenAI model id, or `{model, effort}` (**inherit session**) | the same per-activity split on Codex, independent of `models.*` (Claude ids); re-run the installer to project it into `~/.codex/agents/` |
|
|
495
|
+
| `models.{plan,execute,research,review,debug,verify,codebase-map,grill}` | a full **versioned** model id (**Balanced-profile default**; `inherit session` is an explicit choice) | which model runs each activity; `plan` and `execute` are always set separately |
|
|
496
|
+
| `models.codex.{plan,execute,research,review,debug,verify,codebase-map,grill}` | an OpenAI model id, or `{model, effort}` (**inherit session**) | the same per-activity split on Codex, independent of `models.*` (Claude ids); re-run the installer to project it into `~/.codex/agents/` |
|
|
450
497
|
| `design` | **`disk`** \| `artifacts` \| `stitch` | where UI mockups go: local throwaway HTML, a Design canvas **Artifact** on claude.ai (Claude Code only), or Google Stitch (cross-host, via the Stitch MCP). The legacy `claude.ai/design` value is read as `artifacts` |
|
|
451
498
|
| `design.project` | canvas Artifact URL / Stitch project id | `artifacts` and `stitch` only: the canvas or Stitch project the mockups land on; set at the first UI screen (pick, paste or create), not by hand. A leftover claude.ai/design project is not reused |
|
|
452
499
|
| `design.surface` | **`canvas`** \| `design-system` | `stitch` only: a Stitch project of screens (default) or a shared Stitch design system |
|
|
@@ -455,8 +502,13 @@ The **📊 statusline** step offers to install trailhead's Claude Code status ba
|
|
|
455
502
|
| `tdd` | **`seams`** \| `on` \| `off` | how the `build` engine tests |
|
|
456
503
|
| `acceptance.browser` | **`auto`** \| `on` \| `off` | drive the browser in Verify (after asking who drives, AI or human, or skip); `off` still runs AI-driven checks via commands, and walks you through a conversational UAT for anything only you can do (step by step in chat, not a checklist to self-serve) |
|
|
457
504
|
| `testing.webapp` / `testing.url` | bool / URL | is it browser-drivable, and where |
|
|
505
|
+
| `testing.full` | shell command (**unset**) | the full test suite; when set, the engine runs it through the [test queue and cache](#test-runs) |
|
|
506
|
+
| `testing.targeted` | shell command with `{files}` (**unset**) | a focused run; `{files}` becomes the single-quoted files |
|
|
507
|
+
| `testing.maxParallel` | positive integer (**`1`**) | how many heavy test runs may run at once across the whole machine |
|
|
458
508
|
| `plan_review` | **`off`** \| `on` \| CLI list | send `build` PLANs to external AI CLIs (Gemini, Codex, …) for a second opinion and converge on their concerns |
|
|
459
509
|
| `plan_review.rounds` | integer (**`2`**) | max converge-and-re-review rounds |
|
|
510
|
+
| `plan_review.context` | object (`{"<cli>": <tokens>}`) | override a reviewer CLI's context window; a reviewer whose estimated prompt is over 65% of its window is skipped and recorded in `PLAN-REVIEW` |
|
|
511
|
+
| `inbox.needs_info_days` | positive integer (**`14`**) | days an inbox issue may wait on its reporter before the inbox offers to close it (never closes on its own) |
|
|
460
512
|
| `code_review.rounds` | integer (**`2`**) | max fix-and-re-review rounds in Code review before its boundary HITL checkpoint |
|
|
461
513
|
| `effort` | **`standard`** \| `lean` | how hard the `build`/`bug` engine works a change: `standard` runs every step, `lean` size-triages small changes and offers to skip the heavy ones (plan pass, TDD, cross-AI review, full code review, goal-backward verify). Governs `build`/`bug` only: `quick` always size-triages regardless of this key |
|
|
462
514
|
| `isolation.copy` | array of repo-relative paths (**unset**) | gitignored files and directories (`.env`, `.npmrc`, local config) copied from the main checkout into every new `worktree`/`clone` workspace, never overwriting; unsafe paths (absolute, `..`, `.git`) are rejected. When unset, trailhead suggests the `.env` files that have a tracked `*.example` sibling |
|
|
@@ -468,10 +520,11 @@ The **📊 statusline** step offers to install trailhead's Claude Code status ba
|
|
|
468
520
|
|
|
469
521
|
- `plan` and `execute` are the build/bug engine's steps. Execute is a subagent too, and it commits to `main`.
|
|
470
522
|
- `research`, `review`, `debug`, `verify` (the goal-backward Verify check), and the codebase-map fan-out each run on their own key.
|
|
523
|
+
- `grill` runs the Grilling rounds on the read-only `trailhead-grill` subagent; unset, it uses the same model as `plan`, and an existing config with no `grill` key simply grills on its `plan` model. The session still asks you each question and keeps the answers.
|
|
471
524
|
|
|
472
|
-
Choose `plan` and `execute` separately, each by **full versioned id** (never a bare `opus` or `sonnet`). A key left unset uses its Balanced-profile default, and one equal to the session model simply runs inline; set `inherit session` to use the session model.
|
|
525
|
+
Choose `plan` and `execute` separately, each by **full versioned id** (never a bare `opus` or `sonnet`). A key left unset uses its Balanced-profile default, and one equal to the session model simply runs inline; set `inherit session` to use the session model. On Codex, an unset `models.codex.grill` follows `models.codex.plan` when that is pinned (re-run the installer to project it), else inherits the session model; a pin equal to the session model runs inline. A profile never writes a `grill` entry, so it keeps following `plan`.
|
|
473
526
|
|
|
474
|
-
The **main session stays the orchestrator**. It holds the interactive moments (a build's Discuss, and Verify's acceptance/UAT) and it does charting
|
|
527
|
+
The **main session stays the orchestrator**. It holds the interactive moments (a build's Discuss, and Verify's acceptance/UAT) and it does charting and ticket-writing on its own model; for grilling it relays each question from `trailhead-grill` and asks you itself. No key governs ticket quality, so run charting sessions on your strong model.
|
|
475
528
|
|
|
476
529
|
Example config file (the same shape works for the project `.trailhead/config.json` and the global `trailhead/config.json` in your config directory):
|
|
477
530
|
|
|
@@ -481,13 +534,16 @@ Example config file (the same shape works for the project `.trailhead/config.jso
|
|
|
481
534
|
|
|
482
535
|
**Design mockups and acceptance testing.** How the `design.*` keys play out (canvas, design system, approval, moving from claude.ai/design) is in [Mockups before code](#-mockups-before-code); how `acceptance.browser` and `testing.*` drive Verify is in [Acceptance testing](#-acceptance-testing-uat).
|
|
483
536
|
|
|
484
|
-
|
|
537
|
+
<a id="test-runs"></a>
|
|
538
|
+
**Test runs.** With `testing.full` set, the engine runs the suite through `trailhead-test-run.js`. **Queue**: at most `testing.maxParallel` heavy runs at once across every session and worktree on the machine (the strictest limit among live runs wins); a waiting run says what it is queued behind. **Cache**: a green result is stored under tree SHA + command + git common dir, so identical code is never tested twice (an uncommitted edit gets its own key; a submodule with uncommitted content disables caching for that run; gitignored files, env and toolchain are not in the key, `--no-cache` forces a run). **Priority**: `nice -n 10` and `ionice -c 3` when present. State lives in `$TRAILHEAD_CACHE_DIR`, else `${XDG_CACHE_HOME:-~/.cache}/trailhead`. The cache dir is safe to delete when no test run is active (it only costs a re-run). The launched process group is torn down when the command ends; a command that escapes it (`setsid`, a daemon) is outside that guarantee. **Once per tree**: the full suite runs at most once per tree, at Verify (so once per ticket when nothing changes after Verify), while Execute and Fix run focused tests only; without `testing.full` only the focused-tests rule changes.
|
|
539
|
+
|
|
540
|
+
**Nested or flat.** The list keys above (`isolation.copy`, `preflight.checks`, `uat.setup`, `uat.accounts`) and the `testing.full` / `testing.targeted` / `testing.maxParallel` keys can be written nested (`{"uat": {"setup": [...]}}`) or flat (`"uat.setup": [...]`); if a file has both, the nested form wins with a warning, and a project value replaces the global one as a whole (lists are not merged). `git`, `release` and the isolation *mode* stay in the conventions header; only `isolation.copy` lives in `config.json`.
|
|
485
541
|
|
|
486
542
|
---
|
|
487
543
|
|
|
488
544
|
## 🪝 Hooks
|
|
489
545
|
|
|
490
|
-
Beyond the skill's instructions, trailhead ships
|
|
546
|
+
Beyond the skill's instructions, trailhead ships ten of its own hooks (in `hooks/`, self-contained via `${CLAUDE_PLUGIN_ROOT}`) that *enforce* the parts of the discipline the model shouldn't be trusted to remember:
|
|
491
547
|
|
|
492
548
|
- **Commit guard** (`PreToolUse` on `git commit`): hard-blocks a commit whose subject isn't [Conventional Commits](https://www.conventionalcommits.org), and hard-blocks any `Co-Authored-By` trailer. This mirrors GSD's commit validation.
|
|
493
549
|
- **Secret guard** (`PreToolUse` on `gh` issue/PR writes): trailhead posts a lot to the tracker (ticket bodies, engine comments, codebase/conventions issues, resolutions). This scans what a `gh issue`/`gh pr`/`gh api` write is about to post (the inline `--body`, a heredoc, or a `--body-file`) and **hard-blocks** it if it matches a credential pattern (private keys, GitHub/AWS/OpenAI/Slack/Google/Stripe tokens, JWTs, or a hardcoded `password`/`secret`/`token=…`). The block is a **clean-and-retry cue, not a dead end**: trailhead redacts the flagged value in place (`<REDACTED>` or an env-var reference) and re-posts automatically, so the cleaned content still lands: the secret just never reaches the tracker. It stays a fail-safe *block* rather than a silent auto-rewrite on purpose: a block never leaks, whereas a redaction that quietly failed to apply would. It never scans reads, only outbound writes.
|
|
@@ -495,17 +551,18 @@ Beyond the skill's instructions, trailhead ships nine of its own hooks (in `hook
|
|
|
495
551
|
- **Search guard** (`PreToolUse` on `Bash`): structural backstop for the search-command-hygiene rule (never `cd` then read/search a relative path). It hard-blocks a single Bash command that both changes directory (`cd`/`pushd`/`env -C`) and, in a joined or later statement, reads/searches a relative path (`grep`/`rg`/`ag`/`cat`/`head`/`tail`/`wc`/`nl`/`sed`/`awk`/`less`/`more`, or `git grep`/`log`/`show`/`diff` without a `-C`), so the shape that trips a `Read()` deny rule under bypass permissions (falling back to a manual approval prompt instead of running headless) never runs at all, rather than merely being discouraged in the agent's own prose. Redirection targets (`2>/dev/null`) are not treated as reads, and a `cd` to the session's own directory does not arm it. Claude Code only; not projected to Codex, which has no such `Read()` deny rule.
|
|
496
552
|
- **Body guard** (`PreToolUse` on `gh` issue/PR body writes): hard-blocks a `gh issue edit` / `gh pr edit` / `gh api` write that would set an issue body to empty or whitespace-only, so a read-modify-write whose read returned an empty/degraded body (e.g. a transient HTTP 5xx on `gh issue view --json body`) can never clobber the existing body.
|
|
497
553
|
- **Labels guard** (`PreToolUse` on `gh issue create`): checks the label set of a ticket at the create call. A create carrying any `trailhead:*` label must carry the whole ticket shape: `trailhead:ticket`, exactly one type (`decision`, `research`, `prototype`, `build`, `bug`, `task`) and exactly one destination (one `trailhead:map-<n>` or `trailhead:whiteboard`, never both, never two maps); the block names what is missing or conflicting and asks to retry with every label in the same create call, never a later `--add-label`. Non-ticket issues (`trailhead:map`, `dashboard`, `codebase`, `conventions`, `fog`, `unverified`) are exempt. A create with no `trailhead:*` label at all is blocked too, but only inside a trailhead repo (the working directory sits in a git work tree whose top level has `.trailhead/`) and not when aimed at another repo with `-R`/`--repo`; a deliberate non-trailhead issue opts out by prefixing the command with `TRAILHEAD_PLAIN_ISSUE=1`. A label value the guard cannot resolve statically (`$VAR`, backticks, `$(...)`) is never blocked.
|
|
554
|
+
- **Test guard** (`PreToolUse` on `Bash`): several sessions running the whole test suite at once starve the machine, so when `testing.full` is set (project `.trailhead/config.json` or the global config) a Bash call that runs that bare command is hard-blocked, pointing at focused tests (the files you touched, or `testing.targeted`) or the queue helper `trailhead-test-run.js`. It matches the command statement by statement after dropping env prefixes, redirections and the wrappers `env`, `time`, `command`, `nice` and `timeout`, so `CI=1 npm test`, `timeout 600 npm test` and `npm test 2>&1 | tail -20` are caught, as is a compound `testing.full` (`npm run build && npm test`) run as written; extra flags (`npm test -- --coverage`) still count as the full suite, while any non-flag argument (`npm test -- hooks/x.test.js`) is a focused run and passes. A `cd` before the command (working directory unknown) and the helper's own invocation pass. Off when `testing.full` is unset. A lexical guard for accidental runs: `bash -c` and `eval` are not unwrapped.
|
|
498
555
|
- **Secret-read guard** (`PreToolUse` on `Read|Bash`): denies reads of `.env`/`.env.*`/`.secrets`, whether via the Read tool or a Bash command (a file operand, an option value, or a `<` redirect target), including the `-f`/`--file` script or pattern file of `grep`, `sed`, and `awk`. A command wrapper (`env`, `nice`, `timeout`, `nohup`, `sudo`, `stdbuf`, `time`, `setsid`, `doas`) in front of a nested shell (`bash -c`) or `eval` next to a secret-looking path is blocked too, since the wrapper would hide the inner command. This is a hook `deny` decision rather than a `Read()` permission rule, so secrets stay unreadable without arming Claude Code 2.1.259's `cd-compound-read` approval prompt.
|
|
499
556
|
- **Mockup-link guard** (`PreToolUse` on `AskUserQuestion`): structural backstop for the Prototype rule that a mockup approval ask carries the mockup's link. It hard-blocks an `AskUserQuestion` that asks to approve a mockup (or pick between mockup variants) when neither the reply text right before it nor the approval question itself holds a URL or file path (when the current message is not yet in the transcript, the guard cannot judge and allows), so the agent puts the clickable link in the message before asking again. On Codex, which has no `AskUserQuestion` tool and asks as a plain-text numbered list, its twin `mockup-link-stop` runs on `Stop`: when the final reply asks for that approval without a link, Codex continues the turn so the agent rewrites the ask with it.
|
|
500
557
|
- **Issue injection scanner** (`PostToolUse` on `gh` reads): trailhead reads issue/PR/comment text written by anyone with repo access, i.e. untrusted input. When that text contains prompt-injection phrases, the hook injects an advisory reminding the agent to treat it as data, never as commands. Advisory only: it never blocks.
|
|
501
558
|
|
|
502
|
-
The commit, secret, install, search, body, and
|
|
559
|
+
The commit, secret, install, search, body, labels, and test guards run in **one process**: a single `trailhead-bash-guards.js` registration evaluates them in that order, so a Bash call pays one node start-up instead of seven (three hook processes per Bash call in all: the merged hook, the secret-read guard, and the injection scanner). They remain seven distinct guards with unchanged verdicts; the first block wins, and a guard that errors allows on its own without stopping the others. A guard whose module is missing or broken is skipped too, but never silently: the hook tells the session which guard did not check the command, so a partial install shows up. On Codex the same hook is registered with `--guards=commit,secret,install,body,labels,test`, since there is no search guard there.
|
|
503
560
|
|
|
504
561
|
The secret, body, and secret-read guards also **fail closed on shell constructs they cannot interpret with certainty** (`eval`, `source` / `.`, a bash 5.3 `${ cmd; }`, `printf -v`, a function definition, an alias, a quoted `gh api` body key, a backslash-newline inside a word): when one can hide or precede a write (or sits next to a secret path), the command is blocked with a "rewrite it in a simple form" message instead of being emulated; the same constructs with no write or secret nearby pass.
|
|
505
562
|
|
|
506
563
|
Alongside them trailhead installs a git **`commit-msg` hook** (a POSIX `sh` wrapper, so it also works in `"type": "module"` repos) that enforces the same commit rules on every host, plus the `Refs: #<n>` trailer while a session ticket is active. If it cannot run (script missing, no `node`) it prints `trailhead: commit-msg hook inactive (<reason>); commit rules not enforced.` to stderr and lets the commit through; an installed hook is upgraded at the next ticket start.
|
|
507
564
|
|
|
508
|
-
All
|
|
565
|
+
All ten are crash-safe (any error → allow) and active whenever the plugin is installed. The commit, secret, install, search, body, and secret-read guards therefore apply in **every** repo, not only trailhead projects: intentional, since conventional commits, no `Co-Authored-By`, no leaked secrets, no unvetted installs, no `cd`-then-relative-read, no clobbered issue bodies, and no readable `.env`/`.secrets` are trailhead's standing rules. The test guard is the exception: it only acts where `testing.full` is set. To opt out, disable the plugin's hooks in your Claude Code settings.
|
|
509
566
|
|
|
510
567
|
---
|
|
511
568
|
|
|
@@ -49,7 +49,10 @@ ok('build/bug Verify start with the preflight, before the tests and Code review'
|
|
|
49
49
|
return p >= 0 && p < l.indexOf('**Code review**') && p < l.indexOf('**Goal-backward verification**') && p < l.indexOf('**Acceptance testing**');
|
|
50
50
|
}));
|
|
51
51
|
ok('Verify runs the tests after the sync, before the Code review',
|
|
52
|
-
[buildVerify, bugVerify].every((i) =>
|
|
52
|
+
[buildVerify, bugVerify].every((i) => {
|
|
53
|
+
const run = lines[i].indexOf('runs the full suite once on the synced tree');
|
|
54
|
+
return run > lines[i].indexOf('then sync') && run < lines[i].indexOf('**Code review**');
|
|
55
|
+
}));
|
|
53
56
|
ok('Resolve names git: pr without isolation',
|
|
54
57
|
[buildResolve, bugResolve].every((i) => lines[i].includes('`git: pr` without isolation: on the ticket branch, immediately before the push and PR')));
|
|
55
58
|
ok('Resolve runs it before the Pending-UAT guard',
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Docs test for #361: a bulk create (chart, adopt, split, a cluster) writes a cross-reference
|
|
3
|
+
// to a ticket of the same batch only in the wiring pass, from the ids just created, never guessed.
|
|
4
|
+
// Plain asserts, no framework. Run: node bin/bulk-cross-reference-docs.test.js
|
|
5
|
+
const assert = require('assert');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const skills = path.resolve(__dirname, '..', 'plugins', 'trailhead', 'skills');
|
|
10
|
+
const read = (p) => fs.readFileSync(path.join(skills, p), 'utf8');
|
|
11
|
+
const commands = read('_shared/substrate-commands.md');
|
|
12
|
+
const charting = read('trailhead-chart/references/charting.md');
|
|
13
|
+
const chart = read('trailhead-chart/SKILL.md');
|
|
14
|
+
const teamwork = read('_shared/teamwork.md');
|
|
15
|
+
const RULE = '**Cross-references in a bulk create.**';
|
|
16
|
+
let passed = 0;
|
|
17
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
18
|
+
|
|
19
|
+
const rule = (commands.split('\n').find((l) => l.startsWith(RULE))) || '';
|
|
20
|
+
ok('cookbook: states the rule', rule !== '');
|
|
21
|
+
ok('cookbook: covers every #n and ticket link, not only blockers', /every `#n` and ticket link/.test(rule));
|
|
22
|
+
ok('cookbook: names the bulk paths', ['chart', 'adopt', 'split'].every((w) => rule.includes(w)));
|
|
23
|
+
ok('cookbook: the first pass refers to a sibling by name only', /first pass/.test(rule) && /by name/.test(rule));
|
|
24
|
+
ok('cookbook: written in the wiring pass from the ids just created', /wiring pass/.test(rule) && /ids just created/.test(rule));
|
|
25
|
+
ok('cookbook: says why a guess is harmful', /auto-links? to an unrelated issue/.test(rule));
|
|
26
|
+
ok('cookbook: the body edit obeys the fail-safe rule', /Fail-safe body rewrites/.test(rule));
|
|
27
|
+
|
|
28
|
+
const step4 = charting.split('\n').find((l) => l.startsWith('4. **Create the tickets you can specify now**')) || '';
|
|
29
|
+
ok('charting: step 4 applies the rule', step4.includes('Cross-references in a bulk create') && /never a guessed/.test(step4));
|
|
30
|
+
|
|
31
|
+
const splitStep = teamwork.split('\n').find((l) => l.startsWith('2. **Create the child tickets**')) || '';
|
|
32
|
+
ok('split: the children step applies the rule', splitStep.includes('Cross-references in a bulk create') && /never a guessed/.test(splitStep));
|
|
33
|
+
|
|
34
|
+
ok('ticket verb: a cluster applies the rule', chart.includes('Cross-references in a bulk create'));
|
|
35
|
+
|
|
36
|
+
console.log(`bulk-cross-reference-docs: ${passed} assertions passed`);
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Test for #334: the composite Action actions/capture is a thin wrapper over the capture
|
|
3
|
+
// helper. Checks the input/output mirror, injection safety, the run block against a stub
|
|
4
|
+
// helper, and the docs. Plain asserts, no framework, no YAML dependency.
|
|
5
|
+
// Run: node bin/capture-action.test.js
|
|
6
|
+
const assert = require('assert');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { spawnSync } = require('child_process');
|
|
11
|
+
|
|
12
|
+
const root = path.resolve(__dirname, '..');
|
|
13
|
+
const actionPath = path.join(root, 'actions', 'capture', 'action.yml');
|
|
14
|
+
let passed = 0;
|
|
15
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
16
|
+
const eq = (name, a, b) => { assert.deepStrictEqual(a, b, name); passed++; };
|
|
17
|
+
const EMDASH = String.fromCharCode(0x2014);
|
|
18
|
+
|
|
19
|
+
const yml = fs.readFileSync(actionPath, 'utf8');
|
|
20
|
+
const lines = yml.split('\n');
|
|
21
|
+
|
|
22
|
+
// Top-level keys under a section (2-space indented names) of the minimal YAML.
|
|
23
|
+
function sectionKeys(name) {
|
|
24
|
+
const i = lines.findIndex((l) => l === `${name}:`);
|
|
25
|
+
ok(`action.yml has a ${name}: section`, i >= 0);
|
|
26
|
+
const keys = [];
|
|
27
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
28
|
+
if (lines[j] !== '' && !lines[j].startsWith(' ')) break;
|
|
29
|
+
const m = /^ {2}([A-Za-z0-9_-]+):/.exec(lines[j]);
|
|
30
|
+
if (m) keys.push(m[1]);
|
|
31
|
+
}
|
|
32
|
+
return keys;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Mirror
|
|
36
|
+
eq('inputs mirror the helper flags plus token',
|
|
37
|
+
sectionKeys('inputs').sort(),
|
|
38
|
+
['body', 'body-file', 'dedupe-key', 'map', 'no-dashboard', 'of', 'repo', 'title', 'token', 'trigger', 'type', 'unverified']);
|
|
39
|
+
eq('outputs are issue, url, status', sectionKeys('outputs').sort(), ['issue', 'status', 'url']);
|
|
40
|
+
for (const name of ['issue', 'url', 'status']) {
|
|
41
|
+
ok(`output ${name} reads the capture step`, yml.includes(`value: \${{ steps.capture.outputs.${name} }}`));
|
|
42
|
+
}
|
|
43
|
+
ok('composite action', /^ {2}using: composite$/m.test(yml));
|
|
44
|
+
ok('the step has id: capture', /^ {4}- id: capture$/m.test(yml));
|
|
45
|
+
ok('description notes issues: write', /issues: write/.test(yml.split('\ninputs:')[0]));
|
|
46
|
+
ok('branding present', /^branding:/m.test(yml));
|
|
47
|
+
|
|
48
|
+
// Extract the literal run block (fixed indentation: `run: |` then 8-space lines).
|
|
49
|
+
const runIdx = lines.findIndex((l) => /^ {6}run: \|$/.test(l));
|
|
50
|
+
assert.ok(runIdx >= 0, 'run: | literal block not found in action.yml');
|
|
51
|
+
const runLines = [];
|
|
52
|
+
for (let j = runIdx + 1; j < lines.length; j++) {
|
|
53
|
+
if (lines[j] === '') { runLines.push(''); continue; }
|
|
54
|
+
if (!lines[j].startsWith(' ')) break;
|
|
55
|
+
runLines.push(lines[j].slice(8));
|
|
56
|
+
}
|
|
57
|
+
const script = runLines.join('\n');
|
|
58
|
+
ok('run block is non-empty', script.trim().length > 0);
|
|
59
|
+
|
|
60
|
+
// Injection safety
|
|
61
|
+
ok('no ${{ expression inside the run script', !script.includes('${{'));
|
|
62
|
+
ok('inputs reach the script through env:', /^ {6}env:$/m.test(yml) && /INPUT_BODY: \$\{\{ inputs\.body \}\}/.test(yml));
|
|
63
|
+
|
|
64
|
+
// Runtime against a stub helper
|
|
65
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'capture-action-'));
|
|
66
|
+
const actionDir = path.join(tmp, 'actions', 'capture');
|
|
67
|
+
const tplDir = path.join(tmp, 'plugins', 'trailhead', 'templates');
|
|
68
|
+
fs.mkdirSync(actionDir, { recursive: true });
|
|
69
|
+
fs.mkdirSync(tplDir, { recursive: true });
|
|
70
|
+
fs.writeFileSync(path.join(tmp, 'run.sh'), script);
|
|
71
|
+
fs.writeFileSync(path.join(tplDir, 'trailhead-capture.js'), `
|
|
72
|
+
const fs = require('fs');
|
|
73
|
+
fs.writeFileSync(process.env.STUB_ARGV_FILE, JSON.stringify({ argv: process.argv.slice(2), token: process.env.GH_TOKEN || null }));
|
|
74
|
+
if (process.env.STUB_RAW !== undefined) process.stdout.write(process.env.STUB_RAW);
|
|
75
|
+
else console.log(process.env.STUB_JSON || '{"status":"created","issue":7,"url":"https://github.com/o/r/issues/7"}');
|
|
76
|
+
process.exit(Number(process.env.STUB_EXIT || 0));
|
|
77
|
+
`);
|
|
78
|
+
|
|
79
|
+
const baseInputs = { INPUT_TYPE: 'bug', INPUT_TITLE: 'T', INPUT_REPO: 'o/r', INPUT_TOKEN: 'tok' };
|
|
80
|
+
function run(inputs, stub = {}) {
|
|
81
|
+
const out = path.join(tmp, 'output.txt');
|
|
82
|
+
const argvFile = path.join(tmp, 'argv.json');
|
|
83
|
+
fs.rmSync(out, { force: true });
|
|
84
|
+
fs.rmSync(argvFile, { force: true });
|
|
85
|
+
const env = {
|
|
86
|
+
PATH: process.env.PATH,
|
|
87
|
+
GITHUB_ACTION_PATH: actionDir,
|
|
88
|
+
GITHUB_OUTPUT: out,
|
|
89
|
+
STUB_ARGV_FILE: argvFile,
|
|
90
|
+
GH_TOKEN: inputs.INPUT_TOKEN || '',
|
|
91
|
+
...inputs,
|
|
92
|
+
...stub,
|
|
93
|
+
};
|
|
94
|
+
const r = spawnSync('bash', ['--noprofile', '--norc', '-eo', 'pipefail', path.join(tmp, 'run.sh')], { env, encoding: 'utf8' });
|
|
95
|
+
const outputs = {};
|
|
96
|
+
if (fs.existsSync(out)) {
|
|
97
|
+
for (const l of fs.readFileSync(out, 'utf8').split('\n')) {
|
|
98
|
+
const m = /^([a-z]+)=(.*)$/.exec(l);
|
|
99
|
+
if (m) outputs[m[1]] = m[2];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const seen = fs.existsSync(argvFile) ? JSON.parse(fs.readFileSync(argvFile, 'utf8')) : null;
|
|
103
|
+
return { status: r.status, outputs, seen, stderr: r.stderr };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let r = run(baseInputs);
|
|
107
|
+
eq('minimal argv: empty optionals omitted', r.seen.argv, ['--type', 'bug', '--title', 'T', '--repo', 'o/r']);
|
|
108
|
+
eq('exit 0 on created', r.status, 0);
|
|
109
|
+
eq('outputs from the JSON line', r.outputs, { issue: '7', url: 'https://github.com/o/r/issues/7', status: 'created' });
|
|
110
|
+
eq('GH_TOKEN reaches the helper', r.seen.token, 'tok');
|
|
111
|
+
|
|
112
|
+
r = run({ ...baseInputs, INPUT_BODY: 'a\nb "c" $HOME\n', INPUT_BODY_FILE: 'f.md', INPUT_MAP: '5', INPUT_OF: '9', INPUT_TRIGGER: 'x y',
|
|
113
|
+
INPUT_UNVERIFIED: 'true', INPUT_DEDUPE_KEY: 'k1', INPUT_NO_DASHBOARD: 'true' });
|
|
114
|
+
eq('full argv maps 1:1, multi-line body intact, booleans bare',
|
|
115
|
+
r.seen.argv,
|
|
116
|
+
['--type', 'bug', '--title', 'T', '--body', 'a\nb "c" $HOME\n', '--body-file', 'f.md', '--repo', 'o/r', '--map', '5', '--of', '9',
|
|
117
|
+
'--trigger', 'x y', '--unverified', '--dedupe-key', 'k1', '--no-dashboard']);
|
|
118
|
+
|
|
119
|
+
r = run({ ...baseInputs, INPUT_UNVERIFIED: 'false', INPUT_NO_DASHBOARD: '' });
|
|
120
|
+
ok('false/empty booleans are absent', !r.seen.argv.includes('--unverified') && !r.seen.argv.includes('--no-dashboard'));
|
|
121
|
+
|
|
122
|
+
r = run(baseInputs, { STUB_JSON: '{"status":"existing"}' });
|
|
123
|
+
eq('status only when JSON lacks issue/url', r.outputs, { issue: '', url: '', status: 'existing' });
|
|
124
|
+
eq('existing exits 0', r.status, 0);
|
|
125
|
+
|
|
126
|
+
for (const code of [3, 1, 2]) {
|
|
127
|
+
r = run(baseInputs, { STUB_EXIT: String(code), STUB_JSON: `{"status":"x${code}","issue":4,"url":"u"}` });
|
|
128
|
+
eq(`helper exit ${code} propagates`, r.status, code);
|
|
129
|
+
eq(`status still written on exit ${code}`, r.outputs.status, `x${code}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for (const [label, raw] of [['malformed', 'not json\n'], ['empty', '']]) {
|
|
133
|
+
r = run(baseInputs, { STUB_RAW: raw, STUB_EXIT: '3' });
|
|
134
|
+
eq(`${label} stdout never masks exit 3`, r.status, 3);
|
|
135
|
+
eq(`${label} stdout leaves outputs empty`, { issue: r.outputs.issue || '', url: r.outputs.url || '', status: r.outputs.status || '' }, { issue: '', url: '', status: '' });
|
|
136
|
+
r = run(baseInputs, { STUB_RAW: raw, STUB_EXIT: '0' });
|
|
137
|
+
eq(`${label} stdout with exit 0 exits 1`, r.status, 1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
141
|
+
|
|
142
|
+
// Docs
|
|
143
|
+
const readme = fs.readFileSync(path.join(root, 'README.md'), 'utf8');
|
|
144
|
+
const readmeIt = fs.readFileSync(path.join(root, 'README.it.md'), 'utf8');
|
|
145
|
+
for (const [name, doc] of [['README.md', readme], ['README.it.md', readmeIt]]) {
|
|
146
|
+
ok(`${name} names the Action`, doc.includes('ToRvaLDz/trailhead/actions/capture@'));
|
|
147
|
+
ok(`${name} names issues: write`, doc.includes('issues: write'));
|
|
148
|
+
ok(`${name} names dedupe-key`, doc.includes('dedupe-key'));
|
|
149
|
+
}
|
|
150
|
+
const capDoc = fs.readFileSync(path.join(root, 'plugins', 'trailhead', 'skills', 'trailhead-capture', 'references', 'capture.md'), 'utf8');
|
|
151
|
+
ok('capture.md names the Action', capDoc.includes('actions/capture'));
|
|
152
|
+
for (const [name, text] of [['action.yml', yml], ['README.md', readme], ['README.it.md', readmeIt], ['capture.md', capDoc], ['test', fs.readFileSync(__filename, 'utf8').replace(/String\.fromCharCode\(0x2014\)/g, '')]]) {
|
|
153
|
+
ok(`no em-dash in ${name}`, !text.includes(EMDASH));
|
|
154
|
+
}
|
|
155
|
+
ok('real helper path used by the run block exists',
|
|
156
|
+
fs.existsSync(path.join(root, 'actions/capture', '../../plugins/trailhead/templates/trailhead-capture.js')));
|
|
157
|
+
for (const [name, doc] of [['README.md', readme], ['README.it.md', readmeIt]]) {
|
|
158
|
+
const keys = [...doc.matchAll(/^\s*dedupe-key: (.+)$/gm)].map((m) => m[1]);
|
|
159
|
+
ok(`${name} has a dedupe-key example`, keys.length > 0);
|
|
160
|
+
for (const k of keys) {
|
|
161
|
+
ok(`${name} dedupe-key example avoids unsafe contexts`, !/github\.(ref_name|workflow|head_ref|ref)\b/.test(k));
|
|
162
|
+
// job ids are [A-Za-z0-9_-]; run_id and run_number are digits
|
|
163
|
+
const resolved = k.replace(/\$\{\{\s*github\.(job|run_id|run_number)\s*\}\}/g, 'x1');
|
|
164
|
+
ok(`${name} dedupe-key example matches the helper regex`, /^[A-Za-z0-9._:-]{1,100}$/.test(resolved));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
console.log(`capture-action.test.js: ${passed} checks passed`);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Docs test for #332: capture.md documents the scripted capture helper (CLI, statuses,
|
|
3
|
+
// exit codes, trust, idea as fog, dedupe, dashboard). Plain asserts, no framework.
|
|
4
|
+
// Run: node bin/capture-docs.test.js
|
|
5
|
+
const assert = require('assert');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const root = path.resolve(__dirname, '..');
|
|
10
|
+
const doc = fs.readFileSync(path.join(root, 'plugins', 'trailhead', 'skills', 'trailhead-capture', 'references', 'capture.md'), 'utf8');
|
|
11
|
+
const start = doc.indexOf('## Scripted capture for automations');
|
|
12
|
+
const section = start < 0 ? '' : doc.slice(start);
|
|
13
|
+
let passed = 0;
|
|
14
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
15
|
+
|
|
16
|
+
ok('capture.md has the scripted capture section', section !== '');
|
|
17
|
+
ok('names the helper', section.includes('trailhead-capture.js'));
|
|
18
|
+
for (const flag of ['--type', '--title', '--body-file', '--repo', '--map', '--of', '--trigger', '--unverified', '--dedupe-key', '--no-dashboard']) {
|
|
19
|
+
ok(`names ${flag}`, section.includes(flag));
|
|
20
|
+
}
|
|
21
|
+
for (const status of ['created', 'existing', 'partial', 'error', 'usage']) {
|
|
22
|
+
ok(`documents status ${status}`, section.includes(`"status":"${status}"`));
|
|
23
|
+
}
|
|
24
|
+
ok('documents exit codes 0, 1, 2 and 3', ['exit 0', 'exit 1', 'exit 2', 'exit 3'].every((c) => section.includes(c)));
|
|
25
|
+
ok('partial names prune', /partial[^]*\/trailhead:prune/.test(section));
|
|
26
|
+
ok('trust: allowlist is the repo variable, not the local env', section.includes('TRAILHEAD_LABEL_ALLOWLIST') && /never consults its local environment/.test(section));
|
|
27
|
+
ok('trust: installation tokens are verified post-create', section.includes('github-actions[bot]'));
|
|
28
|
+
ok('idea is parked fog', /`idea` is parked fog/.test(section) && section.includes('`trailhead:fog`'));
|
|
29
|
+
ok('dedupe: marker, cap fallback and the lag residual', section.includes('trailhead-dedupe') && section.includes('1000-result cap') && /search-index lag/.test(section));
|
|
30
|
+
ok('dashboard rule: whiteboard only, update-only', /whiteboard ticket refreshes the dashboard/.test(section) && section.includes('--update-only'));
|
|
31
|
+
ok('no em-dash', !doc.includes(String.fromCharCode(0x2014)));
|
|
32
|
+
console.log(`capture-docs.test.js: ${passed} checks passed`);
|