@marcomigozzi/trailhead 0.17.0 → 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 +28 -18
- package/README.md +28 -18
- package/bin/base-preflight-docs.test.js +4 -1
- package/bin/bulk-cross-reference-docs.test.js +36 -0
- package/bin/engine-route-docs.test.js +7 -0
- package/bin/facet-stamp-docs.test.js +79 -0
- package/bin/forced-mutation-docs.test.js +38 -0
- 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 +8 -8
- package/bin/lib/codex-projection.test.js +19 -3
- 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/seam-coverage-docs.test.js +46 -0
- package/bin/standards-brief-docs.test.js +112 -0
- package/bin/test-once-docs.test.js +101 -0
- package/bin/test-run-docs.test.js +47 -0
- package/bin/trailhead.js +9 -4
- package/bin/trailhead.test.js +18 -6
- 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-grill.md +2 -2
- 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/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/issue-body.test.js +81 -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/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 +30 -14
- 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 +144 -45
- package/plugins/trailhead/hooks/mod/repo-pin.ts +105 -4
- 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 +3 -1
- 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 +5 -0
- package/plugins/trailhead/skills/_shared/configuration-reference.md +12 -0
- package/plugins/trailhead/skills/_shared/load-first.md +2 -0
- package/plugins/trailhead/skills/_shared/substrate-commands.md +11 -2
- package/plugins/trailhead/skills/_shared/substrate.md +3 -2
- package/plugins/trailhead/skills/_shared/teamwork.md +5 -3
- package/plugins/trailhead/skills/_shared/techniques/code-review-pipeline.md +24 -0
- package/plugins/trailhead/skills/_shared/techniques/code-review.md +18 -5
- package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +2 -2
- package/plugins/trailhead/skills/_shared/techniques/debug.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/grilling.md +3 -3
- package/plugins/trailhead/skills/_shared/techniques/plan-review.md +16 -4
- package/plugins/trailhead/skills/_shared/techniques/tdd.md +4 -2
- package/plugins/trailhead/skills/_shared/techniques/verify.md +2 -0
- package/plugins/trailhead/skills/_shared/techniques.md +3 -3
- package/plugins/trailhead/skills/trailhead-chart/SKILL.md +2 -2
- package/plugins/trailhead/skills/trailhead-chart/references/charting.md +1 -1
- package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +8 -3
- package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +17 -2
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +1 -0
- package/plugins/trailhead/skills/trailhead-work/references/auto.md +2 -0
- package/plugins/trailhead/skills/trailhead-work/references/executor-brief.md +2 -0
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +7 -7
- 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 +2 -1
- package/plugins/trailhead/templates/trailhead-checkout-find.js +3 -2
- package/plugins/trailhead/templates/trailhead-checkout-in.js +2 -1
- package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +2 -1
- 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 +92 -6
- 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-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 +268 -17
- 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/repo-pin-late-repo.test.ts +3 -2
- package/plugins/trailhead/tests/repo-pin-relocate.test.ts +175 -0
- 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
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Tests for trailhead-test-guard.js. Run: node trailhead-test-guard.test.js
|
|
3
|
+
// No framework: plain asserts + child_process for the end-to-end hook behaviour.
|
|
4
|
+
const assert = require('assert');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
|
|
10
|
+
// TRAILHEAD_GUARD_HOOK re-runs this suite through the merged trailhead-bash-guards.js.
|
|
11
|
+
const HOOK = process.env.TRAILHEAD_GUARD_HOOK || path.join(__dirname, 'trailhead-test-guard.js');
|
|
12
|
+
const { evaluate } = require('./trailhead-test-guard.js');
|
|
13
|
+
|
|
14
|
+
let passed = 0;
|
|
15
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
16
|
+
|
|
17
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'test-guard-'));
|
|
18
|
+
const GLOBAL = path.join(tmp, 'global'); // CLAUDE_CONFIG_DIR
|
|
19
|
+
fs.mkdirSync(path.join(GLOBAL, 'trailhead'), { recursive: true });
|
|
20
|
+
const ENV = { CLAUDE_CONFIG_DIR: GLOBAL };
|
|
21
|
+
const setGlobal = (cfg) => {
|
|
22
|
+
const f = path.join(GLOBAL, 'trailhead', 'config.json');
|
|
23
|
+
if (cfg === null) fs.rmSync(f, { force: true });
|
|
24
|
+
else fs.writeFileSync(f, typeof cfg === 'string' ? cfg : JSON.stringify(cfg));
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
let n = 0;
|
|
28
|
+
// A temp repo (.git dir) whose project config is `cfg` (object, raw string, or null for none).
|
|
29
|
+
function repo(cfg) {
|
|
30
|
+
const dir = path.join(tmp, `repo${n++}`);
|
|
31
|
+
fs.mkdirSync(path.join(dir, '.git'), { recursive: true });
|
|
32
|
+
fs.mkdirSync(path.join(dir, 'sub', 'deep'), { recursive: true });
|
|
33
|
+
if (cfg !== null) {
|
|
34
|
+
fs.mkdirSync(path.join(dir, '.trailhead'));
|
|
35
|
+
fs.writeFileSync(path.join(dir, '.trailhead', 'config.json'), typeof cfg === 'string' ? cfg : JSON.stringify(cfg));
|
|
36
|
+
}
|
|
37
|
+
return dir;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const blocked = (cmd, cwd) => { const v = evaluate(cmd, { cwd, env: ENV }); return !!v && v.kind === 'block'; };
|
|
41
|
+
const NPM = repo({ testing: { full: 'npm test' } });
|
|
42
|
+
|
|
43
|
+
function runHook(command, cwd) {
|
|
44
|
+
const input = JSON.stringify({ tool_input: { command }, cwd });
|
|
45
|
+
const r = spawnSync('node', [HOOK], { input, cwd: tmp, env: { ...process.env, ...ENV } });
|
|
46
|
+
return { code: r.status, out: r.stdout.toString() };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
setGlobal(null);
|
|
50
|
+
|
|
51
|
+
// --- blocks ---
|
|
52
|
+
for (const c of [
|
|
53
|
+
'npm test',
|
|
54
|
+
'npm test 2>&1 | tail -20',
|
|
55
|
+
'npm test > out.log 2>&1',
|
|
56
|
+
'CI=1 npm test',
|
|
57
|
+
'timeout 600 npm test',
|
|
58
|
+
'timeout -s KILL -k 5 600 npm test',
|
|
59
|
+
'timeout --signal=KILL 10m npm test',
|
|
60
|
+
'env CI=1 FOO=bar npm test',
|
|
61
|
+
'env -i npm test',
|
|
62
|
+
'time npm test',
|
|
63
|
+
'command npm test',
|
|
64
|
+
'nice npm test',
|
|
65
|
+
'nice -n 10 npm test',
|
|
66
|
+
'nice -n10 npm test',
|
|
67
|
+
'nice -5 npm test',
|
|
68
|
+
'nice -n 10 timeout 600 env CI=1 npm test',
|
|
69
|
+
'npm test -- --coverage',
|
|
70
|
+
'npm test --silent',
|
|
71
|
+
'echo hi && npm test',
|
|
72
|
+
'npm test; echo done',
|
|
73
|
+
'npm "test"',
|
|
74
|
+
]) ok(`blocks: ${c}`, blocked(c, NPM));
|
|
75
|
+
|
|
76
|
+
// --- passes ---
|
|
77
|
+
for (const c of [
|
|
78
|
+
'npm test -- hooks/x.test.js',
|
|
79
|
+
'npm test hooks/x.test.js',
|
|
80
|
+
'npm run lint',
|
|
81
|
+
'npm run test',
|
|
82
|
+
'npm install',
|
|
83
|
+
'node /x/trailhead-test-run.js run --repo .',
|
|
84
|
+
'node /x/trailhead-test-run.js run --repo . && npm test -- a.test.js',
|
|
85
|
+
'cd sub && npm test',
|
|
86
|
+
'pushd sub; npm test',
|
|
87
|
+
'echo npm test',
|
|
88
|
+
'git commit -m "npm test"',
|
|
89
|
+
'',
|
|
90
|
+
]) ok(`passes: ${c}`, !blocked(c, NPM));
|
|
91
|
+
|
|
92
|
+
// --- compound testing.full ---
|
|
93
|
+
const COMPOUND = repo({ testing: { full: 'npm run build && npm test' } });
|
|
94
|
+
ok('compound blocks as written', blocked('npm run build && npm test', COMPOUND));
|
|
95
|
+
ok('compound blocks across ; and |', blocked('npm run build ; npm test', COMPOUND) && blocked('npm run build | npm test', COMPOUND));
|
|
96
|
+
ok('compound blocks with wrappers and flags', blocked('CI=1 npm run build && timeout 9 npm test --silent', COMPOUND));
|
|
97
|
+
ok('compound: only the last statement alone passes', !blocked('npm test', COMPOUND));
|
|
98
|
+
ok('compound: only the first statement alone passes', !blocked('npm run build', COMPOUND));
|
|
99
|
+
ok('compound: extra arg on a non-last statement passes', !blocked('npm run build --prod && npm test', COMPOUND));
|
|
100
|
+
ok('compound: focused last statement passes', !blocked('npm run build && npm test -- a.test.js', COMPOUND));
|
|
101
|
+
ok('compound: a cd in between passes', !blocked('cd x && npm run build && npm test', COMPOUND));
|
|
102
|
+
|
|
103
|
+
// --- node --test style ---
|
|
104
|
+
const NODE = repo({ testing: { full: 'node --test' } });
|
|
105
|
+
ok('node --test blocks bare', blocked('node --test', NODE));
|
|
106
|
+
ok('node --test with a file passes', !blocked('node --test a.test.js', NODE));
|
|
107
|
+
ok('node --test with --test-reporter flag blocks', blocked('node --test --test-reporter=spec', NODE));
|
|
108
|
+
|
|
109
|
+
// --- off ---
|
|
110
|
+
ok('off: no config at all', !blocked('npm test', repo(null)));
|
|
111
|
+
ok('off: config without the key', !blocked('npm test', repo({ testing: { targeted: 'x {files}' } })));
|
|
112
|
+
ok('off: invalid JSON', !blocked('npm test', repo('{nope')));
|
|
113
|
+
ok('off: non-string value', !blocked('npm test', repo({ testing: { full: 5 } })));
|
|
114
|
+
ok('off: empty value', !blocked('npm test', repo({ testing: { full: ' ' } })));
|
|
115
|
+
ok('off: testing not an object', !blocked('npm test', repo({ testing: 'npm test' })));
|
|
116
|
+
ok('off: no git repo and no global', !blocked('npm test', tmp));
|
|
117
|
+
|
|
118
|
+
// --- config layers ---
|
|
119
|
+
ok('flat key blocks', blocked('npm test', repo({ 'testing.full': 'npm test' })));
|
|
120
|
+
{
|
|
121
|
+
const both = repo({ testing: { full: 'npm test' }, 'testing.full': 'yarn test' });
|
|
122
|
+
ok('nested wins over flat', blocked('npm test', both) && !blocked('yarn test', both));
|
|
123
|
+
}
|
|
124
|
+
{
|
|
125
|
+
setGlobal({ testing: { full: 'make check' } });
|
|
126
|
+
ok('global-only testing.full blocks', blocked('make check', repo(null)));
|
|
127
|
+
ok('global applies when the project lacks the key', blocked('make check', repo({ testing: { targeted: 'x {files}' } })));
|
|
128
|
+
ok('project replaces global', !blocked('make check', NPM) && blocked('npm test', NPM));
|
|
129
|
+
ok('invalid project value replaces global (off)', !blocked('make check', repo({ testing: { full: 3 } })));
|
|
130
|
+
ok('invalid project JSON falls back to global', blocked('make check', repo('{nope')));
|
|
131
|
+
setGlobal('{nope');
|
|
132
|
+
ok('invalid global JSON is off', !blocked('npm test', repo(null)));
|
|
133
|
+
setGlobal({ 'testing.full': 'make check' });
|
|
134
|
+
ok('flat global blocks', blocked('make check', repo(null)));
|
|
135
|
+
setGlobal(null);
|
|
136
|
+
}
|
|
137
|
+
ok('project config found from a nested cwd', blocked('npm test', path.join(NPM, 'sub', 'deep')));
|
|
138
|
+
ok('worktree-style .git file is a top level', (() => {
|
|
139
|
+
const wt = path.join(tmp, 'wt');
|
|
140
|
+
fs.mkdirSync(path.join(wt, '.trailhead'), { recursive: true });
|
|
141
|
+
fs.writeFileSync(path.join(wt, '.git'), 'gitdir: /elsewhere\n');
|
|
142
|
+
fs.writeFileSync(path.join(wt, '.trailhead', 'config.json'), JSON.stringify({ testing: { full: 'npm test' } }));
|
|
143
|
+
return blocked('npm test', wt);
|
|
144
|
+
})());
|
|
145
|
+
|
|
146
|
+
// --- verdict shape ---
|
|
147
|
+
{
|
|
148
|
+
const v = evaluate('npm test', { cwd: NPM, env: ENV });
|
|
149
|
+
ok('block payload shape', v.kind === 'block' && v.payload.decision === 'block' && v.payload.code === 'UNQUEUED_FULL_TEST_RUN');
|
|
150
|
+
ok('reason names the command, focused tests and the helper',
|
|
151
|
+
v.payload.reason.includes('`npm test`') && /focused tests/.test(v.payload.reason) && /trailhead-test-run\.js run --repo /.test(v.payload.reason));
|
|
152
|
+
ok('reason names the repo root', v.payload.reason.includes(NPM));
|
|
153
|
+
ok('allow verdict is null', evaluate('ls', { cwd: NPM, env: ENV }) === null);
|
|
154
|
+
ok('never throws on garbage', evaluate(undefined, { cwd: NPM, env: ENV }) === null && evaluate('npm test', undefined) === null);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// --- end to end ---
|
|
158
|
+
{
|
|
159
|
+
const h = runHook('npm test 2>&1 | tail -20', NPM);
|
|
160
|
+
const v = evaluate('npm test 2>&1 | tail -20', { cwd: NPM, env: ENV });
|
|
161
|
+
ok('hook exits 2', h.code === 2);
|
|
162
|
+
ok('hook payload is the verdict payload', h.out === JSON.stringify(v.payload));
|
|
163
|
+
const p = runHook('npm test -- a.test.js', NPM);
|
|
164
|
+
ok('hook allows a focused run silently', p.code === 0 && p.out === '');
|
|
165
|
+
const off = runHook('npm test', repo(null));
|
|
166
|
+
ok('hook allows when off', off.code === 0 && off.out === '');
|
|
167
|
+
const bad = spawnSync('node', [HOOK], { input: 'not json', cwd: tmp, env: { ...process.env, ...ENV } });
|
|
168
|
+
ok('unparseable input allows', bad.status === 0);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
172
|
+
console.log(`trailhead-test-guard: ${passed} checks passed`);
|
|
@@ -40,6 +40,11 @@ The project file wins over the global one, key by key; either may spell the key
|
|
|
40
40
|
<!-- core: configuration.md | Three layers, **nearest wins** | 4bfa9380 -->
|
|
41
41
|
<!-- core: multi-map.md | **Active map.** | 702fafcc -->
|
|
42
42
|
|
|
43
|
+
## Repo target check
|
|
44
|
+
|
|
45
|
+
**Before the first tracker write of a session** (any `gh issue create/edit/comment/close` or `gh api` write below), check which repository `gh` will write to: in a fork clone it can resolve the parent (an `upstream` remote, or `gh repo set-default`). Run `node <templates>/trailhead-repo-target.js check --repo <project root>` (`<templates>` as in "Map body writes"; skip silently if no directory has the script). It prints one JSON line: `ok`, `reuse` and `skipped` proceed silently; `ask` stops the write and asks the user which repository (one option per `candidates` entry, naming the fork and the parent, or noting no write access; never pick for them), then `node <templates>/trailhead-repo-target.js record --repo <project root> --target <chosen owner/name>` makes it `gh`'s default and remembers the answer for 8 hours (`record` adds `.trailhead/repo-target.json` to the repo's `.gitignore` if absent); a failed `record` (`status: error`) stops the write with a one-line report. Read-only `gh` calls may run before the check. A capture through `trailhead-capture.js` passes the resolved or chosen target as `--repo`.
|
|
46
|
+
<!-- core: load-first.md | **Repo target check, once per session, before the first tracker write.** | 190926a2 -->
|
|
47
|
+
|
|
43
48
|
## Labels and create commands
|
|
44
49
|
|
|
45
50
|
A ticket carries `trailhead:ticket`, exactly one type label and exactly one destination (its map's `trailhead:map-<n>` or `trailhead:whiteboard`), all in the create call itself, never a later `--add-label`. A known-blocked ticket, a seed included (`trailhead:seed` and `trailhead:blocked`), carries `trailhead:blocked` in the create call too.
|
|
@@ -14,8 +14,13 @@ Effective config = built-in defaults ← global `~/.claude/trailhead/config.json
|
|
|
14
14
|
| `tdd` | **`seams`** \| `on` \| `off` | how the `build` engine tests |
|
|
15
15
|
| `acceptance.browser` | **`auto`** \| `on` \| `off` | how **Acceptance testing** runs |
|
|
16
16
|
| `testing.webapp` / `testing.url` | bool / url | is it browser-drivable, and where |
|
|
17
|
+
| `testing.full` | shell command (**unset**) | the full test suite; when set, the engine runs it through `trailhead-test-run.js` (queue + result cache) |
|
|
18
|
+
| `testing.targeted` | shell command with `{files}` (**unset**) | a focused run; `{files}` becomes the single-quoted files |
|
|
19
|
+
| `testing.maxParallel` | positive integer (**`1`**) | how many heavy test runs may run at once across the whole machine |
|
|
17
20
|
| `plan_review` | **`off`** \| `on` \| CLI list (`gemini,codex`) | send `build` PLANs to external AI CLIs for review (**Cross-AI plan review**) |
|
|
21
|
+
| `plan_review.context` | object (`{"codex": 272000}`) | per-CLI context window in tokens for the plan review size pre-check; a valid entry is a positive integer up to 10000000, merged per reviewer across the project and global files |
|
|
18
22
|
| `plan_review.rounds` | integer (**`2`**) | max converge-and-re-review rounds; an autonomous run asks at start and its answer overrides the key for that run |
|
|
23
|
+
| `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); project over global, flat or nested `inbox: {needs_info_days}`; an invalid value warns and falls to the next layer |
|
|
19
24
|
| `code_review.rounds` | integer (**`2`**) | max fix-and-re-review rounds in **Code review** before the boundary HITL checkpoint |
|
|
20
25
|
| `effort` | **`standard`** \| `lean` | how hard the `build`/`bug` engine works a change: `standard` runs every step; `lean` size-triages and offers to skip heavy steps (plan pass, TDD, cross-AI review, full code review, goal-backward verify) on small changes. **Governs `build`/`bug` only; `quick` always size-triages regardless of this key** (see effort below) |
|
|
21
26
|
| `isolation.copy` | array of repo-relative paths (**unset**) | gitignored files and directories copied from the main checkout into a new `worktree`/`clone` workspace, see **Isolation copy** |
|
|
@@ -71,6 +76,8 @@ If a value is unset, it resolves to its **Balanced**-profile default for that ac
|
|
|
71
76
|
|
|
72
77
|
**Ticket language.** `ticket.language` (ISO 639-1, default `en`) sets the language trailhead **writes** in on the Issues: ticket titles and bodies, engine comments (DISCUSS/PLAN/VERIFY/OPTIONS/REPRO/DIAGNOSIS/PAUSED/resolution), the map body sections, and commit message **descriptions**. It's fully decoupled from the language the agent *converses* in (its session/global setting); setting `ticket.language` never changes how the agent talks to the user. **Untouched by it:** code and identifiers, the fixed `trailhead:*` label names, config keys/values, and the conventional-commit type prefix (`feat:`/`fix:` stay English). The skill's own source docs (SKILL.md, `references/`) are not project prose and stay English.
|
|
73
78
|
|
|
79
|
+
**Test runs.** `testing.full` / `testing.targeted` / `testing.maxParallel` (nested or flat, project replaces global key by key) feed `trailhead-test-run.js`, which every engine test run goes through when `testing.full` is set. **Queue**: at most `testing.maxParallel` heavy runs at once across every session and worktree on the machine (a holder file per run in the cache dir; the strictest live limit wins; a waiting run says what it is queued behind). **Cache**: a green result is stored under tree SHA + command + git common dir, so an identical tree 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**: the command runs under `nice -n 10` and `ionice -c 3` when present. **Cache dir**: `$TRAILHEAD_CACHE_DIR`, else `${XDG_CACHE_HOME:-~/.cache}/trailhead` (`results/`, `logs/`, `queue/`). The cache dir is safe to delete when no test run is active (it only costs a re-run). Process-group teardown covers the launched group only; a command that escapes it (`setsid`, a daemon) is outside the guarantee. A pid reused after a crash makes the queue wait until that pid exits; remove its holder in `queue/` (or a stale `queue/.gate`) by hand. **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. **Guard**: on a host with a hook bus, a Bash call that runs the bare `testing.full` command is blocked by the test guard, which points at focused tests or the helper (a focused run, a `cd` before it, and the helper's own call pass).
|
|
80
|
+
|
|
74
81
|
**tdd.** `seams` (default) = TDD at critical seams only; `on` = TDD for all behaviour with a definable input→output; `off` = tests after, or none for throwaway. The `build` engine's Plan/Execute honour this.
|
|
75
82
|
|
|
76
83
|
**acceptance.browser.** The drive-mode ask (AI-driven, human-driven, or skip, see the Acceptance testing technique) **always comes first, before any step runs**: no value of this key pre-answers it, and none implies a waiver. This key only governs whether and how an AI-driven step uses the browser once that ask is answered AI-driven: `auto` (default) = browser-drive when `testing.webapp` + `testing.url` are set, or (when `testing.webapp` is set but `testing.url` is unset) first try to auto-detect the target URL (running dev server, package scripts, dev-server/framework config, project config) and propose it, driving against the accepted URL; when no URL can be determined, fall back per the Acceptance testing technique (ask the human for the URL, or the guided UAT for that step); `on` = always attempt browser-driving, auto-detecting the URL the same way when `testing.url` is unset; `off` = never drive a browser. **`off` disables *browser-driving*, not acceptance, and not AI-driven checking**: AI-driven still runs its non-browser checks via commands / the test suite for whatever steps allow it; only a step that genuinely needs the human (or the whole UAT, under human-driven) gets the guided UAT walkthrough. The Verify step honours this.
|
|
@@ -81,6 +88,10 @@ If a value is unset, it resolves to its **Balanced**-profile default for that ac
|
|
|
81
88
|
|
|
82
89
|
**plan_review.** `off` (default) = build PLANs are not externally reviewed; `on` = review with every external AI CLI detected on the PATH; a comma list (`gemini,codex`) = only those. Needs the named CLIs installed; if none are available it's skipped, never failed. The `build` engine's Plan step runs **Cross-AI plan review** when this is on.
|
|
83
90
|
|
|
91
|
+
**plan_review.context.** Object of CLI name to context window in tokens, default empty. Before each reviewer call the plan review estimates the brief as `ceil(bytes / 3)` tokens and skips that reviewer when the estimate exceeds 65% of its window (the rest is headroom for the reviewer's own system prompt and output); a brief over 100 KiB under the cap is passed through a file instead of one argument. Built-in windows: claude 200000, gemini 1000000, codex 272000, qwen 256000, kimi 256000, agy 1000000, opencode 128000, cursor-agent 128000, coderabbit 128000; any other CLI 128000. The estimate is not a guarantee: a reviewer that still fails with a context-overflow error is not retried and is recorded as skipped. An entry here overrides the default for that CLI; the project and global files merge per reviewer (project wins for a CLI both name); an invalid entry warns and falls through to the next layer.
|
|
92
|
+
|
|
93
|
+
**inbox.needs_info_days.** Positive integer, default 14. Read by `trailhead-inbox-needs-info.js` (project `.trailhead/config.json` over the global `${CLAUDE_CONFIG_DIR:-~/.claude}/trailhead/config.json`, resolved key by key). An inbox issue parked with `trailhead:needs-info` that has had no reporter answer for this many whole days is only *offered* for closing; the maintainer confirms and nothing closes on its own. An invalid value, an unreadable file or invalid JSON warns and falls to the next layer, then to 14.
|
|
94
|
+
|
|
84
95
|
**code_review.rounds.** Integer, default 2. The max fix-and-re-review rounds the **Code review** technique spends chasing **Critical** (blocking) findings during the `build`/`bug` Verify step. When the budget is exhausted with Critical findings still open, the technique never resolves or continues silently: it surfaces an explicit HITL checkpoint (run another 2 rounds, proceed with the blockers recorded, or run until it converges). Mirrors `plan_review.rounds`.
|
|
85
96
|
|
|
86
97
|
**effort (cost-aware gating).** `standard` (default) = the full flow for the map cycle: every `build`/`bug` step runs at full cost, no triage (today's behaviour, backward-compatible). `lean` = **cost-aware**, defined **canonically here**; the `build`/`bug` engine references this section under `lean` and adds only its own timing. **`quick` is the exception: it runs the size triage and the offer below on *every* ticket, independent of `effort`** (reaching for `quick` is itself the small-piece signal; `effort` governs the map cycle, not `quick`, see `../trailhead-work/SKILL.md`). Under `lean` (and always for `quick`) the engine runs a **size triage** in the orchestrator (main session, inline, no subagent, so the triage itself costs almost nothing), classifying the change predictively from the ticket scope and a quick glance at the target files (the real diff is unknown before Execute):
|
|
@@ -119,6 +130,7 @@ Example `config.json` (same shape for the project `.trailhead/config.json` and t
|
|
|
119
130
|
- **On Codex (host-detected), the Manual path asks `models.codex.*` instead of the Claude lineup.** Same discipline: `plan` and `execute` as two separate `AskUserQuestion` menus, never merged; the rarer `research`/`review`/`debug`/`verify`/`codebase-map`/`grill` behind Advanced, each its own choice; for `grill` the first option is **same as plan (Recommended)**, which leaves `models.codex.grill` unset (it then follows `models.codex.plan`), followed by the explicit models. Offer OpenAI model ids verified reachable at setup time (do not trust this list verbatim): `gpt-5.6-sol` (flagship) / `gpt-5.6-terra` (balanced) / `gpt-5.6-luna` (fast), plus **inherit session** (default). Avoid the retiring `gpt-5.4` / `gpt-5.4-mini` and the deprecated `gpt-5.3-codex`. Write the answers to `models.codex.*`; the Claude `models.*` lineup is not asked on Codex (those ids cannot run there). After writing, tell the user to **re-run the installer** (the same `--host=codex` install command) so the pins are re-projected into `~/.codex/agents/trailhead-*.toml`: the menu updates `.trailhead/config.json`, the installer is what regenerates the TOMLs and enables `features.multi_agent_v2`.
|
|
120
131
|
5. **🎨 Design**: 💾 Local disk · 🖼️ Artifacts (a Design canvas on claude.ai, built with Claude Code's `Artifact` tool; Claude Code only) · 🧵 Stitch (Google Stitch MCP, cross-host: Claude Code + Codex). *Mode only, don't ask for a URL here; the canvas/project is created or reused at execution time and cached in `design.project` (see Prototype).* If Stitch is picked, ask the **🖼️ Design surface**: 🖌️ Canvas (a Stitch project of screens to react to; default) · 🧩 Design system (push components into a shared Stitch design system) → `design.surface`. If Artifacts is picked, optionally ask the **🧩 Design system** to install on the canvas (list the account's Design System Artifacts, or "decide at the first screen") → `design.system`. Then **✅ Mockup approval**: 🖐️ Explicit (wait for a go-ahead before UI code) · ⏩ Auto (proceed without blocking).
|
|
121
132
|
6. **🧪 TDD**: 🎯 Seams only · ✅ On (all definable behaviour) · ⛔ Off.
|
|
133
|
+
- **🧪 Test suite** (sub-step): ⏭️ Keep current (when `testing.full` is set) · 🔎 Detect from package scripts and propose (show the `testing.full` / `testing.targeted` / `testing.maxParallel` it would write, save on yes) · 🚫 None (leave `testing.full` unset).
|
|
122
134
|
7. **⚖️ Effort**: how hard the `build`/`bug` engine works each change (`effort`). 🛠️ **Standard** (default; every step runs) · 🍃 **Lean** (size-triage small changes and offer to skip heavy steps, on an explicit yes only: the plan pass, TDD, cross-AI review, the full code review, the goal-backward verify; correctness checks always stay). Governs `build`/`bug` only: `quick` always size-triages regardless.
|
|
123
135
|
8. **🖥️ Acceptance testing**: 🤖 Auto · ▶️ Always browser · 🚫 No browser (acceptance still runs, starting with the drive-mode ask; only browser-driving is off, see `acceptance.browser` above). If browser is enabled, ask **🔗 base URL** and confirm it's a web app.
|
|
124
136
|
- **🧰 UAT setup** (sub-step): ask whether the project needs setup before a UAT: ⏭️ Keep current (when `uat.setup` is already set) · 🔎 Detect now and propose (probe package scripts, Expo/adb, docker compose, Supabase functions/migrations, an already-running server; show the plan, save on yes with `$ENV` references) · 🚫 None needed (writes `uat.setup: []`) · ⏳ Decide at the first UAT (leaves it unset).
|
|
@@ -20,3 +20,5 @@ Load the **effective config** (per `../_shared/configuration.md`) at session sta
|
|
|
20
20
|
**Pinned model review, once per session.** After loading the effective config, run the read-only check: `node <lib> check <project root>`, where `<lib>` (an absolute path, resolved from this `_shared/` dir's absolute location) is the first of these two paths (relative to this `_shared/` dir) that exists: `../../hooks/lib/model-defaults-review.js` (Claude plugin, Claude npm copy, dev-symlink install), else `../trailhead/hooks/lib/model-defaults-review.js` (Codex). If neither exists, skip silently. If it prints a notice, offer it to the user without blocking the current request; only after the user answers any of its three choices, run `node <lib> ack <project root>`. Do this at most once per session: skip it if the SessionStart hook (or an earlier config load this session) already surfaced the same offer. Repeats across sessions are expected until the user answers.
|
|
21
21
|
|
|
22
22
|
**Label guard check, once per session.** Same pattern for the repo's label guard: run `node <templates>/trailhead-label-guard-sync.js --repo <project root> --notice`, with `<templates>` (an absolute path) resolved as for the commit-msg sync script (see `teamwork.md`, Trust & provenance → Repo-side enforcement; skip silently if none of the template dirs has the script). It prints an offer only when the committed guard is the outdated one (no job-level `if:`, so every label event bills a runner) and the user has not answered for that file yet. Offer it without blocking the current request and follow its instructions; only after the user picks either choice, run the `--ack` command it names. At most once per session: skip it if the SessionStart hook already surfaced it.
|
|
23
|
+
|
|
24
|
+
**Repo target check, once per session, before the first tracker write.** In a fork clone `gh` can resolve the parent repository (an `upstream` remote, or `gh repo set-default`), so a bare `gh issue` or `gh api repos/{owner}/{repo}/...` would write there. Run `node <templates>/trailhead-repo-target.js check --repo <project root>`, with `<templates>` (an absolute path) resolved as for the commit-msg sync script (see `teamwork.md`, Trust & provenance → Repo-side enforcement; skip silently if none of the template dirs has the script). It prints one JSON line. `ok`, `reuse` and `skipped`: proceed silently. `ask`: before any `gh issue` create/edit/comment/close or `gh api` write, stop and ask the user which repository to write to (AskUserQuestion, one option per `candidates` entry, naming which is the fork and which the parent, or noting that the user has no write access there; result-oriented, never pick for them). Then run `node <templates>/trailhead-repo-target.js record --repo <project root> --target <chosen owner/name>`: it makes the chosen repo `gh`'s default for this clone, so every later bare `gh` call lands there, and remembers the answer for the session (8-hour sliding window, in `.trailhead/repo-target.json`; `record` adds that path to the repo's `.gitignore` if absent). A failed `record` (`status: error`) stops the write with a one-line report. Read-only `gh` calls (view, list) may run before the check.
|
|
@@ -28,6 +28,8 @@ gh issue edit <dep> --remove-label "trailhead:blocked"
|
|
|
28
28
|
gh issue edit <n> --add-assignee @me
|
|
29
29
|
# resolve
|
|
30
30
|
gh issue comment <n> --body-file <resolution> && gh issue close <n>
|
|
31
|
+
# a closed ticket never keeps the needs-you mark (a no-op when absent)
|
|
32
|
+
node <templates>/trailhead-needs-you.js clear --issue <n> --repo <owner/name>
|
|
31
33
|
# then the map's Decisions so far line, in one guarded call (fallback: the Fail-safe body rewrites rule in substrate.md, by hand)
|
|
32
34
|
node <templates>/trailhead-issue-body.js append --issue <map> --section "Decisions so far" --line "<pointer>"
|
|
33
35
|
```
|
|
@@ -68,6 +70,8 @@ Wire all three in the same pass; a ticket carrying only one or two has drifted.
|
|
|
68
70
|
|
|
69
71
|
Same-blocker rule: all three must name the SAME blocker ticket, never a superseded parent, a split-origin, or a by-role placeholder ("child A"). Write the prose line and the native dependency from the same id, in the same pass, so they cannot point at different tickets.
|
|
70
72
|
|
|
73
|
+
**Cross-references in a bulk create.** When several tickets are created together (chart, adopt, split, a cluster on the fly, a batch of graduated fog), the rule covers every `#n` and ticket link written in a body, not only blockers: in the first pass a body refers to a ticket of the same batch by name only, with no number and no link; once the batch exists, the wiring pass writes each cross-reference from the ids just created, as read from each `gh issue create` result. Never guess or predict a number: a guessed `#n` auto-links to an unrelated issue and leaves a stray mention on its timeline. A ticket that already existed before the batch (the map, a split origin, a closed decision) has a real number, so read it from the tracker and write it in the first pass. Adding a reference in the wiring pass is a body edit, so it follows **Fail-safe body rewrites** in `substrate.md`.
|
|
74
|
+
|
|
71
75
|
Unblock: remove the label from every dependent the moment its last blocker closes, for map and whiteboard tickets alike. Default path: `node <templates>/trailhead-unblock.js --issue <n> --repo <owner/name>` (`<n>` is the ticket just closed). By hand, when the helper is missing or errors: for each open dependent carrying `trailhead:blocked` in `gh api repos/{owner}/{repo}/issues/<n>/dependencies/blocking`, read `gh api repos/{owner}/{repo}/issues/<dep>/dependencies/blocked_by`, and when every blocker there is closed:
|
|
72
76
|
```bash
|
|
73
77
|
gh issue edit <dep> --remove-label "trailhead:blocked"
|
|
@@ -146,11 +150,16 @@ gh label create trailhead:uat --color BFD4F2 --description "Pending UAT: an acce
|
|
|
146
150
|
```
|
|
147
151
|
Tickets opened before the label existed are tagged by the idempotent backfill helper, `node <templates>/trailhead-uat-backfill.js [--repo <owner/name>] [--marker <path>]`: it ensures the label, tags every open ticket with a line starting `UAT of:`, and prints one JSON line `{"status","tagged":[...],"failed":[...]}` (a failed edit is reported, never fatal to the rest). With `--marker` it runs once per clone and exits `skipped` afterwards.
|
|
148
152
|
|
|
153
|
+
The needs-info label needs the same idempotent ensure, since an inbox triage can park an issue before any first-use setup on an older repo:
|
|
154
|
+
```bash
|
|
155
|
+
gh label create trailhead:needs-info --color FBCA04 --description "Waiting on the reporter: back in the inbox when they answer" 2>/dev/null || true
|
|
156
|
+
```
|
|
157
|
+
|
|
149
158
|
The needs-you label needs the same idempotent ensure, since an autonomous run can park a ticket before any first-use setup on an older repo (the helper below also runs it):
|
|
150
159
|
```bash
|
|
151
160
|
gh label create trailhead:needs-you --color D93F0B --description "Needs you: an autonomous run parked it for the human" --force
|
|
152
161
|
```
|
|
153
|
-
An autonomous run parks a ticket for the human with `node <templates>/trailhead-needs-you.js park --issue <n> --reason <text> --repo <owner/name>` (all three required). It secret-scans the reason (fail closed: nothing written when the guard is missing or hits), ensures the label, then in this order posts a comment whose first line is exactly `NEEDS YOU`, a blank line, then the reason (skipped when the latest `NEEDS YOU` comment carries the same reason, whitespace-normalised; it only sees the comments `gh` returns, so on a very long thread the worst case is one duplicate comment, never a missing one), unassigns `@me`, and adds the label last, so a partial failure leaves the ticket visible. It prints `{"status":"ok","issue","commented","unassigned","labelled"}`. A `work`/`quick` claim clears the mark with `node <templates>/trailhead-needs-you.js clear --issue <n> --repo <owner/name>`, which removes the label when present and otherwise does nothing, printing `{"status":"ok","issue","cleared":bool}`. Any failed or unparseable `gh` step prints `{"status":"error","reason"}` and exits 1 (a usage error exits 2): never read an error as a success.
|
|
162
|
+
An autonomous run parks a ticket for the human with `node <templates>/trailhead-needs-you.js park --issue <n> --reason <text> --repo <owner/name>` (all three required). It secret-scans the reason (fail closed: nothing written when the guard is missing or hits), ensures the label, then in this order posts a comment whose first line is exactly `NEEDS YOU`, a blank line, then the reason (skipped when the latest `NEEDS YOU` comment carries the same reason, whitespace-normalised; it only sees the comments `gh` returns, so on a very long thread the worst case is one duplicate comment, never a missing one), unassigns `@me`, and adds the label last, so a partial failure leaves the ticket visible. It prints `{"status":"ok","issue","commented","unassigned","labelled"}`. A `work`/`quick` claim clears the mark with `node <templates>/trailhead-needs-you.js clear --issue <n> --repo <owner/name>`, which removes the label when present and otherwise does nothing, printing `{"status":"ok","issue","cleared":bool}`. A closed ticket never waits on the human, however it was closed (a Resolve, a hand `gh issue close`, the web UI): Resolve runs `clear` right after the close, and `node <templates>/trailhead-needs-you.js sweep --repo <owner/name>` removes the label from every closed issue still carrying it (one `gh issue list --state closed --label trailhead:needs-you` plus one removal each), printing `{"status":"ok","cleared":[n,...]}`; a failed removal does not stop the others and ends in an error naming each failed issue, still carrying the `cleared` list. Any failed or unparseable `gh` step prints `{"status":"error","reason"}` and exits 1 (a usage error exits 2): never read an error as a success.
|
|
154
163
|
|
|
155
164
|
Before an autonomous run opens a UAT follow-up, it looks for an existing one: `node <templates>/trailhead-uat-followup.js find --parent <n> --repo <owner/name>` (`--repo` required). It reads every `trailhead:uat` issue, open and closed, through a paginated GraphQL `repository.issues` query (`gh api graphql`), not `gh issue list --label`, which goes through the lagging search index and would miss a follow-up opened seconds earlier, keeps those with a line starting `UAT of:` that points at the parent (a link to that repo's issue `<n>`, or a bare `#<n>`), and prints one JSON line `{"status":"ok","parent":<n>,"open":[{"number","title"}],"closed":[...],"reuse":<lowest open number>|null}`. Only an open follow-up is reusable; closed ones are reported, never reused. A failed, unparseable or incomplete read (GraphQL errors, unexpected shape, page cap exceeded: more than 10,000 `trailhead:uat` issues) prints `{"status":"error","reason"}` and exits 1: treat it as unknown, never as none found.
|
|
156
165
|
|
|
@@ -236,6 +245,6 @@ gh api repos/{owner}/{repo}/issues/<map> --jq '.sub_issues_summary.total'
|
|
|
236
245
|
When any checkout path you were handed or recorded is missing, never search the disk by hand: run `node <templates>/trailhead-checkout-find.js --repo <host/owner/name>` (the host whenever it is known; a bare `owner/name` matches any host) and act on its one JSON line. `found` -> use its `path`; `ambiguous` -> ask the user which of the `candidates` (never pick one; a `truncated` result is ambiguous even with one candidate); `none` -> report that no checkout of the repo was found. It matches the `origin` remote, searching the folders around `--cwd` and the usual code folders under `$HOME` first, then `$HOME`. `<templates>` resolves like the other helpers (see **Move into the handed checkout** in `trailhead-work`).
|
|
237
246
|
|
|
238
247
|
## First-use repo setup (do ALL THREE, every chart or adopt, never skip any)
|
|
239
|
-
1. Create any missing labels with `gh label create` (all twenty-
|
|
248
|
+
1. Create any missing labels with `gh label create` (all twenty-two: `trailhead:map`, `trailhead:codebase`, `trailhead:conventions`, `trailhead:dashboard`, `trailhead:ticket`, the six type labels, `trailhead:blocked`, `trailhead:seed`, `trailhead:out-of-scope`, `trailhead:superseded`, `trailhead:unverified`, `trailhead:fog`, `trailhead:whiteboard`, `trailhead:paused`, `trailhead:uat`, `trailhead:needs-you`, `trailhead:needs-info`), then run the UAT backfill helper once (`node <templates>/trailhead-uat-backfill.js`) so a pending UAT follow-up opened before the label existed is tagged too; stay silent when it tagged nothing.
|
|
240
249
|
2. **Check the label guard is installed and current**: `gh api repos/{owner}/{repo}/contents/.github/workflows/trailhead-label-guard.yml`; if it's absent (404), install it; if it's present, run the label guard sync script (`trailhead-label-guard-sync.js`, check-only) on the working root and, when it reports `outdated` (a pre-0.12.1 guard with no job-level `if:`, which starts a billed runner on every `issues: labeled` event), offer the upgrade: **read `teamwork.md` (Trust & provenance → Repo-side enforcement) for the exact steps**. This is part of standing up trailhead in a repo, not an optional extra: *check every time*, so a repo can never end up with the labels but no guard, or with a guard that burns Actions minutes.
|
|
241
250
|
3. **Sync the commit-msg git hook**: run the commit-msg hook sync script (`trailhead-commit-msg-sync.js`; installs the `sh` wrapper plus its `trailhead-commit-msg.cjs` logic if absent, upgrades an outdated trailhead hook (including the legacy single-file one) in place, never touches a foreign one; a hook that cannot run, node off PATH or the `.cjs` missing, says so on stderr at each commit instead of failing silently) so trailhead's commit discipline (Conventional Commits subject, no `Co-Authored-By`, and a `Refs: #<n>` trailer while a `.trailhead/session-ticket` marker is active) runs on **every** `git commit` regardless of host (the Claude-Code `PreToolUse` guard stays too, defence in depth): **read `teamwork.md` (Trust & provenance → Repo-side enforcement) for the exact steps**. If the script is missing or node errors (missing means none of the three template dirs listed in `teamwork.md` has it: try them all first), fall back to inspecting the hooks dir (`git rev-parse --git-path hooks`) per `teamwork.md`, never a blind "not enforced". Like the label guard, *check every time*; unlike it, `.git/hooks/` is per-clone local state (untracked), so it is installed, never committed.
|
|
@@ -9,7 +9,7 @@ Conventions:
|
|
|
9
9
|
| Element | How |
|
|
10
10
|
|----------|------|
|
|
11
11
|
| Map | an issue with label `trailhead:map`. **Not pinned** (the 3 pinned slots are the repo-scoped [dashboard](../trailhead-view/SKILL.md#render-the-dashboard-dashboard) + codebase + conventions): open maps are found via the `trailhead:map` label and listed in the pinned dashboard, so a repo's maps stay one click away without contending for a slot. **When a map is exhausted** (destination reached: no open tickets and no fog left), **ask the user whether to close the map issue**: on a yes, `gh issue close` it (a closed issue is still the durable record) and refresh the dashboard so it drops off; on a no, leave it open. Never close a map unprompted; there is no pin to free either way. Each map's **tickets** share a **`trailhead:map-<n>` label** (n = the map's issue number) that scopes its frontier; the map issue itself is found by its number and does not carry it. |
|
|
12
|
-
| Codebase | a **single per-repo** issue with label `trailhead:codebase`: the distilled codebase map (architecture, stack, conventions, decisions embodied, risks, test/build). It's repo-scoped: **shared by every map of this repo, owned by none**, so it survives when a map is finished. Each map's Notes *links* it (never re-inlines it). Not a `trailhead:ticket`, so it's off the frontier. Written at adopt; a **greenfield map** (`/trailhead:new`) has no code to map at chart, so it's created **later, once the repo has substantial code** (e.g. after the first build tickets land), via the Codebase map technique. **Kept fresh at Resolve**: when a resolved `build`/`bug` ticket **materially changes** the map (a new module or layer, a new dependency/integration, a new seam, a changed build/test command, a new convention), **patch the affected facet of this issue in place** as part of Resolve, one or two lines, not a re-run of the fan-out. Cosmetic or in-facet changes leave it alone; a full re-map is only for major drift. |
|
|
12
|
+
| Codebase | a **single per-repo** issue with label `trailhead:codebase`: the distilled codebase map (architecture, stack, conventions, decisions embodied, risks, test/build). Each facet opens with `_as of <short sha>_`, the commit it was written or last patched against. It's repo-scoped: **shared by every map of this repo, owned by none**, so it survives when a map is finished. Each map's Notes *links* it (never re-inlines it). Not a `trailhead:ticket`, so it's off the frontier. Written at adopt; a **greenfield map** (`/trailhead:new`) has no code to map at chart, so it's created **later, once the repo has substantial code** (e.g. after the first build tickets land), via the Codebase map technique. **Kept fresh at Resolve**: when a resolved `build`/`bug` ticket **materially changes** the map (a new module or layer, a new dependency/integration, a new seam, a changed build/test command, a new convention), **patch the affected facet of this issue in place** as part of Resolve, one or two lines, not a re-run of the fan-out. Cosmetic or in-facet changes leave it alone; a full re-map is only for major drift. |
|
|
13
13
|
| Conventions | a **single per-repo** issue with label `trailhead:conventions`: the project's **way of working**, readable by everyone. Repo-scoped and linked from every map's Notes, like Codebase. It opens with a small machine-read header the engine obeys, then human prose. Header keys (defaults **bold**): `git:` **`main`** \| `pr` (commit straight to `main`, or feature branch + PR) · `release:` **`command`** \| `auto` (never release without an explicit command, or release automatically per the project's flow) · `isolation:` **`auto`** \| `none` \| `worktree` \| `clone` (**`auto`** (default): at a ticket's work-start ask whether more than one ticket will be worked at once on this machine this session, if not work in the current checkout like `none`, if so resolve by repo shape (a plain repo -> `worktree`, a superrepo with submodules -> `clone`); `none`: work in the current checkout; `worktree`: give each executing ticket its own `git worktree` + `trailhead/t<n>` branch; `clone`: a dedicated per-ticket **clone** at `../<repo>-t<n>` for **path-bound apps a worktree can't build**, e.g. React Native/Expo with a local `node_modules`; see [Working as a team](teamwork.md#working-as-a-team)). **`worktree`/`clone` imply a branch per ticket even under `git: main`** (git won't check the trunk out twice); the branch integrates to the trunk per `git:` at Resolve. Under **`none`** two in-progress tickets with overlapping `Scope:` **serialise** (the second holds at claim). Below the header: the standing "how we work here" notes. **Filled at chart/adopt by a brief conventions brainstorming** (see the Invocation modes), not a `trailhead:ticket`, off the frontier. |
|
|
14
14
|
| Dashboard | a **single per-repo** issue with label `trailhead:dashboard`: the pinned index of the whole trailhead surface. Repo-scoped and permanent, like Codebase and Conventions, and it occupies the **fixed 3rd pinned slot** alongside them (`gh api graphql` `pinIssue` mutation on its node id). It holds: a link to **every open `trailhead:map`** (GitHub renders each map's sub-issue **progress bar natively**, so the dashboard only links, it does not recompute per-ticket progress), the **whiteboard** as a section (or a link to the `/trailhead:whiteboard` view), and dynamic **counts** (untriaged inbox, whiteboard frontier size). **Created and pinned at the first chart/adopt**, then **rewritten on structural events** (a map is charted or exhausted, i.e. a map appears or disappears; a whiteboard ticket is born or resolved) **and on demand** via `/trailhead:dashboard`. **Not rewritten on every *map* ticket resolve** (that would churn a pinned issue and its notifications; each map's native progress bar already tracks its per-ticket progress, but the whiteboard has no progress bar, so a whiteboard ticket's birth or close refreshes the dashboard, with one timing exception: a `quick`-born ticket, born and resolved in one session, refreshes at its handoff (Resolve, or pause), not at creation). **An exhausted-but-open map** (destination reached, but its issue never closed) is flagged `exhausted · closeable` here via a cheap per-map `0 open scoped issues?` pre-filter confirmed against remaining fog, and the render offers to close it (recurring across the dashboard render and the read-only self-heal entry points, never auto-closing), so a missed exhaustion moment gets a second chance. It likewise flags a map **near cap · `<total>/100`** or **at cap · prunable** as it nears GitHub's 100 native sub-issue cap, and offers `/trailhead:prune`. See [The dashboard](../trailhead-view/SKILL.md#render-the-dashboard-dashboard). |
|
|
15
15
|
| Ticket | a child issue with label `trailhead:ticket` + exactly one **type** label: `trailhead:decision` / `trailhead:research` / `trailhead:prototype` / `trailhead:build` / `trailhead:bug` / `trailhead:task`, plus its map's `trailhead:map-<n>` label and native sub-issue edge (see Child→map link). **A `trailhead:whiteboard` ticket carries none of the map wiring** (no `trailhead:map-<n>`, no sub-issue edge, no `Parent:`); see [The whiteboard](../trailhead-view/SKILL.md#render-the-whiteboard-whiteboard). |
|
|
@@ -28,6 +28,7 @@ Conventions:
|
|
|
28
28
|
- **`trailhead:superseded`**: closed because split into child tickets that replace it (see [Working as a team](teamwork.md#working-as-a-team)); *not* resolved, and out of `Decisions so far`. Its native sub-issue edge is dropped at close to reclaim a slot (the eager drop in `substrate-commands.md`), references intact.
|
|
29
29
|
- **`trailhead:unverified`**: a `trailhead:*` issue whose provenance isn't trusted (see [Trust & provenance](teamwork.md#working-as-a-team)); quarantined off the frontier until a maintainer adopts or rejects it.
|
|
30
30
|
- **`trailhead:fog`**: an in-scope-but-not-yet-sharp issue (usually an inbound suggestion) **kept open as a clarification space** (visible and tracked, off the frontier and out of the inbox) where the reporter and others discuss until it's sharp enough to graduate into a ticket. See [Inbox](../trailhead-chart/references/inbox.md).
|
|
31
|
+
- **`trailhead:needs-info`**: an inbox issue **parked while waiting on its reporter** (the maintainer asked a question). Not a ticket and not a state of one: the `trailhead:*` label keeps it out of the incoming query, and it comes back in the inbox's waiting section only when the reporter comments after the label; after `inbox.needs_info_days` with no answer the inbox offers to close it, never closes it on its own. Added after the question is posted. See [Inbox](../trailhead-chart/references/inbox.md).
|
|
31
32
|
- **`trailhead:whiteboard`**: a **map-less** ticket, parked on the **whiteboard** (the container for work not tied to any map). **Mutually exclusive with a `trailhead:map-<n>` label**: a ticket is on a map or on the whiteboard, never both, and a whiteboard ticket has no `Parent:` line and no native sub-issue edge. It carries its own frontier (below), off every map's. Captured there when a capture is routed to the whiteboard, or born there by `quick`; see [The whiteboard](../trailhead-view/SKILL.md#render-the-whiteboard-whiteboard).
|
|
32
33
|
- **`trailhead:paused`**: a ticket whose work is paused: set by pause (after the `PAUSED` checkpoint comment), cleared by resume or by a `work`/`quick` claim of it (working it is resuming it). It is **excluded from the frontier** (a paused ticket is taken back with `/trailhead:resume`; an explicit `work <n>` / `quick <n>` claim still works it), and it tells every surface (the panel, the map and whiteboard renders, the dashboard) to show the ticket once, under its `Paused` group.
|
|
33
34
|
- **`trailhead:needs-you`**: a ticket an autonomous run parked because its next step needs the human (a decision, a human-only checklist, a rail action, a surviving blocking review concern): set by the run on parking, together with a `NEEDS YOU` comment naming what the human must do, written at once. It is **not a frontier exclusion for humans**: the ticket stays on the frontier and the map, and `/trailhead:work` still offers it. Only the autonomous run's own scan skips it, so later runs never reconsider it. A `work`/`quick` claim of it clears the label (taking it back is working it), and a human can remove it by hand.
|
|
@@ -41,4 +42,4 @@ Conventions:
|
|
|
41
42
|
|
|
42
43
|
**Blocked-by reconciliation (drift check).** The `## Blocked by` prose and the native dependency are two copies of one fact, so they can drift (it has happened: a body naming the split-*origin* while the native edge pointed at the sibling). When rendering `/trailhead:map` and before working a ticket, **reconcile them deterministically**: parse the issue numbers in the ticket's `## Blocked by`, read its native `blocked_by` (`gh api repos/{owner}/{repo}/issues/<n>/dependencies/blocked_by`), and if the two sets differ, **surface an advisory** naming both sides (the native edge is the structured reference); never auto-fix, never block. If the body has no parseable `## Blocked by`, report it **uncheckable**, never assume they agree. This is the one deterministic, false-positive-free consistency axis; higher-level "does a ticket contradict the map's decisions" drift is left to human judgement.
|
|
43
44
|
|
|
44
|
-
**Base commands & first-use repo setup → `substrate-commands.md`.** The `gh` cookbook: create map/ticket, the child→map three-view wiring and the blocker three-move wiring, the frontier and whiteboard-frontier queries, claim, resolve, the dashboard commands (find, pin/re-pin/unpin, self-heal, body generation with count queries), and the first-use setup that runs at every chart/adopt (create the twenty-
|
|
45
|
+
**Base commands & first-use repo setup → `substrate-commands.md`.** The `gh` cookbook: create map/ticket, the child→map three-view wiring and the blocker three-move wiring, the frontier and whiteboard-frontier queries, claim, resolve, the dashboard commands (find, pin/re-pin/unpin, self-heal, body generation with count queries), and the first-use setup that runs at every chart/adopt (create the twenty-two labels + check/install the label guard + install the commit-msg hook). Read it at work time and at chart/adopt.
|
|
@@ -36,10 +36,10 @@ With the default `auto` this choice is made for you at each ticket's work-start;
|
|
|
36
36
|
|
|
37
37
|
**If the header never set `isolation:` at all**, it resolves to **`auto`** (the default) at runtime: the work-start concurrency ask decides the mode by repo shape (above), and **nothing is written** into the conventions. This supersedes the earlier behaviour where an absent key silently meant `none` with a one-time collision sanity-check that wrote a key; that nudge-and-write flow is **gone**. A header that already carries `isolation:` (any value, including `none` or `auto`) was chosen deliberately and is honoured as-is.
|
|
38
38
|
|
|
39
|
-
**Stale-base preflight.** A UAT run on a base that is behind the trunk tests the wrong code, and a merge that is textually clean can still break the trunk. So the engine measures the base, syncs it,
|
|
39
|
+
**Stale-base preflight.** A UAT run on a base that is behind the trunk tests the wrong code, and a merge that is textually clean can still break the trunk. So the engine measures the base, syncs it, runs the project's own checks and the tests once on the synced tree before UAT and before integration, in this order. In Verify: preflight -> sync -> checks -> tests -> Code review -> goal check -> UAT. At Resolve: preflight -> sync -> checks -> tests -> integration. (The project checks run inside the same preflight call, right after the sync, so they precede the tests. The preflight itself never runs the suite: after `synced`, the run on the synced tree is Verify's single run, or Resolve's, never an extra invocation.) The helper is `node <templates>/trailhead-base-preflight.js --repo <working-root> --config-root <main-checkout> [--sync]` (same three-dir `<templates>` lookup as the commit-msg sync above; `--config-root` is the original checkout the effective config was loaded from, always passed under `worktree`/`clone` so the workspace never re-resolves config). It fetches the trunk (`origin/<trunk>` really advances), picks one base ref, and prints one JSON line `{status, trunk, base, behind, ahead, method?, pulled?, testsRequired, conflicts, checks, checksOk, warnings, message?}`. Run it without `--sync` to measure and with `--sync` to bring the base current.
|
|
40
40
|
|
|
41
41
|
- **When it runs.** At the **start of Verify** (`build`/`bug`), before the tests, the Code review, the goal check and the UAT, so a rebase never rewrites SHAs a review already cited and the later steps see the synced tree; before the drive-mode ask on the paths that skip Verify (Pending-UAT guard option (a), and a `UAT of:` follow-up); and at Resolve before integrating. Under `git: main` in the shared checkout, right before the trunk push. Under `git: pr` without isolation, on the ticket branch, immediately before the push and PR. Under `worktree`/`clone`, first on the **integration target** (the main checkout, on the trunk, with `--sync`) and then on the ticket branch against it, immediately before the merge or PR. A push rejected as non-fast-forward re-runs the preflight with `--sync`, re-runs the suite, and retries. A Resolve sync that rewrote the ticket's commits (`method: rebase` with `pulled` > 0) leaves the review ledger citing stale SHAs: recompute the ticket diff and re-run the Code review on it before integrating.
|
|
42
|
-
- **Reading the status.** Fail closed: only `current` and `synced` (with `checksOk`) let the work go on. `synced` means the base was brought current (`method`: `ff`, `rebase` or `merge`; `pulled` commits) and `testsRequired` is true:
|
|
42
|
+
- **Reading the status.** Fail closed: only `current` and `synced` (with `checksOk`) let the work go on. `synced` means the base was brought current (`method`: `ff`, `rebase` or `merge`; `pulled` commits) and `testsRequired` is true: run the test suite on the synced tree (through `trailhead-test-run.js` when `testing.full` is configured) before going on: in Verify that run is the single full run of the ticket, recorded as its test record; at Resolve it is a real run only when the sync produced a new tree, and its test record is added to the resolution comment, superseding the Verify record. An unchanged tree returns the cached green (`source: cache`). A failure loops back to Execute or Fix. `current` needs no re-run at Resolve when the tree equals the Verify record's tree. `behind` (run without `--sync`) means sync it. `conflict`: stop, name the `conflicts` files, HITL (the helper aborted, the tree is as it was; auto never resolves a merge conflict). `dirty`: commit the work in progress first. `unverified` (the fetch failed, the distance is from local refs): stop and offer a retry or proceeding on local refs knowingly. `trunk-diverged`: the local trunk and `origin/<trunk>` both moved, reconcile the trunk first. `sync-failed` (git refused for another reason, for example an untracked file it would overwrite; the message says which): stop. `error`, `skipped`, a missing helper or a node failure also stop before UAT or integration with a HITL choice; never skip the preflight silently.
|
|
43
43
|
- **Project checks.** Failing `checks` (declared in `preflight.checks`, see `configuration-reference.md`, **Preflight checks**) are an acceptance or verify finding: stop before UAT or integration. A `checksError` (invalid declaration) stops the same way.
|
|
44
44
|
- **Report.** One result-oriented line, for example: the base was 60 commits behind main: synced, tests re-run, green.
|
|
45
45
|
|
|
@@ -72,7 +72,7 @@ A repo can hold **more than one live map** at once: you park one to work another
|
|
|
72
72
|
## Splitting a ticket
|
|
73
73
|
A ticket is sized to one ~100K-token session. When a claimed ticket turns out too big, or fans into distinct pieces, **split it**: don't grind through an oversized ticket. **Splitting is never autonomous.** Whether it was asked for explicitly (`/trailhead:split`) or surfaced mid-work ("this is too big once I'm in it"), **stop and ask the user before creating anything** (the split's shape, which children, is an advisory choice that **offers** the delegate option, per the injection recipe in `choices.md`):
|
|
74
74
|
1. **Propose the split and wait for a go-ahead.** Present the children you would create (each one's title and why it's a distinct session-sized piece) and how the original's blockers would re-point, then **wait for the user's confirmation**. Create nothing until they say go: this is a stop-and-ask moment like the rest of the engine's, never the agent's own call. (An explicit `/trailhead:split` is the user asking *to* split, not a go-ahead on *how*: you still propose the decomposition and confirm it.)
|
|
75
|
-
2. **Create the child tickets** (create-then-wire): each carries `Parent: <map>`, a `Split from: <original name+link>` line, its type label, the map's `trailhead:map-<n>` label + native sub-issue edge (like any ticket), and `trailhead:blocked` if it has an open blocker. **When children block each other** (B builds on A), wire B's blocker (both the `## Blocked by` line and the native dependency) at the **sibling child's real id (A)**, never at the split-origin: that original is being superseded and closed, so nothing may point at it as a live blocker. Describe A by role if you like, but *link its actual number*.
|
|
75
|
+
2. **Create the child tickets** (create-then-wire): each carries `Parent: <map>`, a `Split from: <original name+link>` line, its type label, the map's `trailhead:map-<n>` label + native sub-issue edge (like any ticket), and `trailhead:blocked` if it has an open blocker. **When children block each other** (B builds on A), wire B's blocker (both the `## Blocked by` line and the native dependency) at the **sibling child's real id (A)**, never at the split-origin: that original is being superseded and closed, so nothing may point at it as a live blocker. Describe A by role if you like, but *link its actual number*. Every other reference between siblings follows the same pass, from the ids just created, never a guessed number (**Cross-references in a bulk create** in `substrate-commands.md`).
|
|
76
76
|
3. **Supersede the original**: post a comment listing the children (`split into <names+links>`), label it **`trailhead:superseded`**, **drop its native sub-issue edge from the map** (the eager drop at close in `substrate-commands.md`: label first, then the 404-tolerant DELETE, then close, so a slot is reclaimed while the label, `trailhead:map-<n>`, `Parent:` and split-origin links stay intact), and **close it**. The children fully replace it.
|
|
77
77
|
4. The superseded ticket stays **out of `Decisions so far`** (it wasn't a decision walked; its children are). If other tickets were blocked by the original, re-point their `## Blocked by` at the relevant child and re-evaluate their `trailhead:blocked`.
|
|
78
78
|
|
|
@@ -112,3 +112,5 @@ The map issue's **body** is a shared mutable document: two sessions appending to
|
|
|
112
112
|
**The commit-msg git hook (host-independent commit guard).** On every host trailhead ALSO installs a git **`commit-msg`** hook as a host-independent backstop (a small `sh` wrapper, with its logic installed beside it as `trailhead-commit-msg.cjs`: a `.cjs` is always CommonJS, so a repo whose `package.json` says `"type": "module"` cannot break it; the wrapper never blocks a commit on a startup failure such as node or the `.cjs` missing, and then prints a one-line `trailhead: commit-msg hook inactive (<reason>)` notice to stderr) (defence in depth alongside the host's own `PreToolUse` guard on Claude Code and Codex alike), enforcing the two rules (a Conventional Commits subject of at most 72 chars; never a `Co-Authored-By` trailer) plus, while a `.trailhead/session-ticket` marker is present at the worktree root, a `Refs: #<n>` trailer matching its number, on every `git commit`, whatever host drives it (decision #16). **Every chart or adopt, and at every `work`/`quick` ticket start, run the sync script**: `node <templates>/trailhead-commit-msg-sync.js --repo <working-root>`, where `<templates>` is the first of `${CLAUDE_PLUGIN_ROOT}/templates/`, `${CLAUDE_CONFIG_DIR:-~/.claude}/trailhead/templates/`, `~/.codex/skills/trailhead/templates/` that contains `trailhead-commit-msg-sync.js`: try all three in order, skipping the first when `CLAUDE_PLUGIN_ROOT` is unset or empty (it is often absent from the Bash environment), and never stop at the first miss. It prints one JSON status line and handles every case itself: absent -> installs it; an older trailhead hook (the legacy single-file JS hook, or an outdated wrapper or logic file; recognised by its own header line, not by trusting the filename) -> upgraded in place to the current layout; already current -> left alone; a foreign (non-trailhead) hook -> never touched, only reported (offer to chain it); under a repo's `core.hooksPath`, an existing trailhead hook there is still upgraded in place, but nothing is newly added to that directory. Unlike the label guard, `.git/hooks/` is not version-controlled, so this is a per-clone local install with nothing to commit. **If the sync script is missing (none of the three dirs has it: an outdated install) or node errors, never claim git enforces nothing before looking**: resolve the effective hooks dir with `git -C <working-root> rev-parse --git-path hooks` (resolve a relative output against the working root; it honours `core.hooksPath` and the git common dir shared by linked worktrees), read `<hooks dir>/commit-msg`, and classify it by its header line (a trailhead hook has `# trailhead commit-msg hook (git).` (the `sh` wrapper) or, for the legacy single-file JS hook, `// trailhead commit-msg hook (git).` among its first 5 lines, trimmed; never trust the filename alone; a symlinked `commit-msg` counts as foreign, as the script treats it). The wrapper also needs `<hooks dir>/trailhead-commit-msg.cjs` beside it: without it the wrapper lets every commit through, so treat a missing `.cjs` (or a legacy JS hook, which breaks in `"type": "module"` repos) as present-but-stale: suggest updating the install so the sync script can repair it. Report one result-oriented line: (a) trailhead hook present and executable (`test -x`): git does enforce trailhead's rules here (provided node is on the PATH git runs with; a GUI git client may lack it, and then the wrapper prints the inactive notice on each commit), but its freshness could not be checked or upgraded, so suggest updating the trailhead install; a trailhead hook that is NOT executable: git skips it, so it does not enforce trailhead's rules here (suggest updating the install so the sync script can fix the mode); (b) a foreign hook (or a symlink): left untouched, git does not enforce trailhead's rules here; (c) no hook: git does not enforce trailhead's rules here (suggest updating the install so the sync script can install it). Never blocks the work.
|
|
113
113
|
|
|
114
114
|
**Work state file (the ticket in play, for tooling).** Alongside the session-ticket marker, trailhead keeps a gitignored `.trailhead/work-state.json` at the working root recording the one ticket in play: `{map, ticket, title, step, auto, autoDone, session, updatedAt}` (`autoDone` is the number of tickets resolved so far in an autonomous run, `0` otherwise). It is independent of `session-ticket` and of the commit-msg hook, and written the same way on Claude Code and Codex by the template helper `trailhead-work-state.js`, found with the same three-dir `<templates>` lookup as the commit-msg sync above (the first of `${CLAUDE_PLUGIN_ROOT}/templates/`, `${CLAUDE_CONFIG_DIR:-~/.claude}/trailhead/templates/`, `~/.codex/skills/trailhead/templates/` that contains it). Forms: `node <templates>/trailhead-work-state.js set --repo <working-root> --map <n|none> --ticket <n> --title "<title>" --step discuss` at work-start (`--map none` for a whiteboard ticket), `node <templates>/trailhead-work-state.js set --repo <working-root> --step <step>` at each step change (`discuss`, `plan`, `execute`, `verify`, `resolve`, `paused`; it merges onto the file, and a different `--ticket` starts it fresh; it needs a ticket on file, so where the file may be absent, as at resume or a pause in a fresh workspace, pass the full work-start form with that step), and `node <templates>/trailhead-work-state.js clear --repo <working-root>` at Resolve, which removes both the work state file and the `.trailhead/session-ticket` marker in one call (each removal attempted independently; it prints `sessionTicket: removed|absent|skipped` next to the usual status). During an autonomous run every `set` adds `--auto true --auto-done <n>`, `n` being the tickets resolved so far in this run. The step-to-state mapping per ticket type lives in `trailhead-work/references/ticket-engines.md`. A missing helper or a non-zero exit is silent and never blocks the work (no user-facing line).
|
|
115
|
+
|
|
116
|
+
**Repo target record.** Beside `isolation-answer.json` and `work-state.json`, the engine keeps a gitignored `.trailhead/repo-target.json` at the project root: the repository the user chose for tracker writes in a fork clone (see `load-first.md`, Repo target check), reused for 8 hours of idle time. `trailhead-repo-target.js record` adds it to that repo's `.gitignore` if absent (only in a git checkout); inside a linked worktree the helper resolves to the main checkout, so every worktree reads the same record.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Code review: pipeline reliability
|
|
2
|
+
|
|
3
|
+
Loaded only on a pipeline-touching diff (see **Pipeline reliability** in `code-review.md`). It is not a sixth axis: findings use the same IDs, counts line and ledger as the other axes.
|
|
4
|
+
|
|
5
|
+
**Scope.** Review only jobs and steps the ticket's commits add or change, per the Ticket diff rule in `code-review.md`; the rest of the file is context.
|
|
6
|
+
|
|
7
|
+
**Checks.** Look for each of these in the in-scope jobs and steps:
|
|
8
|
+
|
|
9
|
+
- **Install ignoring the lockfile**: `npm install` where a lockfile exists (use `npm ci` instead).
|
|
10
|
+
- **No timeout**: a job without `timeout-minutes`.
|
|
11
|
+
- **No concurrency group**: a deploy without a `concurrency` group.
|
|
12
|
+
- **Rebuild per environment**: a rebuild per environment instead of build once, promote. It applies only when the workflow targets more than one environment.
|
|
13
|
+
- **No safety net on a deploy**: a deploy with neither a health check nor a rollback (either one present is enough).
|
|
14
|
+
- **Swallowed failure**: `continue-on-error` on a step that later steps depend on.
|
|
15
|
+
- **Static cloud credentials**: static cloud keys where the provider supports OIDC.
|
|
16
|
+
- **Floating action ref**: a third-party action on `@main` or `@latest`.
|
|
17
|
+
|
|
18
|
+
**Severity.** Every finding is a Warning, never Critical, so it never enters the Critical-only fix-and-re-review loop.
|
|
19
|
+
|
|
20
|
+
**Not findings**: global tool installs (`npm install -g`), reusable workflows (a job calling one takes neither `timeout-minutes` nor steps of its own), actions already pinned to a tag, and test-only workflows (a workflow that runs tests and publishes or deploys nothing: none of the checks apply).
|
|
21
|
+
|
|
22
|
+
**Secrets.** Hardcoded secrets are not in this reference: they stay Critical under the Security axis.
|
|
23
|
+
|
|
24
|
+
**Naming a deploy.** A deploy finding names what makes the workflow a deploy one (a quoted command, an `environment:`, a target).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Code review
|
|
2
2
|
|
|
3
|
-
**First, route it.** Run `node <hooks>/lib/model-route.js review --session <your full model id> <project root>` and obey its `route`. Your full model id is in your system prompt or environment ("The exact model ID is ..."); pass `unknown` only if it is truly absent. Obey it exactly: never decide inline versus dispatch by hand, never read the config yourself to decide. On `route: dispatch` spawn the `agent` it returns with model `alias` (`agent_type=<agent>` on Codex) and use its report; on `route: inline` run the protocol below in the session. Finding `<hooks>`, the flags and the two exceptions are in **Route every step through model-route.js** in `../techniques.md`. (If you are the `trailhead-code-review` agent, skip this gate: you are the dispatched step.)
|
|
3
|
+
**First, route it.** Run `node <hooks>/lib/model-route.js review --session <your full model id> <project root>` and obey its `route`. Your full model id is in your system prompt or environment ("The exact model ID is ..."); pass `unknown` only if it is truly absent. Obey it exactly: never decide inline versus dispatch by hand, never read the config yourself to decide. On `route: dispatch` spawn the `agent` it returns with model `alias` (`agent_type=<agent>` on Codex) and use its report; on `route: inline` run the protocol below in the session. On `route: dispatch` the spawn prompt also carries, with the protocol, one line `pipeline reference: <absolute path>`, the path being `code-review-pipeline.md` in the same directory as the `code-review.md` you just read by absolute path (never the reference's contents), and one line `standards files: <list>`, built per **Standards files** below. Finding `<hooks>`, the flags and the two exceptions are in **Route every step through model-route.js** in `../techniques.md`. (If you are the `trailhead-code-review` agent, skip this gate: you are the dispatched step.)
|
|
4
4
|
|
|
5
5
|
Runs after execute, before closing a `build`/`bug` ticket: as the routed step (a subagent on `route: dispatch`, in the session on `route: inline`; an accepted `lean` skip's lighter pass is also inline). On `route: dispatch` it is the **`trailhead-code-review`** agent (read-only: it reviews and reports, it must never Edit/Write or commit), with the route's `alias` as the model override; **never** an external-plugin reviewer agent that another installed tool registered, which follows a different protocol and may write its own review file instead of trailhead's `VERIFY` comment.
|
|
6
6
|
|
|
@@ -14,17 +14,30 @@ git -C <repo> log --reverse --basic-regexp --grep='^Refs: #<n>$' --format=%H HEA
|
|
|
14
14
|
|
|
15
15
|
git applies `--grep` per message line, so the `^...$` anchors match only the whole trailer line: `#<n>` never matches `#<n>0` or a mid-sentence mention, and `--basic-regexp` overrides any user `grep.patternType`. The set may be non-contiguous (other tickets' commits interleaved on the trunk), so consume it **per commit**, oldest first, with `git -C <repo> show --remerge-diff <sha>` (git 2.36+): for a regular commit that is its plain patch; for a trunk-sync merge carrying the trailer it diffs git's automatic re-merge against the recorded result, so it shows exactly the conflict resolution the ticket authored and nothing the merge brought in cleanly (an octopus merge carrying the trailer is surfaced to the user, not judged). The per-commit patches define what is attributed to the ticket; read the touched files at `HEAD` only to judge how the change integrates, and treat later foreign edits there as context, not findings. Never collapse the set into one `git diff <first>^..<last>` span, which re-includes the interleaved foreign changes. A commit without the trailer is, by this rule, not part of the ticket's diff; only an entirely empty set is detectable. **An empty set is an error**: stop and surface it (name the ticket and the command run), never fall back silently to a base range or the whole tree. Recompute the set on every review round and every verify run, never reuse a cached list: fix commits carry the same trailer, so the set grows with each fix-and-re-review round.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
**Standards files: list them before the review.** So the Standards & spec axis reads every documented standard in the repo, not whichever ones the reviewer happens to find, list them up front, on every pass including the lean inline one, with exactly this command (`<repo>` as in **Ticket diff**; a tracked file only, at any depth):
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
git -C <repo> ls-files -- ':(glob,icase)**/CONTRIBUTING*' ':(glob,icase)**/CODING_STANDARDS*' ':(glob)**/AGENTS.md' ':(glob)**/CLAUDE.md' ':(glob)**/.editorconfig' ':(glob)**/.eslintrc*' ':(glob)**/eslint.config.*' ':(glob)**/.prettierrc*' ':(glob)**/prettier.config.*' ':(glob)**/biome.json*' ':(glob)**/.stylelintrc*' ':(glob)**/.markdownlint*' ':(glob)**/ruff.toml' ':(glob)**/.ruff.toml' ':(glob)**/.flake8' ':(glob)**/.pylintrc' ':(glob)**/pyproject.toml' ':(glob)**/.rubocop.yml' ':(glob)**/.golangci.*' ':(glob)**/rustfmt.toml' ':(glob)**/.rustfmt.toml' ':(glob)**/clippy.toml' ':(glob)**/.clang-format' ':(glob)**/.shellcheckrc'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Then add the repo's `trailhead:conventions` issue as `issue #<n>`, looked up per **Find a singleton issue** in `../substrate-commands.md` (a repo without one simply has no such entry). On `route: dispatch` the orchestrator does this before the spawn and the spawn prompt carries the result as one line, `standards files: <list>`, comma-separated absolute paths (each listed path joined to `<repo>`) plus the issue entry, the paths only, never their contents; an empty result is `standards files: none`. Recompute the list on every review round, as with the **Ticket diff** set: a fix commit may touch a standard. On `route: inline` the session runs the listing and reads the files itself. The reviewer reads every entry before judging the axis (the issue with `gh issue view <n> --json body`), and each standards finding cites the file (or the issue) and the line it applies; with no such citation it is not a standards finding. A linter or formatter config also tells the reviewer what tooling already enforces, which the axis skips. This does not lift **Review adversarially** above: a listed file may suppress a style smell, never a correctness or security finding. With no `standards files:` line in the prompt, the reviewer runs the same listing itself, never skips it; an entry it cannot read is named in the report, not guessed at. `standards files: none` means the repo documents no standard: the axis then judges the spec alone.
|
|
24
|
+
|
|
25
|
+
Review on five axes:
|
|
18
26
|
|
|
19
27
|
- **Correctness** (real bugs): broken logic, unhandled errors, edge cases, race conditions, wrong data flow.
|
|
20
28
|
- **Security**: injection, secret leakage, auth/authz gaps, unvalidated boundary input.
|
|
21
|
-
- **Reuse & simplification**: hunt the classic smells, each a *judgement call* (never a hard violation), and for each name it → say how to simplify: duplicated code (extract), mysterious name (rename), primitive obsession & data clumps (introduce a type), long parameter list, feature envy / message chains (move or collapse), repeated switches, speculative generality (delete), shotgun surgery & divergent change, middle-man / shallow pass-through (delete or deepen). Suppress any a documented standard endorses; skip what tooling enforces.
|
|
22
|
-
- **Standards & spec**: does it match the repo's documented conventions AND do what the ticket actually asked (missing requirements, scope creep, plausibly-wrong implementations)? Suppress smells a documented standard endorses; skip anything tooling already enforces.
|
|
29
|
+
- **Reuse & simplification**: hunt the classic smells, each a *judgement call* (never a hard violation), and for each name it → say how to simplify: duplicated code (extract), mysterious name (rename), primitive obsession & data clumps (introduce a type), long parameter list, feature envy / message chains (move or collapse), repeated switches, speculative generality (delete; see **Impossible-state guards**), shotgun surgery & divergent change, middle-man / shallow pass-through (delete or deepen). Suppress any a documented standard endorses; skip what tooling enforces.
|
|
30
|
+
- **Standards & spec**: does it match the repo's documented conventions (read the **Standards files** listed above and cite the file for each finding) AND do what the ticket actually asked (missing requirements, scope creep, plausibly-wrong implementations)? Suppress smells a documented standard endorses; skip anything tooling already enforces.
|
|
31
|
+
- **Impossible-state guards**: reachable code that defends against a problem that cannot occur in this project's stack. Subtypes: a guard on an impossible state; defence already enforced elsewhere; a pattern copied without its premises; a stale or phantom requirement; an abstraction with one instance (this extends the speculative generality smell). The guard's own code must be reachable (live), while the state it guards against must be shown impossible. Every finding cites concrete evidence (file:line or a doc) that the state cannot occur: a closed type or enum, an API or framework invariant, fixed configuration, validation already done upstream, or a guarantee of the stack itself. Without such evidence there is no finding, never the reviewer's say-so. Severity is at most Warning, never Critical, so it never enters the Critical-only fix-and-re-review loop. Not findings: trust boundaries (user input, network, files, env), deliberate defence-in-depth, test and DI seams, and dead code (a separate smell, not this axis). The exclusions win over the evidence: no evidence, upstream validation included, turns a check at a trust boundary into a finding.
|
|
32
|
+
|
|
33
|
+
**Pipeline reliability (only on a pipeline-touching diff)**: list the paths the ticket's commits touch (`git -C <repo> show --format= --name-only <sha>` per commit, the same set as the Ticket diff). When one matches `.github/workflows/*`, `Dockerfile*`, `docker-compose*` (basename, at any depth) or a deploy script (a script with `deploy` in its file name; a release script under another name is not matched), read `code-review-pipeline.md` and apply it, on every pass including the lean inline one; otherwise do not read it. Dispatched, read it at the path the spawn prompt names. Inline, it sits beside this file. It is not a sixth axis: the findings use the same IDs, counts line and ledger. If a path matches and the reference cannot be read (no path passed, or unreadable), the review is incomplete and fails closed: the report carries a Critical finding `pipeline checks did not run` naming the matched path, so its status is `issues_found` and Resolve stays blocked. The orchestrator discards that incomplete report (never posted, no fix round spent, the finding never enters the ledger, so the Disposition gate is untouched) and re-spawns with the path. If the reference is still unreadable, it surfaces a HITL checkpoint naming the path, and Resolve stays blocked. Inline, the session surfaces that checkpoint directly.
|
|
23
34
|
|
|
24
|
-
Classify each finding: **Critical** (bug/security/breaks the spec) → **Warning** (should fix) → **Info** (judgement call). Before reporting ANY finding, **verify it adversarially**: re-read the actual code, confirm the failing path is reachable and the claim holds. Discard plausible-but-wrong findings; an unverified finding is worse than none. Quote the offending hunk (and the standard/spec line) per finding. **Stable finding IDs**: each verified finding gets an ID the first time it's reported, `C1`, `W1`, `I1`, ..., by severity, numbered in discovery order, and keeps that ID across every round of the fix-and-re-review loop below; a re-review that finds the same issue still open reuses its ID, a genuinely new finding takes the next number in its severity. An ID's severity is fixed at first report: if a re-review reclassifies a finding to a different severity, mark the old ID `reclassified as <new ID>` in the ledger and continue the finding under a new ID of the new severity; a `reclassified` line is a pointer, not a disposition, and is left out of the counts, so the finding counts once, under its current severity. Return the report (the agent never posts it; the orchestrator posts it as the `VERIFY` comment after the report-status check), **led by a machine-checkable status tag on its own first line**: `status: clean` when no **Critical** finding remains (nothing blocks Resolve), or `status: issues_found` when one or more Criticals are still open (the caller must iterate the fix-and-re-review loop below, or run the HITL checkpoint, before resolving). Warnings/Info alone leave the tag `status: clean`, since they don't gate resolution. Emit that line verbatim as the report's first line.
|
|
35
|
+
Classify each finding: **Critical** (bug/security/breaks the spec) → **Warning** (should fix) → **Info** (judgement call). Before reporting ANY finding, **verify it adversarially**: re-read the actual code, confirm the failing path is reachable and the claim holds (except for Impossible-state guards, where what you verify is that the state cannot occur, not a reachable failing path; Impossible-state guards findings are at most Warning, never Critical, so they never enter the Critical-only fix-and-re-review loop). Discard plausible-but-wrong findings; an unverified finding is worse than none. Quote the offending hunk (and the standard/spec line) per finding. **Stable finding IDs**: each verified finding gets an ID the first time it's reported, `C1`, `W1`, `I1`, ..., by severity, numbered in discovery order, and keeps that ID across every round of the fix-and-re-review loop below; a re-review that finds the same issue still open reuses its ID, a genuinely new finding takes the next number in its severity. An ID's severity is fixed at first report: if a re-review reclassifies a finding to a different severity, mark the old ID `reclassified as <new ID>` in the ledger and continue the finding under a new ID of the new severity; a `reclassified` line is a pointer, not a disposition, and is left out of the counts, so the finding counts once, under its current severity. Return the report (the agent never posts it; the orchestrator posts it as the `VERIFY` comment after the report-status check), **led by a machine-checkable status tag on its own first line**: `status: clean` when no **Critical** finding remains (nothing blocks Resolve), or `status: issues_found` when one or more Criticals are still open (the caller must iterate the fix-and-re-review loop below, or run the HITL checkpoint, before resolving). Warnings/Info alone leave the tag `status: clean`, since they don't gate resolution. Emit that line verbatim as the report's first line.
|
|
25
36
|
|
|
26
37
|
**Counts line, right under `status:`.** The comment's second line, right under `status:`, is the cumulative severity count: `critical: <n> · warning: <n> · info: <n>` (zeros written out, never omitted), the count of distinct findings (by ID, excluding `reclassified` pointers) reported on this ticket so far, across every round. Cumulative means a finding fixed in an earlier round still counts here, so the totals always match the ledger below. Below the counts comes the **findings ledger** (authoritative for this ticket), one line per finding ID with its disposition: `fixed (<commit>)`, `deferred → <ticket>` (a ticket that exists, named and linked: an already-open ticket, or one opened on the user's explicit yes per **Scope that surfaces while working a ticket** (`../surfaced-scope.md`), never unasked; when the user declines or leaves it unanswered, and under an autonomous run, where no ticket is opened unasked, use `accepted: <one-line reason>` instead), `accepted: <one-line reason>`, or `open` while the disposition is still undecided. The ledger lives in the latest `VERIFY` comment of the ticket and is rewritten each round; the resolution comment links to it rather than duplicating it. Then the findings grouped by severity with file:line and the fix, as before, so the Verify step branches on the tag without parsing prose. A `status: clean` report whose counts line has Critical above zero must carry a ledger line closing each Critical (`fixed`, `deferred` or `accepted`); otherwise the report-status check fails it closed to `issues_found`.
|
|
27
38
|
|
|
39
|
+
**Test record.** When the orchestrator passes a test record (`test record: ...`, the full-suite outcome of Verify), read it and never re-run the full suite (a focused test to probe one finding is fine); with no record passed, test as today (without `testing.full` no test record is passed).
|
|
40
|
+
|
|
28
41
|
**Converge on the blockers with a bounded budget and an explicit boundary.** A **Critical** finding is *blocking*: it must be fixed before Resolve. Fixing one can introduce or reveal another, so the review is a **loop, not a single pass**: fix every Critical, then re-review the recomputed **Ticket diff** set, up to `config.code_review.rounds` (default 2) fix-and-re-review rounds. Stop when no Critical remains, then Resolve, giving each remaining Warning/Info its disposition in the ledger. **If the rounds are exhausted with Critical findings still open**, never resolve silently and never silently keep going: surface an explicit HITL checkpoint in the chat, **name each surviving blocker**, and put the choice to the user, (a) **run another 2 review rounds** on the remaining blockers (the point of this ask), or (b) **proceed with the blockers recorded** (each surviving Critical is recorded in the ledger as `deferred` or `accepted`), or (c) **run until it converges** (keep the fix + re-review loop going with no round cap, stopping only when no Critical remains; as a guard against an endless loop, if a round makes no progress, i.e. the same Critical findings come back unfixed, stop and bring the choice back to this checkpoint, naming them). **Wait for their pick; never auto-advance in either direction over open blocking findings.** (This mirrors **Cross-AI plan review**'s boundary ask: see `plan-review.md` step 4.)
|
|
29
42
|
|
|
30
43
|
**Disposition gate**: no finding reaches Resolve with `open`, or without a disposition: the ledger lists every ID the counts line counts (their sum equals the number of ledger lines other than `reclassified` pointers) and each carries a disposition other than `open`. A Critical leaves `open` only by becoming `fixed`, or, via the rounds-exhausted HITL option (b) above, `deferred` or `accepted`.
|
|
@@ -12,7 +12,7 @@ One-time at project adoption. Fan out 5 read-only reader subagents in parallel (
|
|
|
12
12
|
- **Risk & complexity hotspots**: tech debt, fragile/God files, security-sensitive paths, TODO/FIXME clusters, expensive-to-change areas.
|
|
13
13
|
- **Test & build setup**: framework, test layout, mocking style, how to run tests/build/lint, coverage posture.
|
|
14
14
|
|
|
15
|
-
Give each agent read-only tools only (Read/Grep/Glob, never Edit/Write), a tight facet brief, and instruction to cite concrete file paths in backticks and return an investigating report (`Return contract` in its definition) whose `answer:` is a compact summary (~10–20 lines), NOT file dumps, with `evidence:` as `file:line` paths. Keep facets disjoint so contexts don't collide; while they run, wait for all five. Run the report-status check on each report (`--technique codebase-map`, the *Malformed review report* protocol in `../techniques.md`) and read its `fields`: `status: found` -> use its `answer:`; `status: inconclusive` -> use its `answer:` too and note the gap in that facet; `status: needs_input` -> ask the user its `open questions:` once, then re-spawn that facet with the answers, or mark the facet unmapped if they cannot answer. Then **distil** the `answer:` fields (don't concatenate) into ONE compact write-up (a few lines per facet, load-bearing paths preserved) and
|
|
15
|
+
Give each agent read-only tools only (Read/Grep/Glob, never Edit/Write), a tight facet brief, and instruction to cite concrete file paths in backticks and return an investigating report (`Return contract` in its definition) whose `answer:` is a compact summary (~10–20 lines), NOT file dumps, with `evidence:` as `file:line` paths. Keep facets disjoint so contexts don't collide; while they run, wait for all five. Run the report-status check on each report (`--technique codebase-map`, the *Malformed review report* protocol in `../techniques.md`) and read its `fields`: `status: found` -> use its `answer:`; `status: inconclusive` -> use its `answer:` too and note the gap in that facet; `status: needs_input` -> ask the user its `open questions:` once, then re-spawn that facet with the answers, or mark the facet unmapped if they cannot answer. Then **distil** the `answer:` fields (don't concatenate) into ONE compact write-up (a few lines per facet, load-bearing paths preserved) and **stamp every facet**: each `## ` facet opens with `_as of <short sha>_` and a blank line, the sha from `git -C <scan root> rev-parse --short HEAD`, the commit the facet is true at; a full re-run restamps all of them. Post the write-up as the body of the repo's **`trailhead:codebase` issue**: a single per-repo issue (label `trailhead:codebase`), created if absent, updated if present. It is **not** in any map's Notes: it's repo-scoped, so every map of the repo links it and a finished map never strands it (each map's Notes carries just a link). The raw per-facet output is throwaway scratch: mine it, discard it. Do NOT persist per-facet reports as project state, do NOT re-run per ticket or per map.
|
|
16
16
|
|
|
17
|
-
**Keeping it fresh (incremental, not a re-run).** The full fan-out happens once. After that the issue is kept current **at Resolve**: when a resolved `build`/`bug` ticket materially changes the map (a new module or layer, a new dependency/integration, a new seam, a changed build/test command, a new convention), **patch just the affected facet in place**, a line or two, as part of that ticket's Resolve. Leave it untouched for cosmetic or in-facet changes. A **full re-run** is reserved for major drift (a rewrite, a stack change, an architecture the incremental patches no longer describe).
|
|
17
|
+
**Keeping it fresh (incremental, not a re-run).** The full fan-out happens once. After that the issue is kept current **at Resolve**: when a resolved `build`/`bug` ticket materially changes the map (a new module or layer, a new dependency/integration, a new seam, a changed build/test command, a new convention), **patch just the affected facet in place**, a line or two, as part of that ticket's Resolve. Leave it untouched for cosmetic or in-facet changes. Patch with `node <templates>/trailhead-issue-body.js ... --section "<facet>" --stamp <short sha>`: the stamp moves with the patch, a facet with no stamp gains it on its next patch, and an untouched facet keeps its old stamp. The audit flags a facet whose cited files changed after its stamp. A **full re-run** is reserved for major drift (a rewrite, a stack change, an architecture the incremental patches no longer describe).
|
|
18
18
|
|
|
@@ -21,5 +21,5 @@ When run as a subagent (`route: dispatch`), it is the **`trailhead-debug`** agen
|
|
|
21
21
|
|
|
22
22
|
**Pre-fix checkpoint: a confirmed cause, or no fix.** Before any fix is written, the `DIAGNOSIS` must record (1) the root cause stated as a *falsifiable* hypothesis and (2) the falsification test whose result would disprove it, actually run with the prediction holding. A cause that has not survived its own falsification test is still a guess: stay in diagnosis (loop steps 5–8, generating new hypotheses), never proceed to Fix. This is the hard gate on the `bug` engine's Diagnose→Fix boundary: only a confirmed cause graduates to the fix.
|
|
23
23
|
|
|
24
|
-
9. **Verify the fix.** State the oracle before the assertion (specified > derived/model > metamorphic > implicit-crash; never settle for "didn't crash"). Write the regression test at a correct seam exercising the real bug pattern; if none can exist, that absence IS a finding: flag it. Watch it fail, apply the fix, watch it pass. Then: original repro gone, full suite green, boundary neighbours (N-1/N/N+1, empty/singleton) pass, all `[DEBUG-]` probes and throwaway harnesses removed, winning hypothesis recorded in the commit.
|
|
24
|
+
9. **Verify the fix.** State the oracle before the assertion (specified > derived/model > metamorphic > implicit-crash; never settle for "didn't crash"). Write the regression test at a correct seam exercising the real bug pattern; if none can exist, that absence IS a finding: flag it. Watch it fail, apply the fix, watch it pass. When that red comes from a forced change (the fix reverted, code or a fixture mutated), prove the change landed before trusting the run: diff the mutated file against a clean copy, or re-read it, and see the mutation there. A mutation that silently failed to apply leaves the code as it was, so the run proves nothing: a red then has another cause, and a green says nothing about the test. Then: original repro gone, full suite green (through `node <templates>/trailhead-test-run.js run --repo <working-root> --config-root <project root>` when `testing.full` is set; inside the `bug` engine Debug and Fix run focused tests only and that full run is the one at Verify; standalone Debug runs it once through the helper, and a later Verify on the same tree is a cache hit), boundary neighbours (N-1/N/N+1, empty/singleton) pass, all `[DEBUG-]` probes and throwaway harnesses removed, winning hypothesis recorded in the commit.
|
|
25
25
|
|