@marcomigozzi/trailhead 0.10.0 → 0.11.1
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 +2 -2
- package/README.md +2 -2
- package/bin/lib/install-verify.js +257 -0
- package/bin/lib/install-verify.test.js +379 -0
- package/bin/trailhead.js +86 -5
- package/bin/trailhead.test.js +500 -4
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/hooks/lib/commit-message-check.js +58 -2
- package/plugins/trailhead/hooks/lib/commit-message-check.test.js +103 -13
- package/plugins/trailhead/hooks/lib/commit-msg-sync.test.js +375 -0
- package/plugins/trailhead/hooks/lib/fixtures/commit-msg-a319156 +57 -0
- package/plugins/trailhead/hooks/lib/model-defaults-review.js +399 -0
- package/plugins/trailhead/hooks/lib/model-defaults-review.test.js +437 -0
- package/plugins/trailhead/hooks/lib/model-defaults.json +12 -0
- package/plugins/trailhead/hooks/trailhead-check-update.js +69 -14
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +32 -3
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +41 -0
- package/plugins/trailhead/skills/_shared/configuration-reference.md +9 -6
- package/plugins/trailhead/skills/_shared/configuration.md +1 -1
- package/plugins/trailhead/skills/_shared/load-first.md +2 -0
- package/plugins/trailhead/skills/_shared/principles.md +1 -1
- package/plugins/trailhead/skills/_shared/session-handoff.md +4 -4
- package/plugins/trailhead/skills/_shared/substrate-commands.md +1 -1
- package/plugins/trailhead/skills/_shared/teamwork.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/code-review.md +15 -3
- package/plugins/trailhead/skills/_shared/techniques/plan-review.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/prototype.md +5 -1
- package/plugins/trailhead/skills/_shared/techniques/verify.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques.md +1 -1
- package/plugins/trailhead/skills/trailhead/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-capture/references/capture.md +1 -1
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +3 -2
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +6 -6
- package/plugins/trailhead/templates/trailhead-commit-msg +49 -2
- package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +171 -0
|
@@ -30,9 +30,9 @@ 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, routed by `config.design` (disk / claude.ai/design / stitch) and gated by `config.design.approval` (`explicit` waits for a go-ahead, `auto` proceeds after surfacing). **Route by `config.design` and never shortcut a hosted mode to a local disk mockup**: when `config.design` is `claude.ai/design` or `stitch`, the mockup is built there (reusing a cached `design.project`); `disk` is only for `config.design: disk` or the technique's explicit, announced fallback, never a convenience default. Read `config.design` before producing anything. For a redesign or new screen the default is to offer mockups; don't jump straight to code on a UI ticket. **Once the mockup is approved, reproduce it with 100% fidelity** (layout, spacing, sizing, colour, type, copy, and every state), never a loose approximation "in the spirit of" it; if part of the approved mockup is genuinely infeasible, stop and raise it rather than silently deviate (full statement in the **Prototype** technique, `../../_shared/techniques/prototype.md`). **Make fidelity verifiable, not a hope:** before finishing Execute, enumerate the approved mockup's elements and states, confirm each is reproduced, and **record that enumerated fidelity list as a ticket comment** (noting any element raised as infeasible with its resolution), so Verify works from a durable checklist instead of reconstructing intent. At Verify, **Acceptance testing** checks the built screen against the approved mockup (element by element, state by state) and **Goal-backward verification** carries fidelity as an explicit goal claim (see `../../_shared/techniques/acceptance-testing.md` and `../../_shared/techniques/verify.md`). Then implement with **atomic commits** (conventional commits, each with a `Refs: #<n>` trailer for this ticket, see **Principles** in `../../_shared/principles.md`), 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
|
-
4. **Verify**: a `VERIFY` comment: run the tests / the plan's criterion; then the **Code review** technique (its subagent reviews *adversarially*: it doesn't defer to a `CLAUDE.md` convention or a "settled decision" memory), **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), **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 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
|
-
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`). **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.
|
|
33
|
+
3. **Execute**: **if this ticket changes user-facing UI (a new screen, a redesign, a changed layout), run the Prototype technique FIRST, before any real UI code**: surface a mockup to react to (its clickable link always in the same message as the approval question, per the Prototype technique), routed by `config.design` (disk / claude.ai/design / stitch) and gated by `config.design.approval` (`explicit` waits for a go-ahead, `auto` proceeds after surfacing). **Route by `config.design` and never shortcut a hosted mode to a local disk mockup**: when `config.design` is `claude.ai/design` or `stitch`, the mockup is built there (reusing a cached `design.project`); `disk` is only for `config.design: disk` or the technique's explicit, announced fallback, never a convenience default. Read `config.design` before producing anything. For a redesign or new screen the default is to offer mockups; don't jump straight to code on a UI ticket. **Once the mockup is approved, reproduce it with 100% fidelity** (layout, spacing, sizing, colour, type, copy, and every state), never a loose approximation "in the spirit of" it; if part of the approved mockup is genuinely infeasible, stop and raise it rather than silently deviate (full statement in the **Prototype** technique, `../../_shared/techniques/prototype.md`). **Make fidelity verifiable, not a hope:** before finishing Execute, enumerate the approved mockup's elements and states, confirm each is reproduced, and **record that enumerated fidelity list as a ticket comment** (noting any element raised as infeasible with its resolution), so Verify works from a durable checklist instead of reconstructing intent. At Verify, **Acceptance testing** checks the built screen against the approved mockup (element by element, state by state) and **Goal-backward verification** carries fidelity as an explicit goal claim (see `../../_shared/techniques/acceptance-testing.md` and `../../_shared/techniques/verify.md`). Then implement with **atomic commits** (conventional commits, each with a `Refs: #<n>` trailer for this ticket, see **Principles** in `../../_shared/principles.md`; the trailer is also what scopes Verify's review and goal check, per **Ticket diff** in `../../_shared/techniques/code-review.md`), following the repo's `trailhead:conventions` `git:`, straight to `main` by default, or a feature branch + PR when `git: pr`. **Under `isolation: worktree`, do the work in this ticket's `git worktree`** (`trailhead/t<n>` branch), integrating to the trunk at Resolve (see `../../_shared/teamwork.md`). One commit = one verifiable step. Run this via an **executor subagent on `config.models.execute`** (dispatch the **`trailhead-executor`** agent, full tools that can Edit/Write/commit, passing `config.models.execute` as the model override; inline when the model is unset or equals the session model); the commits land on `main`, visible. **Before installing any new dependency, run the verify-before-install gate** (slopsquatting barrier): check the package's legitimacy (name / typosquat similarity to popular packages, age / first-seen date, download counts, repository metadata present and matching) and **stop and ask the user on a suspicious result** (block-and-ask), never installing on your own judgement. On a host with a hook bus the `trailhead-install-guard` `PreToolUse` hook enforces this, blocking a named public-registry install (npm / PyPI / crates.io / Go / RubyGems) until it is vetted; once vetted, proceed by re-running the exact install command prefixed with `TRAILHEAD_VERIFIED_INSTALL=1`. New scope surfacing mid-execute is captured/split, not worked here.
|
|
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
|
+
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
|
|
|
37
37
|
New scope that surfaces mid-build (while planning, executing, or testing) → **capture or split, never expand this ticket in flight** (see **Scope that surfaces while working a ticket**, served by the capture cluster). If the user defers that in-the-moment choice rather than settling it, mark the follow-up **pending** so the **Pending follow-up guard** re-poses it at handoff (`../../_shared/session-handoff.md`), instead of losing it to scrollback.
|
|
38
38
|
|
|
@@ -49,9 +49,9 @@ Then run the cycle, all in ticket comments:
|
|
|
49
49
|
|
|
50
50
|
1. **Repro**: a `REPRO` comment: how to reproduce, expected vs observed behaviour.
|
|
51
51
|
2. **Diagnose**: run the **Debug** technique. A `DIAGNOSIS` comment with the root cause **stated as a falsifiable hypothesis plus the falsification test that confirmed it**. **Pre-fix checkpoint: only a confirmed cause proceeds to Fix** (a cause that has not survived its falsification test stays in diagnosis, looping the hypothesis steps); the full gate lives in the **Debug** technique (`../../_shared/techniques/debug.md`).
|
|
52
|
-
3. **Fix**: implement the correction with **atomic commits** (each with a `Refs: #<n>` trailer for this ticket, see **Principles** in `../../_shared/principles.md`; per the repo's `trailhead:conventions` `git:`, `main` by default, else a branch + PR; in this ticket's `git worktree` under `isolation: worktree`), via an **executor subagent on `config.models.execute`** (dispatch the **`trailhead-fix`** agent, full tools, passing `config.models.execute` as the model override; inline when unset/same as session). Where sensible, a test that fails before and passes after (**TDD**). The **verify-before-install gate** from the `build` Execute step applies here too: vet any new dependency (typosquat similarity, age, downloads, repository metadata) and stop and ask the user on a suspicious result, before installing. **For a user-facing fix that changes UI, the `build` Execute step's Prototype + approved-mockup fidelity rule applies here too:** where the fix changes a screen, surface a mockup, and once approved reproduce it with 100% fidelity with the same enumerated, Verify-checked fidelity list.
|
|
53
|
-
4. **Verify**: a `VERIFY` comment: the repro now passes, no regressions; **Code review** on the fix, **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` -> Resolve, `status: issues_found` -> iterate the loop or run the checkpoint first)**; then the **Goal-backward verification** technique on `config.models.verify` (does the fix deliver what the ticket promised, backward from its `Question`; the read-only `trailhead-verify` subagent), background and collected by polling, branching on its leading `status:` tag (`status: met` -> proceed, `status: gaps_found` -> loop back to Fix for the gap or run the checkpoint, `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); **Acceptance testing** if the bug was user-facing, opening with the drive-mode ask (`../../_shared/techniques/acceptance-testing.md`): AI-driven, or **walk the user through a guided UAT conversationally, step by step**, not a checklist to self-serve: interactive by design, so it runs inline, not backgrounded. 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.
|
|
54
|
-
5. **Resolve**: a resolution comment (cause + fix) → `gh issue close` → update `Decisions so far` (a body read-modify-write: obey the **Fail-safe body rewrites** rule in `../../_shared/substrate.md`). **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. **Run the post-commit self-check before posting, exactly as in the `build` Resolve step** (each cited hash resolves and the files it names appear in that commit's diff; correct or drop any citation that fails). **If the fix materially altered the codebase**, patch the affected facet of the `trailhead:codebase` issue (see **Codebase** in `../../_shared/substrate.md`; also a body read-modify-write, same **Fail-safe body rewrites** rule) in place (a line or two). Then close the session with the **Session handoff** ritual (`../../_shared/session-handoff.md`): `/clear` + next command.
|
|
52
|
+
3. **Fix**: implement the correction with **atomic 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`; per the repo's `trailhead:conventions` `git:`, `main` by default, else a branch + PR; in this ticket's `git worktree` under `isolation: worktree`), via an **executor subagent on `config.models.execute`** (dispatch the **`trailhead-fix`** agent, full tools, passing `config.models.execute` as the model override; inline when unset/same as session). Where sensible, a test that fails before and passes after (**TDD**). The **verify-before-install gate** from the `build` Execute step applies here too: vet any new dependency (typosquat similarity, age, downloads, repository metadata) and stop and ask the user on a suspicious result, before installing. **For a user-facing fix that changes UI, the `build` Execute step's Prototype + approved-mockup fidelity rule applies here too:** where the fix changes a screen, surface a mockup, and once approved reproduce it with 100% fidelity with the same enumerated, Verify-checked fidelity list.
|
|
53
|
+
4. **Verify**: a `VERIFY` comment: the repro now passes, no regressions; **Code review** on the fix, 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), 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` -> Resolve, `status: issues_found` -> iterate the loop or run the checkpoint first)**; then the **Goal-backward verification** technique on `config.models.verify` (does the fix deliver what the ticket promised, backward from its `Question`; the read-only `trailhead-verify` subagent, on the same **Ticket diff** set), background and collected by polling, branching on its leading `status:` tag (`status: met` -> proceed, `status: gaps_found` -> loop back to Fix for the gap or run the checkpoint, `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); **Acceptance testing** if the bug was user-facing, opening with the drive-mode ask (`../../_shared/techniques/acceptance-testing.md`): AI-driven, or **walk the user through a guided UAT conversationally, step by step**, not a checklist to self-serve: interactive by design, so it runs inline, not backgrounded. 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.
|
|
54
|
+
5. **Resolve**: a resolution comment (cause + fix) → `gh issue close` → update `Decisions so far` (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. **Run the post-commit self-check before posting, exactly as in the `build` Resolve step** (each cited hash resolves and the files it names appear in that commit's diff; correct or drop any citation that fails). **If the fix materially altered the codebase**, patch the affected facet of the `trailhead:codebase` issue (see **Codebase** in `../../_shared/substrate.md`; also a body read-modify-write, same **Fail-safe body rewrites** rule) in place (a line or two). Then close the session with the **Session handoff** ritual (`../../_shared/session-handoff.md`): `/clear` + next command.
|
|
55
55
|
|
|
56
56
|
A **blocking** bug that halts other work is worked immediately; an isolated one is a normal frontier ticket. New scope surfacing while fixing → capture or split, never expand this ticket in flight (see **Scope that surfaces while working a ticket**). A follow-up raised but left unopened is carried to the **Pending follow-up guard** at handoff (`../../_shared/session-handoff.md`), re-posed at Resolve rather than dropped.
|
|
57
57
|
|
|
@@ -6,19 +6,26 @@
|
|
|
6
6
|
// hook bus; this git hook is how the commit-guard survives there). Decision #16.
|
|
7
7
|
//
|
|
8
8
|
// Self-contained by design: detached from the trailhead install, it inlines the
|
|
9
|
-
// same
|
|
10
|
-
// lib's test drives this hook end-to-end so the
|
|
9
|
+
// same rules as plugins/trailhead/hooks/lib/commit-message-check.js. That
|
|
10
|
+
// lib's test drives this hook end-to-end so the copies cannot drift.
|
|
11
11
|
//
|
|
12
12
|
// git passes the commit-message file path as argv[2]. On a violation: print the
|
|
13
13
|
// reason to stderr and exit 1 (git aborts the commit). Otherwise exit 0.
|
|
14
14
|
// Crash-safe: any unexpected error exits 0 (never wedge a commit).
|
|
15
|
+
//
|
|
16
|
+
// Note: a merge/revert/fixup message already fails the conventional-subject
|
|
17
|
+
// rule below, so it needs no special case; `git commit --amend` keeps whatever
|
|
18
|
+
// trailer the original message carried; `git commit --no-verify` bypasses this
|
|
19
|
+
// hook entirely, as before.
|
|
15
20
|
|
|
16
21
|
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
17
23
|
|
|
18
24
|
const TYPES = 'feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert';
|
|
19
25
|
const CONVENTIONAL = new RegExp(`^(${TYPES})(\\([^)]+\\))?!?:\\s.+`);
|
|
20
26
|
const CO_AUTHORED = /co-authored-by\s*:/i;
|
|
21
27
|
const SCISSORS = '# ------------------------ >8';
|
|
28
|
+
const REFS_LINE = /^\s*refs\s*:(.*)$/i;
|
|
22
29
|
|
|
23
30
|
function messageRegion(raw) {
|
|
24
31
|
const text = String(raw == null ? '' : raw);
|
|
@@ -33,6 +40,40 @@ function subjectOf(raw) {
|
|
|
33
40
|
return '';
|
|
34
41
|
}
|
|
35
42
|
|
|
43
|
+
// Mirrors parseSessionTicket() in commit-message-check.js: parses the first
|
|
44
|
+
// line of a .trailhead/session-ticket marker (`#<n> <title>`).
|
|
45
|
+
function parseSessionTicket(text) {
|
|
46
|
+
const raw = String(text == null ? '' : text).replace(/^/, '');
|
|
47
|
+
const firstLine = raw.split('\n')[0].replace(/\r$/, '');
|
|
48
|
+
const m = firstLine.match(/^#([1-9]\d*)(?:\s|$)/);
|
|
49
|
+
return m ? Number(m[1]) : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Mirrors hasRefsFor() in commit-message-check.js.
|
|
53
|
+
function hasRefsFor(message, n) {
|
|
54
|
+
const target = new RegExp('#' + n + '(?![\\w])');
|
|
55
|
+
for (const line of messageRegion(message).split('\n')) {
|
|
56
|
+
if (line.trim().startsWith('#')) continue;
|
|
57
|
+
const m = line.match(REFS_LINE);
|
|
58
|
+
if (m && target.test(m[1])) return true;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Reads the session-ticket marker at the worktree top-level. git runs
|
|
64
|
+
// commit-msg from the worktree top-level (linked worktrees included), so
|
|
65
|
+
// process.cwd() is the right root here. Its own try/catch: a read error
|
|
66
|
+
// (marker absent, unreadable, etc.) must never skip the other rules below,
|
|
67
|
+
// it just leaves the Refs rule off.
|
|
68
|
+
function readTicket() {
|
|
69
|
+
try {
|
|
70
|
+
const markerPath = path.join(process.cwd(), '.trailhead', 'session-ticket');
|
|
71
|
+
return parseSessionTicket(fs.readFileSync(markerPath, 'utf8'));
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
36
77
|
try {
|
|
37
78
|
const file = process.argv[2];
|
|
38
79
|
const raw = file ? fs.readFileSync(file, 'utf8') : '';
|
|
@@ -51,6 +92,12 @@ try {
|
|
|
51
92
|
process.stderr.write('trailhead: commit subject must be 72 characters or less.\n');
|
|
52
93
|
process.exit(1);
|
|
53
94
|
}
|
|
95
|
+
|
|
96
|
+
const ticket = readTicket();
|
|
97
|
+
if (Number.isInteger(ticket) && ticket > 0 && !hasRefsFor(raw, ticket)) {
|
|
98
|
+
process.stderr.write(`trailhead: this commit is on ticket #${ticket} (per .trailhead/session-ticket) but has no "Refs: #${ticket}" trailer. Add "Refs: #${ticket}" as the last line of the message and commit again. If this commit is not part of ticket #${ticket}, remove the stale .trailhead/session-ticket marker first.\n`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
54
101
|
} catch {
|
|
55
102
|
process.exit(0); // never wedge a commit on an unexpected error
|
|
56
103
|
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// trailhead commit-msg hook sync (self-contained, ships in templates/ so it
|
|
4
|
+
// reaches every install channel: bin/trailhead.js copies templates/ wholesale
|
|
5
|
+
// for Claude/npm and Codex alike). Installs the sibling template
|
|
6
|
+
// trailhead-commit-msg into the repo's git hooks dir, upgrades it in place
|
|
7
|
+
// when an OLDER trailhead hook is already there (recognised by its own
|
|
8
|
+
// header line, never by trusting the filename), and never touches a foreign
|
|
9
|
+
// (non-trailhead) hook. #185.
|
|
10
|
+
//
|
|
11
|
+
// CLI: node trailhead-commit-msg-sync.js [--repo <dir>] (default cwd)
|
|
12
|
+
// Prints exactly one JSON line: {"status","hook","message"}.
|
|
13
|
+
// status: installed | upgraded | current | foreign | skipped
|
|
14
|
+
// hook: absolute path to the hook file, or null
|
|
15
|
+
// Exit 0 on every normal outcome (including "not a repo" and any unexpected
|
|
16
|
+
// error: this must never block the caller); exit 2 only on a usage error
|
|
17
|
+
// (unknown flag, or --repo with no value).
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const crypto = require('crypto');
|
|
22
|
+
const { execFileSync } = require('child_process');
|
|
23
|
+
|
|
24
|
+
// La prima riga riconosciuta di ogni versione dell'hook trailhead: basta che
|
|
25
|
+
// una delle prime 5 righe (trim) inizi con questa stringa.
|
|
26
|
+
const TRAILHEAD_MARK = '// trailhead commit-msg hook (git).';
|
|
27
|
+
|
|
28
|
+
function isTrailheadHook(text) {
|
|
29
|
+
const lines = String(text == null ? '' : text).split('\n').slice(0, 5);
|
|
30
|
+
return lines.some((line) => line.trim().startsWith(TRAILHEAD_MARK));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Uniforma i fine riga per il confronto di contenuto: CRLF -> LF, non tocca
|
|
34
|
+
// il file su disco (usato solo per decidere current vs upgraded).
|
|
35
|
+
function normalize(text) {
|
|
36
|
+
return String(text == null ? '' : text).replace(/\r\n/g, '\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// stdio 'pipe' su tutti e tre i canali: un git fallito (es. "not a repo") non
|
|
40
|
+
// deve stampare rumore sul terminale dell'invocante, solo far fallire questa
|
|
41
|
+
// chiamata (il chiamante decide come reagire).
|
|
42
|
+
function defaultRunGit(args, cwd) {
|
|
43
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Risolve la hooksDir effettiva del repo (rispetta core.hooksPath e i
|
|
47
|
+
// worktree collegati: `git rev-parse --git-path hooks` la restituisce già
|
|
48
|
+
// corretta, eventualmente relativa a cwd). { skip: 'no-repo' } se repoDir non
|
|
49
|
+
// è un repo git.
|
|
50
|
+
function resolveHooksTarget({ repoDir, runGit = defaultRunGit }) {
|
|
51
|
+
try {
|
|
52
|
+
runGit(['rev-parse', '--git-dir'], repoDir);
|
|
53
|
+
} catch {
|
|
54
|
+
return { skip: 'no-repo' };
|
|
55
|
+
}
|
|
56
|
+
const hooksPathOut = runGit(['rev-parse', '--git-path', 'hooks'], repoDir);
|
|
57
|
+
const hooksDir = path.resolve(repoDir, hooksPathOut);
|
|
58
|
+
let hooksPathSet = false;
|
|
59
|
+
try {
|
|
60
|
+
const value = runGit(['config', '--get', 'core.hooksPath'], repoDir);
|
|
61
|
+
hooksPathSet = Boolean(value && value.trim());
|
|
62
|
+
} catch {
|
|
63
|
+
hooksPathSet = false;
|
|
64
|
+
}
|
|
65
|
+
return { hooksDir, hooksPathSet };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Applica la sincronizzazione vera e propria dentro hooksDir. fs è iniettato
|
|
69
|
+
// per testabilità (default: il modulo fs reale).
|
|
70
|
+
function syncCommitMsgHook({ hooksDir, templateText, hooksPathSet, fs: fsMod = fs }) {
|
|
71
|
+
const hookPath = path.join(hooksDir, 'commit-msg');
|
|
72
|
+
|
|
73
|
+
if (!fsMod.existsSync(hookPath)) {
|
|
74
|
+
if (hooksPathSet) {
|
|
75
|
+
return {
|
|
76
|
+
status: 'skipped',
|
|
77
|
+
hook: null,
|
|
78
|
+
message: 'core.hooksPath is set and has no commit-msg; trailhead does not add files there, copy the template yourself.',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
fsMod.mkdirSync(hooksDir, { recursive: true });
|
|
82
|
+
fsMod.writeFileSync(hookPath, templateText);
|
|
83
|
+
fsMod.chmodSync(hookPath, 0o755);
|
|
84
|
+
return { status: 'installed', hook: hookPath, message: 'installed the trailhead commit-msg hook.' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const stat = fsMod.lstatSync(hookPath);
|
|
88
|
+
if (stat.isSymbolicLink()) {
|
|
89
|
+
return { status: 'foreign', hook: hookPath, message: 'a foreign commit-msg hook (symlink) is present; left untouched.' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const existingText = fsMod.readFileSync(hookPath, 'utf8');
|
|
93
|
+
if (!isTrailheadHook(existingText)) {
|
|
94
|
+
return { status: 'foreign', hook: hookPath, message: 'a foreign commit-msg hook is present; left untouched.' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (normalize(existingText) === normalize(templateText)) {
|
|
98
|
+
if ((stat.mode & 0o777) !== 0o755) fsMod.chmodSync(hookPath, 0o755);
|
|
99
|
+
return { status: 'current', hook: hookPath, message: 'the trailhead commit-msg hook is already current.' };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Upgrade atomico: scrive su un temp file unico nella stessa dir, poi rename
|
|
103
|
+
// sopra l'hook esistente (mai un truncate in place).
|
|
104
|
+
const tmpPath = path.join(hooksDir, `.commit-msg.trailhead-${process.pid}-${crypto.randomBytes(4).toString('hex')}`);
|
|
105
|
+
try {
|
|
106
|
+
fsMod.writeFileSync(tmpPath, templateText);
|
|
107
|
+
fsMod.chmodSync(tmpPath, 0o755);
|
|
108
|
+
fsMod.renameSync(tmpPath, hookPath);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
try { fsMod.unlinkSync(tmpPath); } catch { /* best effort cleanup */ }
|
|
111
|
+
throw err;
|
|
112
|
+
}
|
|
113
|
+
return { status: 'upgraded', hook: hookPath, message: 'upgraded the trailhead commit-msg hook to the current template.' };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseArgs(argv) {
|
|
117
|
+
let repo = null;
|
|
118
|
+
for (let i = 0; i < argv.length; i++) {
|
|
119
|
+
const arg = argv[i];
|
|
120
|
+
if (arg === '--repo') {
|
|
121
|
+
i++;
|
|
122
|
+
if (i >= argv.length) return { error: 'usage: --repo requires a value' };
|
|
123
|
+
repo = argv[i];
|
|
124
|
+
} else {
|
|
125
|
+
return { error: `usage: unknown argument: ${arg}` };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { repo };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function writeResult(stdout, result) {
|
|
132
|
+
stdout.write(JSON.stringify(result) + '\n');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function main(argv, { cwd, stdout } = {}) {
|
|
136
|
+
const cwd_ = cwd || process.cwd();
|
|
137
|
+
const out = stdout || process.stdout;
|
|
138
|
+
|
|
139
|
+
const parsed = parseArgs(argv || []);
|
|
140
|
+
if (parsed.error) {
|
|
141
|
+
process.stderr.write(parsed.error + '\n');
|
|
142
|
+
return 2;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const repoDir = parsed.repo ? path.resolve(cwd_, parsed.repo) : cwd_;
|
|
146
|
+
let hooksDirForError = null;
|
|
147
|
+
try {
|
|
148
|
+
const target = resolveHooksTarget({ repoDir });
|
|
149
|
+
if (target.skip) {
|
|
150
|
+
writeResult(out, { status: 'skipped', hook: null, message: 'not a git repository; nothing to sync.' });
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
hooksDirForError = target.hooksDir;
|
|
154
|
+
const templatePath = path.join(__dirname, 'trailhead-commit-msg');
|
|
155
|
+
const templateText = fs.readFileSync(templatePath, 'utf8');
|
|
156
|
+
const result = syncCommitMsgHook({ hooksDir: target.hooksDir, templateText, hooksPathSet: target.hooksPathSet, fs });
|
|
157
|
+
writeResult(out, result);
|
|
158
|
+
return 0;
|
|
159
|
+
} catch (err) {
|
|
160
|
+
const hookGuess = hooksDirForError ? path.join(hooksDirForError, 'commit-msg') : null;
|
|
161
|
+
writeResult(out, { status: 'skipped', hook: hookGuess, message: 'error: ' + (err && err.message ? err.message : String(err)) });
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = { resolveHooksTarget, isTrailheadHook, normalize, syncCommitMsgHook, parseArgs, main };
|
|
167
|
+
|
|
168
|
+
if (require.main === module) {
|
|
169
|
+
const code = main(process.argv.slice(2), { cwd: process.cwd(), stdout: process.stdout });
|
|
170
|
+
process.exit(code);
|
|
171
|
+
}
|