@marcomigozzi/trailhead 0.13.0 → 0.15.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 +142 -29
- package/README.md +141 -28
- package/bin/base-preflight-docs.test.js +65 -0
- package/bin/execute-split-docs.test.js +146 -0
- package/bin/isolation-answer-docs.test.js +41 -0
- package/bin/isolation-copy-docs.test.js +28 -0
- package/bin/lib/codex-projection.js +17 -10
- package/bin/lib/codex-projection.test.js +80 -11
- package/bin/lib/install-verify.js +10 -5
- package/bin/lib/install-verify.test.js +51 -0
- package/bin/report-status-docs.test.js +195 -0
- package/bin/resolve-helpers-docs.test.js +36 -0
- package/bin/scope-confirm-skip-docs.test.js +70 -0
- package/bin/trailhead.js +40 -28
- package/bin/trailhead.test.js +503 -6
- package/bin/uat-setup-docs.test.js +65 -0
- package/package.json +4 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +3 -2
- package/plugins/trailhead/agents/trailhead-code-review.md +15 -2
- package/plugins/trailhead/agents/trailhead-codebase-map.md +20 -1
- package/plugins/trailhead/agents/trailhead-debug.md +20 -1
- package/plugins/trailhead/agents/trailhead-executor.md +29 -2
- package/plugins/trailhead/agents/trailhead-fix.md +29 -2
- package/plugins/trailhead/agents/trailhead-plan.md +20 -1
- package/plugins/trailhead/agents/trailhead-research.md +22 -3
- package/plugins/trailhead/agents/trailhead-verify.md +15 -2
- package/plugins/trailhead/commands/panel.md +10 -0
- package/plugins/trailhead/hooks/hooks.json +3 -18
- package/plugins/trailhead/hooks/lib/base-preflight.test.js +513 -0
- package/plugins/trailhead/hooks/lib/body-sources.js +795 -0
- package/plugins/trailhead/hooks/lib/commit-message-check.js +1 -1
- package/plugins/trailhead/hooks/lib/commit-message-check.test.js +3 -2
- package/plugins/trailhead/hooks/lib/commit-msg-module-repo.test.js +116 -0
- package/plugins/trailhead/hooks/lib/commit-msg-sync.test.js +100 -9
- package/plugins/trailhead/hooks/lib/dashboard.test.js +409 -0
- package/plugins/trailhead/hooks/lib/fixtures/commit-msg-legacy-js +104 -0
- package/plugins/trailhead/hooks/lib/gh-subcommand.js +92 -10
- package/plugins/trailhead/hooks/lib/gh-subcommand.test.js +55 -0
- package/plugins/trailhead/hooks/lib/isolation-answer.test.js +258 -0
- package/plugins/trailhead/hooks/lib/isolation-copy.test.js +298 -0
- package/plugins/trailhead/hooks/lib/issue-body.test.js +470 -0
- package/plugins/trailhead/hooks/lib/mockup-link.js +21 -5
- package/plugins/trailhead/hooks/lib/report-status.test.js +366 -0
- package/plugins/trailhead/hooks/lib/shell-scan.js +532 -65
- package/plugins/trailhead/hooks/lib/shell-scan.test.js +434 -0
- package/plugins/trailhead/hooks/lib/uat-backfill.test.js +150 -0
- package/plugins/trailhead/hooks/lib/uat-setup.test.js +340 -0
- package/plugins/trailhead/hooks/lib/unmodelled.js +345 -0
- package/plugins/trailhead/hooks/lib/unmodelled.test.js +173 -0
- package/plugins/trailhead/hooks/lib/work-state.test.js +297 -0
- package/plugins/trailhead/hooks/mod/README.md +219 -0
- package/plugins/trailhead/hooks/mod/map-pane-view.ts +606 -0
- package/plugins/trailhead/hooks/mod/map-pane.tsx +832 -0
- package/plugins/trailhead/hooks/mod/register.tsx +293 -0
- package/plugins/trailhead/hooks/mod/work-state-prompt.ts +69 -0
- package/plugins/trailhead/hooks/mod/work-state-view.ts +110 -0
- package/plugins/trailhead/hooks/trailhead-bash-guards.js +140 -0
- package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +233 -0
- package/plugins/trailhead/hooks/trailhead-body-guard.js +145 -61
- package/plugins/trailhead/hooks/trailhead-body-guard.test.js +224 -3
- package/plugins/trailhead/hooks/trailhead-commit-guard.js +41 -14
- package/plugins/trailhead/hooks/trailhead-commit-guard.test.js +2 -1
- package/plugins/trailhead/hooks/trailhead-install-guard.js +187 -42
- package/plugins/trailhead/hooks/trailhead-install-guard.test.js +182 -2
- package/plugins/trailhead/hooks/trailhead-mockup-link-guard.js +48 -11
- package/plugins/trailhead/hooks/trailhead-mockup-link-guard.test.js +75 -3
- package/plugins/trailhead/hooks/trailhead-search-guard.js +59 -13
- package/plugins/trailhead/hooks/trailhead-search-guard.test.js +68 -3
- package/plugins/trailhead/hooks/trailhead-secret-guard.js +541 -57
- package/plugins/trailhead/hooks/trailhead-secret-guard.test.js +1000 -4
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +171 -6
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +170 -0
- package/plugins/trailhead/skills/_shared/choices.md +7 -3
- package/plugins/trailhead/skills/_shared/configuration-reference.md +15 -3
- package/plugins/trailhead/skills/_shared/load-first.md +3 -3
- package/plugins/trailhead/skills/_shared/multi-map.md +3 -3
- package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
- package/plugins/trailhead/skills/_shared/substrate-commands.md +22 -11
- package/plugins/trailhead/skills/_shared/substrate.md +6 -4
- package/plugins/trailhead/skills/_shared/teamwork.md +17 -6
- package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +17 -9
- package/plugins/trailhead/skills/_shared/techniques/code-review.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/debug.md +2 -0
- package/plugins/trailhead/skills/_shared/techniques/prototype.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/research.md +4 -2
- package/plugins/trailhead/skills/_shared/techniques/verify.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques.md +12 -2
- package/plugins/trailhead/skills/trailhead/SKILL.md +3 -3
- package/plugins/trailhead/skills/trailhead-capture/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-capture/references/capture.md +2 -2
- package/plugins/trailhead/skills/trailhead-chart/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-chart/references/charting.md +1 -1
- package/plugins/trailhead/skills/trailhead-manage/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +1 -1
- package/plugins/trailhead/skills/trailhead-view/SKILL.md +8 -6
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +16 -13
- package/plugins/trailhead/skills/trailhead-work/references/auto.md +6 -4
- package/plugins/trailhead/skills/trailhead-work/references/executor-brief.md +29 -0
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +28 -10
- package/plugins/trailhead/templates/trailhead-base-preflight.js +305 -0
- package/plugins/trailhead/templates/trailhead-commit-msg +38 -102
- package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +58 -27
- package/plugins/trailhead/templates/trailhead-commit-msg.cjs +114 -0
- package/plugins/trailhead/templates/trailhead-dashboard.js +340 -0
- package/plugins/trailhead/templates/trailhead-isolation-answer.js +227 -0
- package/plugins/trailhead/templates/trailhead-isolation-copy.js +239 -0
- package/plugins/trailhead/templates/trailhead-issue-body.js +459 -0
- package/plugins/trailhead/templates/trailhead-report-status.js +274 -0
- package/plugins/trailhead/templates/trailhead-uat-backfill.js +114 -0
- package/plugins/trailhead/templates/trailhead-uat-setup.js +385 -0
- package/plugins/trailhead/templates/trailhead-work-state.js +217 -0
- package/plugins/trailhead/tests/map-pane-ui.test.ts +1921 -0
- package/plugins/trailhead/tests/map-pane-view.test.ts +853 -0
- package/plugins/trailhead/tests/mod.test.ts +14 -0
- package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +207 -0
- package/plugins/trailhead/tests/work-state-prompt.test.ts +109 -0
- package/plugins/trailhead/tests/work-state-ui.test.ts +365 -0
- package/plugins/trailhead/tests/work-state-view.test.ts +110 -0
- package/plugins/trailhead/types/index.d.ts +103 -0
package/bin/trailhead.test.js
CHANGED
|
@@ -118,11 +118,41 @@ ok('codex: commit-msg template projected', fs.existsSync(codexTemplate));
|
|
|
118
118
|
ok('codex: commit-msg template is verbatim (not converted)',
|
|
119
119
|
fs.readFileSync(codexTemplate, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-commit-msg'), 'utf8'));
|
|
120
120
|
|
|
121
|
+
// #257: the hook logic ships as a .cjs beside the sh wrapper.
|
|
122
|
+
const codexCommitMsgCjs = path.join(codexDir, 'skills', 'trailhead', 'templates', 'trailhead-commit-msg.cjs');
|
|
123
|
+
ok('codex: commit-msg .cjs logic projected', fs.existsSync(codexCommitMsgCjs));
|
|
124
|
+
ok('codex: commit-msg .cjs logic is verbatim (not converted)',
|
|
125
|
+
fs.readFileSync(codexCommitMsgCjs, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-commit-msg.cjs'), 'utf8'));
|
|
126
|
+
|
|
121
127
|
// #185: the commit-msg hook sync script ships wholesale with templates/ too.
|
|
122
128
|
const codexCommitMsgSync = path.join(codexDir, 'skills', 'trailhead', 'templates', 'trailhead-commit-msg-sync.js');
|
|
123
129
|
ok('codex: commit-msg-sync script projected', fs.existsSync(codexCommitMsgSync));
|
|
124
130
|
ok('codex: commit-msg-sync script is byte-identical to source',
|
|
125
131
|
fs.readFileSync(codexCommitMsgSync, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-commit-msg-sync.js'), 'utf8'));
|
|
132
|
+
// #205: the work state helper rides in templates/ on Codex too, executable.
|
|
133
|
+
const codexWorkState = path.join(codexDir, 'skills', 'trailhead', 'templates', 'trailhead-work-state.js');
|
|
134
|
+
ok('codex: work-state helper projected', fs.existsSync(codexWorkState));
|
|
135
|
+
ok('codex: work-state helper is byte-identical to source',
|
|
136
|
+
fs.readFileSync(codexWorkState, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-work-state.js'), 'utf8'));
|
|
137
|
+
ok('codex: work-state helper keeps its executable bit', (fs.statSync(codexWorkState).mode & 0o111) !== 0);
|
|
138
|
+
// #242: the isolation copy helper rides in templates/ on Codex too, executable.
|
|
139
|
+
const codexIsolationCopy = path.join(codexDir, 'skills', 'trailhead', 'templates', 'trailhead-isolation-copy.js');
|
|
140
|
+
ok('codex: isolation-copy helper projected', fs.existsSync(codexIsolationCopy));
|
|
141
|
+
ok('codex: isolation-copy helper is byte-identical to source',
|
|
142
|
+
fs.readFileSync(codexIsolationCopy, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-isolation-copy.js'), 'utf8'));
|
|
143
|
+
ok('codex: isolation-copy helper keeps its executable bit', (fs.statSync(codexIsolationCopy).mode & 0o111) !== 0);
|
|
144
|
+
// #243: the stale-base preflight helper rides in templates/ on Codex too, executable.
|
|
145
|
+
const codexBasePreflight = path.join(codexDir, 'skills', 'trailhead', 'templates', 'trailhead-base-preflight.js');
|
|
146
|
+
ok('codex: base-preflight helper projected', fs.existsSync(codexBasePreflight));
|
|
147
|
+
ok('codex: base-preflight helper is byte-identical to source',
|
|
148
|
+
fs.readFileSync(codexBasePreflight, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-base-preflight.js'), 'utf8'));
|
|
149
|
+
ok('codex: base-preflight helper keeps its executable bit', (fs.statSync(codexBasePreflight).mode & 0o111) !== 0);
|
|
150
|
+
// #252: the UAT setup helper rides in templates/ on Codex too, executable.
|
|
151
|
+
const codexUatSetup = path.join(codexDir, 'skills', 'trailhead', 'templates', 'trailhead-uat-setup.js');
|
|
152
|
+
ok('codex: uat-setup helper projected', fs.existsSync(codexUatSetup));
|
|
153
|
+
ok('codex: uat-setup helper is byte-identical to source',
|
|
154
|
+
fs.readFileSync(codexUatSetup, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-uat-setup.js'), 'utf8'));
|
|
155
|
+
ok('codex: uat-setup helper keeps its executable bit', (fs.statSync(codexUatSetup).mode & 0o111) !== 0);
|
|
126
156
|
// Il sync del label guard (migrazione all'if a livello di job) viaggia con templates/.
|
|
127
157
|
const codexLabelGuardSync = path.join(codexDir, 'skills', 'trailhead', 'templates', 'trailhead-label-guard-sync.js');
|
|
128
158
|
ok('codex: label-guard-sync script is byte-identical to source', fs.existsSync(codexLabelGuardSync) &&
|
|
@@ -134,6 +164,18 @@ ok('codex: audit-walk script is byte-identical to source',
|
|
|
134
164
|
fs.readFileSync(codexAuditWalk, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-audit-walk.js'), 'utf8'));
|
|
135
165
|
ok('codex: audit-walk script keeps its executable bit', (fs.statSync(codexAuditWalk).mode & 0o111) !== 0);
|
|
136
166
|
|
|
167
|
+
// #253: the Resolve bookkeeping helpers ride in templates/ on Codex too, executable,
|
|
168
|
+
// and find the secret-guard module from their installed location.
|
|
169
|
+
for (const helper of ['trailhead-issue-body.js', 'trailhead-dashboard.js']) {
|
|
170
|
+
const projected = path.join(codexDir, 'skills', 'trailhead', 'templates', helper);
|
|
171
|
+
ok(`codex: ${helper} projected`, fs.existsSync(projected));
|
|
172
|
+
ok(`codex: ${helper} is byte-identical to source`,
|
|
173
|
+
fs.readFileSync(projected, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', helper), 'utf8'));
|
|
174
|
+
ok(`codex: ${helper} keeps its executable bit`, (fs.statSync(projected).mode & 0o111) !== 0);
|
|
175
|
+
}
|
|
176
|
+
ok('codex: the installed issue-body helper finds the secret guard',
|
|
177
|
+
require(path.join(codexDir, 'skills', 'trailhead', 'templates', 'trailhead-issue-body.js')).loadGuard() !== null);
|
|
178
|
+
|
|
137
179
|
ok('codex: no commands dir', !fs.existsSync(path.join(codexDir, 'commands')));
|
|
138
180
|
ok('codex: no settings.json file', !fs.existsSync(path.join(codexDir, 'settings.json')));
|
|
139
181
|
// #46: per-verb discoverability now projects one Codex SKILL per verb
|
|
@@ -143,6 +185,8 @@ ok('codex: no settings.json file', !fs.existsSync(path.join(codexDir, 'settings.
|
|
|
143
185
|
// dir, so its discoverability is the cluster itself; use a non-colliding verb.
|
|
144
186
|
ok('codex: per-verb skills/trailhead-bug/SKILL.md exists', fs.existsSync(path.join(codexDir, 'skills', 'trailhead-bug', 'SKILL.md')));
|
|
145
187
|
ok('codex: trailhead-bug SKILL.md frontmatter is at byte 0', fs.readFileSync(path.join(codexDir, 'skills', 'trailhead-bug', 'SKILL.md'), 'utf8').startsWith('---\nname: trailhead-bug\n'));
|
|
188
|
+
// #208: /trailhead:panel is answered by the Claude Code mod only: no Codex skill for it.
|
|
189
|
+
ok('codex: no per-verb skill for the mod-only panel verb', !fs.existsSync(path.join(codexDir, 'skills', 'trailhead-panel')));
|
|
146
190
|
ok('codex: trailhead-bug skill delegates to $trailhead bug', fs.readFileSync(path.join(codexDir, 'skills', 'trailhead-bug', 'SKILL.md'), 'utf8').includes('$trailhead bug'));
|
|
147
191
|
// #190: audit verb projected per-verb on Codex, description from commands/audit.md.
|
|
148
192
|
const auditCmdSrc190 = fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'commands', 'audit.md'), 'utf8');
|
|
@@ -202,6 +246,28 @@ for (const cl of ['trailhead', 'trailhead-chart', 'trailhead-work', 'trailhead-v
|
|
|
202
246
|
ok(`source: ${cl}/SKILL.md does NOT duplicate the core-list signature line`, !fs.readFileSync(path.join(sourceSkillsDir, cl, 'SKILL.md'), 'utf8').includes(loadFirstSignature));
|
|
203
247
|
}
|
|
204
248
|
|
|
249
|
+
// --- #237: the shared core is loaded by ABSOLUTE path with the Read tool -----
|
|
250
|
+
const clusters237 = ['trailhead', 'trailhead-chart', 'trailhead-work', 'trailhead-view', 'trailhead-capture', 'trailhead-manage'];
|
|
251
|
+
const loadFirst237 = fs.readFileSync(loadFirstSourcePath, 'utf8');
|
|
252
|
+
for (const cl of clusters237) {
|
|
253
|
+
const src = fs.readFileSync(path.join(sourceSkillsDir, cl, 'SKILL.md'), 'utf8');
|
|
254
|
+
const start = src.indexOf('## Load first');
|
|
255
|
+
const rest = src.slice(start + 3);
|
|
256
|
+
const nextH = rest.search(/\n## /);
|
|
257
|
+
const para = start < 0 ? '' : (nextH < 0 ? rest : rest.slice(0, nextH));
|
|
258
|
+
ok(`#237: ${cl} Load first names the base directory`, /base directory/i.test(para));
|
|
259
|
+
ok(`#237: ${cl} Load first requires an absolute path`, /absolute path/i.test(para));
|
|
260
|
+
ok(`#237: ${cl} Load first requires the Read tool`, /Read tool/.test(para));
|
|
261
|
+
ok(`#237: ${cl} Load first has no bare relative "read ../_shared/load-first.md"`, !/read `\.\.\/_shared\/load-first\.md`/i.test(para));
|
|
262
|
+
ok(`#237: ${cl} SKILL.md has no shell read of a relative _shared/ path`, !/\b(cat|head|tail|sed\s+-n\s+\S+)\s+(\.\.\/)*_shared\//.test(src));
|
|
263
|
+
ok(`#237: ${cl} SKILL.md has no cd-and-read compound`, !/\bcd\s+\S+\s*&&\s*(cat|head|tail|sed|grep)\b/.test(src));
|
|
264
|
+
}
|
|
265
|
+
ok('#237: load-first.md states the absolute-path rule with the Read tool', /absolute path/i.test(loadFirst237) && /Read tool/.test(loadFirst237));
|
|
266
|
+
ok('#237: load-first.md resolves <lib> and <templates> to absolute paths', loadFirst237.includes('`<lib>` (an absolute path') && loadFirst237.includes('`<templates>` (an absolute path)'));
|
|
267
|
+
ok('#237: load-first.md covers every other ../_shared/ or references/ path', loadFirst237.includes('references/') && /every other/i.test(loadFirst237));
|
|
268
|
+
ok('#237: load-first.md no longer says paths are relative to the cluster SKILL.md', !loadFirst237.includes('relative to the cluster `SKILL.md`') && !loadFirst237.includes('relative to the cluster SKILL.md that'));
|
|
269
|
+
ok('#237: load-first.md has no shell read of a relative _shared/ path', !/\b(cat|head|tail|sed\s+-n\s+\S+)\s+(\.\.\/)*_shared\//.test(loadFirst237) && !/\bcd\s+\S+\s*&&\s*(cat|head|tail|sed|grep)\b/.test(loadFirst237));
|
|
270
|
+
|
|
205
271
|
// --- #146: single-sourced ticket-language directive (anti-drift) -------------
|
|
206
272
|
// The standing ticket-language rule (write Issue prose + commit bodies in
|
|
207
273
|
// config.ticket.language, decoupled from the chat language) lives in ONE place,
|
|
@@ -279,6 +345,27 @@ ok('source: capture.md offers /trailhead:quick as the lighter alternative, after
|
|
|
279
345
|
ok('source: capture.md says quick is offered alongside work, never in its place', captureSource.includes('never in its place'));
|
|
280
346
|
ok('source: capture.md keeps /trailhead:work the anchor even for a whiteboard capture', /whiteboard[\s\S]{0,200}the anchor stays `\/trailhead:work <n>`/.test(captureSource));
|
|
281
347
|
|
|
348
|
+
// --- #246: `bug --of <ticket>` inherits the origin's destination ------------
|
|
349
|
+
// Source-level invariant: capture.md's `bug` bullet makes `--of` inherit the origin
|
|
350
|
+
// ticket's map (open) or the whiteboard without the which-map ask, checks the origin
|
|
351
|
+
// map's state before inheriting, falls back to the Map or whiteboard rule on a closed
|
|
352
|
+
// map / absent or contradictory labels, and names the inherited destination and the
|
|
353
|
+
// origin (each by name and number) in the confirmation line. The Map or whiteboard
|
|
354
|
+
// paragraph cross-references the exception (single source stays in the `bug` bullet).
|
|
355
|
+
const captureBugBullet = (captureSource.match(/^- \*\*`bug`\*\*.*$/m) || [''])[0];
|
|
356
|
+
const captureMapParagraph = (captureSource.match(/^\*\*Map or whiteboard:\*\*.*$/m) || [''])[0];
|
|
357
|
+
ok('source: capture.md `bug --of` inherits the origin destination instead of asking', /\*\*`--of` also inherits the origin's destination instead of asking/.test(captureBugBullet));
|
|
358
|
+
ok('source: capture.md `bug --of` lands on an OPEN origin map with no ask, and a closed one falls back',
|
|
359
|
+
/\*\*open\*\* -> land on map `<n>`[\s\S]{0,120}no ask; \*\*closed\*\* -> fall back/.test(captureBugBullet));
|
|
360
|
+
ok('source: capture.md `bug --of` lands on the whiteboard for a whiteboard origin, no ask, with the dashboard refresh',
|
|
361
|
+
/carries `trailhead:whiteboard`[\s\S]{0,80}land on the whiteboard, no ask \(dashboard refresh/.test(captureBugBullet));
|
|
362
|
+
ok('source: capture.md `bug --of` checks the origin map state (--json state) before inheriting', /gh issue view <n> --json state/.test(captureBugBullet));
|
|
363
|
+
ok('source: capture.md `bug --of` falls back to the Map or whiteboard rule on closed map, absent or contradictory labels',
|
|
364
|
+
/[Ff]all back to the existing Map or whiteboard rule[\s\S]{0,600}closed[\s\S]{0,300}neither label[\s\S]{0,300}contradictory/.test(captureBugBullet));
|
|
365
|
+
ok('source: capture.md `bug --of` confirmation names the inherited destination and the origin, each by name and number',
|
|
366
|
+
/[Cc]onfirmation line[\s\S]{0,300}inherited[\s\S]{0,200}by name and number[\s\S]{0,200}origin[\s\S]{0,100}by name and number/.test(captureBugBullet));
|
|
367
|
+
ok('source: capture.md Map or whiteboard paragraph cross-references the `bug --of` ask exception', /`bug --of`[\s\S]{0,200}skip/.test(captureMapParagraph));
|
|
368
|
+
|
|
282
369
|
// --- #179: `quick <n>` on a map ticket is never "off the map" ----------------
|
|
283
370
|
// Source-level invariant: `quick <n>` on an EXISTING ticket does not leave the
|
|
284
371
|
// map (it keeps its trailhead:map-<n> label and sub-issue edge, and dependent-
|
|
@@ -512,6 +599,53 @@ const workSkillSource = fs.readFileSync(path.join(sourceSkillsDir, 'trailhead-wo
|
|
|
512
599
|
ok('source: trailhead-work SKILL.md out-of-scope close drops the native sub-issue edge',
|
|
513
600
|
/out of scope\*\* \(label, then \*\*drop its native sub-issue edge\*\*/.test(workSkillSource));
|
|
514
601
|
|
|
602
|
+
// The text of a structural unit of a source, from its anchor: a missing anchor fails loudly.
|
|
603
|
+
const unitFrom = (source, anchor, endPattern) => {
|
|
604
|
+
const start = source.indexOf(anchor);
|
|
605
|
+
if (start === -1) throw new Error(`anchor not found in source: ${anchor}`);
|
|
606
|
+
const rest = source.slice(start);
|
|
607
|
+
const end = rest.slice(anchor.length).search(endPattern);
|
|
608
|
+
return end === -1 ? rest : rest.slice(0, anchor.length + end);
|
|
609
|
+
};
|
|
610
|
+
// work step 1: its paragraph, up to the next numbered step or blank line.
|
|
611
|
+
const workStep1 = unitFrom(workSkillSource, '1. Load the **map**', /\n(?:\d+\. |\s*\n)/);
|
|
612
|
+
// quick's isolation bullet: up to the end of its line.
|
|
613
|
+
const quickIsolationBullet = unitFrom(workSkillSource, '- **Same isolation setup as `work`.**', /\n/);
|
|
614
|
+
// The rule is stated once, in multi-map.md; the work places point to it.
|
|
615
|
+
const multiMapSource = fs.readFileSync(path.join(sourceSkillsDir, '_shared', 'multi-map.md'), 'utf8');
|
|
616
|
+
const namedTicketMapSentence = /a named ticket that carries a `trailhead:map-<n>` label makes that map the active map \(write `\.trailhead\/active-map`\); a whiteboard ticket leaves the active map alone/i;
|
|
617
|
+
const namedTicketMapPointer = /named ticket's map becomes the active map[\s\S]*whiteboard ticket leaves it alone[\s\S]*multi-map\.md/i;
|
|
618
|
+
ok('source: multi-map.md Active map paragraph states the named-ticket rule',
|
|
619
|
+
namedTicketMapSentence.test(unitFrom(multiMapSource, '**Active map.**', /\n/)));
|
|
620
|
+
ok('source: trailhead-work SKILL.md work step 1 points to the named-ticket active-map rule',
|
|
621
|
+
/its map becomes the active map, a whiteboard ticket leaves it alone[\s\S]*multi-map\.md/i.test(workStep1));
|
|
622
|
+
ok('source: trailhead-work SKILL.md quick isolation bullet points to the named-ticket active-map rule',
|
|
623
|
+
namedTicketMapPointer.test(quickIsolationBullet));
|
|
624
|
+
|
|
625
|
+
// --- #234: the ticket-start scope confirm is a host menu, not free text ------
|
|
626
|
+
const workStep3 = unitFrom(workSkillSource, '3. **Brief the ticket and confirm scope', /\n(?:\d+\. |\s*\n)/);
|
|
627
|
+
const quickBriefBullet = unitFrom(workSkillSource, '- **Briefs and confirms scope first', /\n/);
|
|
628
|
+
const choicesScopeSource = fs.readFileSync(path.join(sourceSkillsDir, "_shared", "choices.md"), "utf8");
|
|
629
|
+
ok('source: work step 3 poses the scope confirm as an AskUserQuestion menu, Proceed (Recommended) first, Change something second',
|
|
630
|
+
/`AskUserQuestion`[\s\S]{0,300}\*\*Proceed\*\* \(Recommended\)[\s\S]{0,200}\*\*Change something\*\*/.test(workStep3));
|
|
631
|
+
ok('source: work step 3 offers the delegate option on the scope confirm',
|
|
632
|
+
/\*\*delegate\*\*[\s\S]{0,200}choices\.md/i.test(workStep3));
|
|
633
|
+
ok('source: work step 3 divergent change is a menu (as-is vs separate ticket) without delegate',
|
|
634
|
+
/diverges[\s\S]{0,500}`AskUserQuestion`[\s\S]{0,300}without the delegate option/.test(workStep3));
|
|
635
|
+
ok('source: work step 3 has a plain-text numbered list fallback for hosts without a question tool (Codex)',
|
|
636
|
+
/no structured question tool[\s\S]{0,120}plain-text numbered list/.test(workStep3));
|
|
637
|
+
ok('source: work step 3 keeps the auto sentence (proceeds without stopping to brief)',
|
|
638
|
+
/`auto` proceeds \(takes the delegate option\) without stopping to brief/.test(workStep3));
|
|
639
|
+
ok('source: work step 3 no longer asks the change question as free text',
|
|
640
|
+
!/ask whether they want to change anything/.test(workStep3));
|
|
641
|
+
ok('source: quick brief bullet poses the same menu and keeps a one-line note for quick "<text>"',
|
|
642
|
+
/`AskUserQuestion`/.test(quickBriefBullet) && /\*\*Proceed\*\* \(Recommended\)/.test(quickBriefBullet) &&
|
|
643
|
+
/one-line note/.test(quickBriefBullet) && !/one-line confirmation/.test(quickBriefBullet));
|
|
644
|
+
ok('source: choices.md call-site table has the ticket-start scope confirm row (delegate yes, defer no)',
|
|
645
|
+
/\| Ticket-start scope confirm \(`work`\/`quick` brief\) \| Yes[^|]*\| No[^|]*\|/.test(choicesScopeSource));
|
|
646
|
+
ok('source: choices.md call-site table has the divergent-change row (delegate no, defer no)',
|
|
647
|
+
/\| Divergent change at the brief \(as-is vs separate ticket\) \| No[^|]*\| No[^|]*\|/.test(choicesScopeSource));
|
|
648
|
+
|
|
515
649
|
const inboxSource = fs.readFileSync(path.join(sourceSkillsDir, 'trailhead-chart', 'references', 'inbox.md'), 'utf8');
|
|
516
650
|
ok('source: inbox.md out-of-scope close drops the native sub-issue edge',
|
|
517
651
|
/`trailhead:out-of-scope`, \*\*drop its native sub-issue edge\*\*/.test(inboxSource));
|
|
@@ -549,7 +683,7 @@ for (const phrase of [
|
|
|
549
683
|
'**wait for an explicit answer**',
|
|
550
684
|
'**silence or an ambiguous reply re-asks**',
|
|
551
685
|
'**One-path skip**',
|
|
552
|
-
'**
|
|
686
|
+
'**states the reason in one line**',
|
|
553
687
|
'VERIFY',
|
|
554
688
|
'only for the steps it cannot perform',
|
|
555
689
|
'runs unchanged',
|
|
@@ -634,6 +768,11 @@ ok('source: auto.md states the drive-mode-ask outcomes together (grouped, not sc
|
|
|
634
768
|
const choicesSourcePath = path.join(sourceSkillsDir, '_shared', 'choices.md');
|
|
635
769
|
const choicesSource = fs.readFileSync(choicesSourcePath, 'utf8');
|
|
636
770
|
ok('source: choices.md call-site table carries the drive-mode ask row', /\|[^\n]*[Dd]rive-mode ask[^\n]*\|/.test(choicesSource));
|
|
771
|
+
// Formato della scelta: fino a 4 opzioni reali -> menu; oltre -> lista a lettere in prosa.
|
|
772
|
+
ok('source: choices.md routes a choice of up to 4 real options to the AskUserQuestion menu',
|
|
773
|
+
/\*\*Which format\.\*\*[\s\S]{0,400}up to 4 real options[\s\S]{0,200}`AskUserQuestion` menu/.test(choicesSource));
|
|
774
|
+
ok('source: choices.md routes a choice of more than 4 real options to a lettered prose list',
|
|
775
|
+
/\*\*Which format\.\*\*[\s\S]{0,800}more than 4 real options[\s\S]{0,200}lettered list/.test(choicesSource));
|
|
637
776
|
|
|
638
777
|
// --- #178 (3/4): acceptance.browser off keeps AI-driven UAT via commands ----
|
|
639
778
|
const configReferencePath = path.join(sourceSkillsDir, '_shared', 'configuration-reference.md');
|
|
@@ -685,22 +824,124 @@ ok('source: auto.md Run-end summary section exists', runEndSummarySection.length
|
|
|
685
824
|
ok('source: auto.md Run-end summary "what remains" list includes human-only UAT steps',
|
|
686
825
|
/what remains[\s\S]{0,400}human-only UAT steps/.test(runEndSummarySection));
|
|
687
826
|
|
|
827
|
+
// --- #254: skip option at the drive-mode ask (a waiver, not a deferral) -----
|
|
828
|
+
// The waiver's operational contract is single-sourced in the Drive-mode bullet
|
|
829
|
+
// (acceptance-testing.md); the other call-sites carry only the routing /
|
|
830
|
+
// closure consequence and name the bullet.
|
|
831
|
+
const driveModeMarkerLine = driveModeSpan.split('\n')[0] || '';
|
|
832
|
+
ok('source: Drive-mode marker line names the skip option alongside AI-driven / human-driven',
|
|
833
|
+
driveModeMarkerLine.includes('AI-driven') && driveModeMarkerLine.includes('human-driven') && driveModeMarkerLine.includes('skip (waive acceptance)'));
|
|
834
|
+
const skipSubBullet = driveModeSpan.split('\n').find((l) => l.startsWith(' - **Skip (waive acceptance).**')) || '';
|
|
835
|
+
ok('source: Drive-mode span carries the "Skip (waive acceptance)" sub-bullet', skipSubBullet.length > 0);
|
|
836
|
+
for (const phrase of [
|
|
837
|
+
'waived by the user (unconfirmed)',
|
|
838
|
+
'never a pass',
|
|
839
|
+
'the Pending-UAT guard does not fire',
|
|
840
|
+
'no follow-up',
|
|
841
|
+
'The delegate option never picks skip',
|
|
842
|
+
'`auto` never picks skip',
|
|
843
|
+
'UAT of:',
|
|
844
|
+
'closes as waived',
|
|
845
|
+
'resolution comment',
|
|
846
|
+
'all three start paths',
|
|
847
|
+
]) {
|
|
848
|
+
ok(`source: Skip sub-bullet pins "${phrase}"`, skipSubBullet.includes(phrase));
|
|
849
|
+
}
|
|
850
|
+
const oneAskLine = driveModeSpan.split('\n').find((l) => l.startsWith(' - **One-path skip**')) || '';
|
|
851
|
+
ok('source: one-path skip no longer suppresses the ask outright (drops only the AI-driven option)',
|
|
852
|
+
oneAskLine.includes('drops only the AI-driven option') && oneAskLine.includes('human-driven or skip') && !oneAskLine.includes('skip the ask'));
|
|
853
|
+
ok('source: one-path skip still states the reason in one line, naming the blocking class',
|
|
854
|
+
oneAskLine.includes('**states the reason in one line**') && oneAskLine.includes('blocking class') && !oneAskLine.includes('prefaced by'));
|
|
855
|
+
ok('source: on the reduced ask (no performable step) the delegate option resolves to human-driven, never skip',
|
|
856
|
+
/delegate option resolves to human-driven \(never skip\)/.test(oneAskLine));
|
|
857
|
+
ok('source: Pending-UAT bullet distinguishes a waiver from a deferral', pendingUatLine.includes('a waiver is not a deferral'));
|
|
858
|
+
ok('source: Pending-UAT bullet pins the waiver exception (picked skip, recorded as unconfirmed, nothing opened)',
|
|
859
|
+
/The one exception is an explicit waiver[^.]*\. The human picked skip at the drive-mode ask[^.]*unconfirmed in `VERIFY`[^.]*nothing is opened/.test(pendingUatLine));
|
|
860
|
+
const waiverResolveRe = /resolv(e|ing) on a confirmed pass, or on an explicit waiver if the user skips at the ask/;
|
|
861
|
+
ok('source: Pending-UAT bullet option (a) resolves on a confirmed pass or an explicit waiver',
|
|
862
|
+
waiverResolveRe.test(pendingUatLine) && !pendingUatLine.includes('only on a confirmed pass'));
|
|
863
|
+
ok('source: both ticket-engines Resolve lines let option (a) resolve on a confirmed pass or an explicit waiver',
|
|
864
|
+
guardLines254Early().length === 2 && guardLines254Early().every((l) => waiverResolveRe.test(l) && !l.includes('only on a confirmed pass')));
|
|
865
|
+
ok('source: with-no-ticket preflight-then-ask phrase still occurs exactly twice in ticket-engines.md',
|
|
866
|
+
ticketEnginesSource.split('with no ticket (the stale-base preflight first, then the UAT setup, then the drive-mode ask').length - 1 === 2);
|
|
867
|
+
ok('source: both Verify steps let the user skip, recorded as waived',
|
|
868
|
+
verifyStepLines.length === 2 && verifyStepLines.every((l) => /or skip \(waive acceptance\), recorded as waived/.test(l)));
|
|
869
|
+
ok('source: techniques.md index row and auto.md lead-in name skip',
|
|
870
|
+
/ask who drives \(AI or human, or skip\)/.test(fs.readFileSync(path.join(sourceSkillsDir, '_shared', 'techniques.md'), 'utf8')) &&
|
|
871
|
+
autoSource.includes('drive-mode ask (AI-driven, human-driven, or skip)'));
|
|
872
|
+
|
|
873
|
+
function guardLines254Early() { return ticketEnginesSource.split('\n').filter((l) => l.includes('Pending-UAT guard (before the close)')); }
|
|
874
|
+
const guardLines254 = ticketEnginesSource.split('\n').filter((l) => l.includes('Pending-UAT guard (before the close)'));
|
|
875
|
+
ok('source: both Pending-UAT guard lines say the guard does not fire when acceptance was explicitly waived',
|
|
876
|
+
guardLines254.length === 2 && guardLines254.every((l) => l.includes('explicitly waived')));
|
|
877
|
+
ok('source: both Resolve steps state the waiver in the resolution comment',
|
|
878
|
+
guardLines254.length === 2 && guardLines254.every((l) => /resolution comment states the waiver/.test(l)));
|
|
879
|
+
ok('source: the UAT follow-ups section closes on a confirmed pass or an explicit waiver',
|
|
880
|
+
uatFollowupSection.includes('confirmed pass or an explicit waiver') && uatFollowupSection.includes('closes as waived'));
|
|
881
|
+
const driveRowLine = choicesSource.split('\n').find((l) => /^\|[^\n]*[Dd]rive-mode ask/.test(l)) || '';
|
|
882
|
+
ok('source: choices.md drive-mode row names skip and states delegate never picks it',
|
|
883
|
+
driveRowLine.includes('skip') && /delegate never picks skip/.test(driveRowLine) && driveRowLine.includes('reduced ask') && driveRowLine.includes('human-driven'));
|
|
884
|
+
ok('source: configuration-reference.md acceptance.browser paragraph names skip alongside AI-driven / human-driven',
|
|
885
|
+
/\*\*acceptance\.browser\.\*\*[\s\S]{0,200}human-driven,? or skip/.test(configReferenceSource));
|
|
886
|
+
ok('source: auto.md states auto never picks skip', /never picks skip[^\n]*waiving acceptance/.test(autoSource));
|
|
887
|
+
|
|
888
|
+
// Docs mirror: README, README.it, site (waiver recorded as unconfirmed; no unconditional "never closes").
|
|
889
|
+
ok('README.md acceptance testing section names the waiver (skip) recorded as unconfirmed',
|
|
890
|
+
/\*\*Skip is a waiver, not a deferral\*\*[^\n]{0,80}`waived by the user \(unconfirmed\)`, never as a pass/.test(readmeSource));
|
|
891
|
+
ok('README.md no-performable-step case asks human-driven or skip', /human-driven\*\* or \*\*skip\*\*/.test(readmeSource));
|
|
892
|
+
ok('README.md point 6 allows an explicit waiver', /never closes on a UAT nobody ran unless you explicitly waive it/.test(readmeSource));
|
|
893
|
+
const siteAcceptanceSource = fs.readFileSync(path.join(repoRoot, 'site', 'src', 'content', 'docs', 'docs', 'acceptance-testing.md'), 'utf8');
|
|
894
|
+
ok('docs: auto never-picks-skip antecedent is the auto run, in all three docs',
|
|
895
|
+
/listed in the run's summary; `\/trailhead:auto` never picks skip/.test(readmeSource) && /listed in the run's summary; `\/trailhead:auto` never picks skip/.test(siteAcceptanceSource) && /elencato nel riepilogo della run; `\/trailhead:auto` non sceglie mai skip/.test(readmeItSource));
|
|
896
|
+
ok('README.md build row and acceptance.browser row name skip',
|
|
897
|
+
/🔨 `build`[\s\S]{0,500}or skip/.test(readmeSource) && /`acceptance\.browser`[\s\S]{0,300}or skip/.test(readmeSource));
|
|
898
|
+
ok('README.it.md acceptance testing section names the waiver (skip) recorded as unconfirmed',
|
|
899
|
+
/\*\*Skip è una rinuncia, non un rinvio\*\*[^\n]{0,160}`waived by the user \(unconfirmed\)`, cioè non confermata/.test(readmeItSource));
|
|
900
|
+
ok('README.it.md no-performable-step case asks human-driven o skip', /human-driven\*\* o \*\*skip\*\*/.test(readmeItSource));
|
|
901
|
+
ok('README.it.md point 6 allows an explicit waiver', /non si chiude mai su una UAT che nessuno ha eseguito, salvo che tu ci rinunci esplicitamente/.test(readmeItSource));
|
|
902
|
+
ok('README.it.md build row and acceptance.browser row name skip',
|
|
903
|
+
/🔨 `build`[\s\S]{0,500}o skip/.test(readmeItSource) && /`acceptance\.browser`[\s\S]{0,300}o skip/.test(readmeItSource));
|
|
904
|
+
ok('site acceptance-testing.md names the waiver (skip) recorded as unconfirmed', /\*\*Skip is a waiver, not a deferral\*\*[^\n]{0,80}`waived by the user \(unconfirmed\)`, never as a pass/.test(siteAcceptanceSource));
|
|
905
|
+
ok('site acceptance-testing.md no-performable-step case asks human-driven or skip', /human-driven\*\* or \*\*skip\*\*/.test(siteAcceptanceSource));
|
|
906
|
+
ok('site acceptance-testing.md point 6 allows an explicit waiver', /never closes on a UAT nobody ran unless you explicitly waive it/.test(siteAcceptanceSource));
|
|
907
|
+
ok('site ticket-types.md build row and configuration.mdx acceptance.browser row name skip',
|
|
908
|
+
/`build`[\s\S]{0,500}or skip/.test(ticketTypesSource) && /`acceptance\.browser`[\s\S]{0,300}or skip/.test(configurationMdxSource));
|
|
909
|
+
|
|
688
910
|
// --- codex hooks (#29) --------------------------------------------------------
|
|
689
911
|
const codexHooksJsonPath = path.join(codexDir, 'hooks.json');
|
|
690
912
|
ok('codex: hooks.json exists', fs.existsSync(codexHooksJsonPath));
|
|
691
913
|
const codexHooksJson = JSON.parse(fs.readFileSync(codexHooksJsonPath, 'utf8'));
|
|
692
914
|
const codexHooksJsonStr = JSON.stringify(codexHooksJson);
|
|
693
|
-
ok('codex: hooks.json registers
|
|
694
|
-
(
|
|
695
|
-
ok('codex: hooks.json registers
|
|
915
|
+
ok('codex: hooks.json registers the merged Bash guards under PreToolUse', (codexHooksJson.hooks.PreToolUse || []).some((g) => g.matcher === 'Bash' &&
|
|
916
|
+
(g.hooks || []).some((h) => h.command.includes('trailhead-bash-guards.js') && h.command.endsWith(' --guards=commit,secret,install,body'))));
|
|
917
|
+
ok('codex: hooks.json registers none of the per-guard scripts on their own', !/trailhead-(commit|secret|install|search|body)-guard\.js/.test(codexHooksJsonStr));
|
|
918
|
+
ok('codex: the five guard modules the merged hook requires are still shipped (no search)',
|
|
919
|
+
['commit', 'secret', 'install', 'body'].every((n) => fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', `trailhead-${n}-guard.js`))) &&
|
|
920
|
+
fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'trailhead-bash-guards.js')));
|
|
696
921
|
ok('codex: hooks.json registers injection-scanner under PostToolUse', (codexHooksJson.hooks.PostToolUse || []).some((g) => (g.hooks || []).some((h) => h.command.includes('trailhead-issue-injection-scanner.js'))));
|
|
697
922
|
ok('codex: hooks.json registers check-update under SessionStart', (codexHooksJson.hooks.SessionStart || []).some((g) => (g.hooks || []).some((h) => h.command.includes('trailhead-check-update.js'))));
|
|
698
923
|
|
|
924
|
+
// #202: the mod (hooks/mod/register.tsx + the "modules" key) is Claude Code only;
|
|
925
|
+
// the Codex projection must carry neither.
|
|
926
|
+
function listFilesRecursive(dir) {
|
|
927
|
+
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
|
|
928
|
+
const full = path.join(dir, e.name);
|
|
929
|
+
return e.isDirectory() ? listFilesRecursive(full) : [full];
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
ok('codex: hooks.json has no "modules" key (mod is Claude-only, #202)', !('modules' in codexHooksJson));
|
|
933
|
+
ok('codex: no register.tsx under the install dir (#202)', !listFilesRecursive(codexDir).some((f) => path.basename(f) === 'register.tsx'));
|
|
934
|
+
ok('codex: no .ts or .tsx file anywhere under the install dir (#202)', !listFilesRecursive(codexDir).some((f) => f.endsWith('.ts') || f.endsWith('.tsx')));
|
|
935
|
+
|
|
699
936
|
ok('codex: skills/trailhead/hooks/trailhead-secret-guard.js exists', fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'trailhead-secret-guard.js')));
|
|
700
937
|
ok('codex: skills/trailhead/hooks/lib/commit-message-check.js exists (commit-guard require target)',
|
|
701
938
|
fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'commit-message-check.js')));
|
|
702
939
|
ok('codex: skills/trailhead/hooks/lib/gh-subcommand.js exists (body-guard/secret-guard require target, #153)',
|
|
703
940
|
fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'gh-subcommand.js')));
|
|
941
|
+
ok('codex: skills/trailhead/hooks/lib/body-sources.js exists (secret-guard require target, #210)',
|
|
942
|
+
fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'body-sources.js')));
|
|
943
|
+
ok('codex: skills/trailhead/hooks/lib/unmodelled.js exists (guards require target, #227)',
|
|
944
|
+
fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'unmodelled.js')));
|
|
704
945
|
ok('codex: skills/trailhead/hooks/lib/model-defaults-review.js exists (check-update require target, #186)',
|
|
705
946
|
fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'model-defaults-review.js')));
|
|
706
947
|
ok('codex: skills/trailhead/hooks/lib/model-defaults.json exists (#186)',
|
|
@@ -796,6 +1037,25 @@ ok('codex: every trailhead-*.toml is pin-less when models.codex.* is unset', (()
|
|
|
796
1037
|
return tomls.every((f) => !/^model = /m.test(fs.readFileSync(path.join(dir, f), 'utf8')));
|
|
797
1038
|
})());
|
|
798
1039
|
|
|
1040
|
+
// #250: the reviewing block (single source scripts/agent-blocks.md) is projected
|
|
1041
|
+
// verbatim into the two reviewing agents' TOMLs and into none of the other six.
|
|
1042
|
+
{
|
|
1043
|
+
const sync250 = require(path.join(repoRoot, 'scripts', 'sync-agent-blocks.js'));
|
|
1044
|
+
const reviewingBlock250 = sync250.parseSource(fs.readFileSync(path.join(repoRoot, 'scripts', 'agent-blocks.md'), 'utf8')).reviewing;
|
|
1045
|
+
ok('#250: the source defines a reviewing block', typeof reviewingBlock250 === 'string' && reviewingBlock250.length > 0);
|
|
1046
|
+
const agentsDir250 = path.join(unsetCodexHomeDir, 'agents');
|
|
1047
|
+
const reviewingNames250 = new Set(['trailhead-verify', 'trailhead-review']); // the code-review agent projects as trailhead-review.toml
|
|
1048
|
+
const tomls250 = fs.readdirSync(agentsDir250).filter((f) => /^trailhead-.*\.toml$/.test(f));
|
|
1049
|
+
ok('#250: all 8 TOMLs are checked', tomls250.length === 8);
|
|
1050
|
+
for (const f of tomls250) {
|
|
1051
|
+
const name = f.replace(/\.toml$/, '');
|
|
1052
|
+
const text = fs.readFileSync(path.join(agentsDir250, f), 'utf8');
|
|
1053
|
+
const has = typeof reviewingBlock250 === 'string' && reviewingBlock250.length > 0 && text.includes(reviewingBlock250);
|
|
1054
|
+
ok(`#250: ${f} ${reviewingNames250.has(name) ? 'carries' : 'does not carry'} the reviewing block verbatim`,
|
|
1055
|
+
has === reviewingNames250.has(name));
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
799
1059
|
// --- codex migration (#46): install over old prompt shims sweeps them --------
|
|
800
1060
|
const migCodexDir = mktmp();
|
|
801
1061
|
fs.mkdirSync(path.join(migCodexDir, 'prompts'), { recursive: true });
|
|
@@ -829,7 +1089,7 @@ ok('codex uninstall: cluster trailhead-view gone', !fs.existsSync(path.join(code
|
|
|
829
1089
|
ok('codex uninstall: hooks.json no longer contains trailhead commands', (() => {
|
|
830
1090
|
const h = JSON.parse(fs.readFileSync(codexHooksJsonPath, 'utf8'));
|
|
831
1091
|
const str = JSON.stringify(h);
|
|
832
|
-
return !str.includes('trailhead-
|
|
1092
|
+
return !str.includes('trailhead-bash-guards.js') && !/trailhead-(commit|secret|install|search|body)-guard\.js/.test(str) &&
|
|
833
1093
|
!str.includes('trailhead-issue-injection-scanner.js') && !str.includes('trailhead-check-update.js');
|
|
834
1094
|
})());
|
|
835
1095
|
|
|
@@ -858,17 +1118,31 @@ ok('claude: trailhead-work/../_shared/ticket-language.md resolves', fs.existsSyn
|
|
|
858
1118
|
|
|
859
1119
|
ok('claude: commands/trailhead/work.md exists', fs.existsSync(path.join(claudeDir, 'commands', 'trailhead', 'work.md')));
|
|
860
1120
|
ok('claude: commands/trailhead/audit.md projected', fs.existsSync(path.join(claudeDir, 'commands', 'trailhead', 'audit.md')));
|
|
1121
|
+
ok('claude: commands/trailhead/panel.md projected (the mod answers it)', fs.existsSync(path.join(claudeDir, 'commands', 'trailhead', 'panel.md')));
|
|
861
1122
|
ok('claude: commands/trailhead/auto.md exists', fs.existsSync(path.join(claudeDir, 'commands', 'trailhead', 'auto.md')));
|
|
862
1123
|
ok('claude: hooks/trailhead-commit-guard.js exists', fs.existsSync(path.join(claudeDir, 'hooks', 'trailhead-commit-guard.js')));
|
|
863
1124
|
ok('claude: hooks/lib/commit-message-check.js exists (commit-guard require target)',
|
|
864
1125
|
fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'commit-message-check.js')));
|
|
865
1126
|
ok('claude: hooks/lib/gh-subcommand.js exists (body-guard/secret-guard require target, #153)',
|
|
866
1127
|
fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'gh-subcommand.js')));
|
|
1128
|
+
ok('claude: hooks/lib/body-sources.js exists (secret-guard require target, #210)',
|
|
1129
|
+
fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'body-sources.js')));
|
|
1130
|
+
ok('claude: hooks/lib/unmodelled.js exists (guards require target, #227)',
|
|
1131
|
+
fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'unmodelled.js')));
|
|
867
1132
|
ok('claude: hooks/lib/model-defaults-review.js exists (check-update require target, #186)',
|
|
868
1133
|
fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'model-defaults-review.js')));
|
|
869
1134
|
ok('claude: hooks/lib/model-defaults.json exists (#186)',
|
|
870
1135
|
fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'model-defaults.json')));
|
|
871
1136
|
|
|
1137
|
+
// #202: the mod is not delivered by the npm installer yet.
|
|
1138
|
+
ok('claude: no hooks/mod/ dir in the npm install (#202)', !fs.existsSync(path.join(claudeDir, 'hooks', 'mod')));
|
|
1139
|
+
|
|
1140
|
+
// #257: the hook logic ships as a .cjs beside the sh wrapper.
|
|
1141
|
+
const claudeCommitMsgCjs = path.join(claudeDir, 'trailhead', 'templates', 'trailhead-commit-msg.cjs');
|
|
1142
|
+
ok('claude: commit-msg .cjs logic projected', fs.existsSync(claudeCommitMsgCjs));
|
|
1143
|
+
ok('claude: commit-msg .cjs logic is byte-identical to source',
|
|
1144
|
+
fs.readFileSync(claudeCommitMsgCjs, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-commit-msg.cjs'), 'utf8'));
|
|
1145
|
+
|
|
872
1146
|
// #185: the commit-msg hook sync script ships wholesale with templates/ too.
|
|
873
1147
|
const claudeCommitMsgSync = path.join(claudeDir, 'trailhead', 'templates', 'trailhead-commit-msg-sync.js');
|
|
874
1148
|
ok('claude: commit-msg-sync script projected', fs.existsSync(claudeCommitMsgSync));
|
|
@@ -881,6 +1155,17 @@ const claudeAuditWalk = path.join(claudeDir, 'trailhead', 'templates', 'trailhea
|
|
|
881
1155
|
ok('claude: audit-walk script projected', fs.existsSync(claudeAuditWalk));
|
|
882
1156
|
ok('claude: audit-walk script is byte-identical to source',
|
|
883
1157
|
fs.readFileSync(claudeAuditWalk, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-audit-walk.js'), 'utf8'));
|
|
1158
|
+
// #253: the Resolve bookkeeping helpers are installed, and (npm layout: templates at
|
|
1159
|
+
// <config>/trailhead/templates, hooks at <config>/hooks) find the secret guard.
|
|
1160
|
+
for (const helper of ['trailhead-issue-body.js', 'trailhead-dashboard.js']) {
|
|
1161
|
+
const installed = path.join(claudeDir, 'trailhead', 'templates', helper);
|
|
1162
|
+
ok(`claude: ${helper} installed`, fs.existsSync(installed));
|
|
1163
|
+
ok(`claude: ${helper} is byte-identical to source`,
|
|
1164
|
+
fs.readFileSync(installed, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', helper), 'utf8'));
|
|
1165
|
+
ok(`claude: ${helper} keeps its executable bit`, (fs.statSync(installed).mode & 0o111) !== 0);
|
|
1166
|
+
}
|
|
1167
|
+
ok('claude: the installed issue-body helper finds the secret guard',
|
|
1168
|
+
require(path.join(claudeDir, 'trailhead', 'templates', 'trailhead-issue-body.js')).loadGuard() !== null);
|
|
884
1169
|
// Regression: the commit-guard does require('./lib/commit-message-check.js'), so
|
|
885
1170
|
// it only loads if the lib was copied alongside it. Run it with a benign Bash
|
|
886
1171
|
// payload and assert it does not crash with a missing-module error.
|
|
@@ -1018,7 +1303,15 @@ for (const nm of ['trailhead', 'trailhead-chart', 'trailhead-work', 'trailhead-v
|
|
|
1018
1303
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
1019
1304
|
ok('claude: settings.json exists', fs.existsSync(settingsPath));
|
|
1020
1305
|
const settingsContent = fs.readFileSync(settingsPath, 'utf8');
|
|
1021
|
-
ok('claude: settings.json
|
|
1306
|
+
ok('claude: settings.json registers the merged Bash guards', settingsContent.includes('trailhead-bash-guards.js'));
|
|
1307
|
+
ok('claude: settings.json registers none of the per-guard scripts on their own',
|
|
1308
|
+
!/trailhead-(commit|secret|install|search|body)-guard\.js/.test(settingsContent));
|
|
1309
|
+
ok('claude: exactly one PreToolUse/Bash trailhead-bash-guards registration', (() => {
|
|
1310
|
+
const s = JSON.parse(settingsContent);
|
|
1311
|
+
return (s.hooks.PreToolUse || []).flatMap((g) => g.hooks || []).filter((h) => h.command.includes('trailhead-bash-guards.js')).length === 1;
|
|
1312
|
+
})());
|
|
1313
|
+
ok('claude: hooks/trailhead-bash-guards.js and its five modules exist', ['bash-guards', 'commit-guard', 'secret-guard', 'install-guard', 'search-guard', 'body-guard']
|
|
1314
|
+
.every((n) => fs.existsSync(path.join(claudeDir, 'hooks', `trailhead-${n}.js`))));
|
|
1022
1315
|
// mockup-link-guard: Claude-only PreToolUse(AskUserQuestion) backstop for the
|
|
1023
1316
|
// Prototype "link in the same message as the approval ask" rule.
|
|
1024
1317
|
ok('claude: hooks/trailhead-mockup-link-guard.js exists', fs.existsSync(path.join(claudeDir, 'hooks', 'trailhead-mockup-link-guard.js')));
|
|
@@ -1180,6 +1473,9 @@ ok('source: teamwork.md fallback resolves the hooks dir via rev-parse --git-path
|
|
|
1180
1473
|
teamworkSource.includes('git -C <working-root> rev-parse --git-path hooks'));
|
|
1181
1474
|
ok('source: teamwork.md fallback classifies by the trailhead header line',
|
|
1182
1475
|
teamworkSource.includes('// trailhead commit-msg hook (git).'));
|
|
1476
|
+
ok('source: teamwork.md fallback knows the sh wrapper header and the .cjs logic file (#257)',
|
|
1477
|
+
teamworkSource.includes('# trailhead commit-msg hook (git).') && teamworkSource.includes('trailhead-commit-msg.cjs'));
|
|
1478
|
+
ok('source: substrate-commands.md names the .cjs logic file (#257)', substrateCommandsSource.includes('trailhead-commit-msg.cjs'));
|
|
1183
1479
|
ok('source: teamwork.md fallback checks the executable bit (test -x)',
|
|
1184
1480
|
teamworkSource.includes('test -x'));
|
|
1185
1481
|
ok('source: teamwork.md fallback reads only the first 5 lines',
|
|
@@ -1342,6 +1638,78 @@ const missingBodyGuardPkg = brokenPkg((p) => fs.rmSync(path.join(p, 'plugins', '
|
|
|
1342
1638
|
ok('(h) pre-#169 search-guard migration: the stale entry is gone', !afterHooksJson.includes('trailhead-search-guard.js'));
|
|
1343
1639
|
}
|
|
1344
1640
|
|
|
1641
|
+
// (#207) Migration to the merged Bash guard hook. An install that registered
|
|
1642
|
+
// each guard on its own ends with only the merged entry; install and uninstall
|
|
1643
|
+
// are idempotent; uninstall removes the merged entry and every legacy one.
|
|
1644
|
+
const LEGACY_GUARD_RE = /trailhead-(commit|secret|install|search|body)-guard\.js/;
|
|
1645
|
+
const bashGuardCommands = (registry) => (registry.hooks.PreToolUse || [])
|
|
1646
|
+
.flatMap((g) => (g.matcher === 'Bash' ? g.hooks || [] : []))
|
|
1647
|
+
.map((h) => h.command)
|
|
1648
|
+
.filter((c) => c.includes('trailhead-bash-guards.js') || LEGACY_GUARD_RE.test(c));
|
|
1649
|
+
{
|
|
1650
|
+
const mDir = mktmp();
|
|
1651
|
+
runInstaller([`--claude`, `--dir=${mDir}`]);
|
|
1652
|
+
const settingsFile = path.join(mDir, 'settings.json');
|
|
1653
|
+
const settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
|
|
1654
|
+
const hooksDirM = path.join(mDir, 'hooks');
|
|
1655
|
+
settings.hooks.PreToolUse = settings.hooks.PreToolUse.map((g) => ({
|
|
1656
|
+
...g,
|
|
1657
|
+
hooks: g.hooks.filter((h) => !h.command.includes('trailhead-bash-guards.js')),
|
|
1658
|
+
})).filter((g) => g.hooks.length);
|
|
1659
|
+
const legacyGroup = { matcher: 'Bash', hooks: ['commit', 'secret', 'install', 'search', 'body']
|
|
1660
|
+
.map((n) => ({ type: 'command', command: `node "${path.join(hooksDirM, `trailhead-${n}-guard.js`)}"` })) };
|
|
1661
|
+
settings.hooks.PreToolUse.push(legacyGroup);
|
|
1662
|
+
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
|
|
1663
|
+
const out = runPkg(repoRoot, [`--claude`, `--dir=${mDir}`]);
|
|
1664
|
+
ok('(#207) claude migration: exit 0', out.status === 0);
|
|
1665
|
+
const after = JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
|
|
1666
|
+
const cmds = bashGuardCommands(after);
|
|
1667
|
+
ok('(#207) claude migration: only the merged entry remains', cmds.length === 1 && cmds[0].includes('trailhead-bash-guards.js'));
|
|
1668
|
+
const once = fs.readFileSync(settingsFile, 'utf8');
|
|
1669
|
+
runInstaller([`--claude`, `--dir=${mDir}`]);
|
|
1670
|
+
ok('(#207) claude install twice leaves the same settings.json', fs.readFileSync(settingsFile, 'utf8') === once);
|
|
1671
|
+
runInstaller([`--claude`, `--dir=${mDir}`, '--uninstall']);
|
|
1672
|
+
const gone = fs.readFileSync(settingsFile, 'utf8');
|
|
1673
|
+
ok('(#207) claude uninstall strips the merged entry', !gone.includes('trailhead-bash-guards.js'));
|
|
1674
|
+
runInstaller([`--claude`, `--dir=${mDir}`, '--uninstall']);
|
|
1675
|
+
ok('(#207) claude uninstall twice leaves the same settings.json', fs.readFileSync(settingsFile, 'utf8') === gone);
|
|
1676
|
+
}
|
|
1677
|
+
{
|
|
1678
|
+
const mDir = mktmp();
|
|
1679
|
+
const hooksJsonFile = path.join(mDir, 'hooks.json');
|
|
1680
|
+
const scriptsDir = path.join(mDir, 'skills', 'trailhead', 'hooks');
|
|
1681
|
+
runInstaller([`--codex`, `--dir=${mDir}`]);
|
|
1682
|
+
const reg = JSON.parse(fs.readFileSync(hooksJsonFile, 'utf8'));
|
|
1683
|
+
reg.hooks.PreToolUse = reg.hooks.PreToolUse.map((g) => ({
|
|
1684
|
+
...g,
|
|
1685
|
+
hooks: g.hooks.filter((h) => !h.command.includes('trailhead-bash-guards.js')),
|
|
1686
|
+
})).filter((g) => g.hooks.length);
|
|
1687
|
+
reg.hooks.PreToolUse.push({ matcher: 'Bash', hooks: ['commit', 'secret', 'install', 'search', 'body']
|
|
1688
|
+
.map((n) => ({ type: 'command', command: `node "${path.join(scriptsDir, `trailhead-${n}-guard.js`)}"` })) });
|
|
1689
|
+
fs.writeFileSync(hooksJsonFile, JSON.stringify(reg, null, 2));
|
|
1690
|
+
const out = runPkg(repoRoot, [`--codex`, `--dir=${mDir}`]);
|
|
1691
|
+
ok('(#207) codex migration: exit 0', out.status === 0);
|
|
1692
|
+
const cmds = bashGuardCommands(JSON.parse(fs.readFileSync(hooksJsonFile, 'utf8')));
|
|
1693
|
+
ok('(#207) codex migration: only the merged entry remains', cmds.length === 1 &&
|
|
1694
|
+
cmds[0].endsWith(' --guards=commit,secret,install,body'));
|
|
1695
|
+
const once = fs.readFileSync(hooksJsonFile, 'utf8');
|
|
1696
|
+
runInstaller([`--codex`, `--dir=${mDir}`]);
|
|
1697
|
+
ok('(#207) codex install twice leaves the same hooks.json', fs.readFileSync(hooksJsonFile, 'utf8') === once);
|
|
1698
|
+
// A legacy registration reappearing next to the merged one is removed on uninstall too.
|
|
1699
|
+
const withLegacy = JSON.parse(once);
|
|
1700
|
+
withLegacy.hooks.PreToolUse.push({ matcher: 'Bash', hooks: [
|
|
1701
|
+
{ type: 'command', command: `node "${path.join(scriptsDir, 'trailhead-search-guard.js')}"` },
|
|
1702
|
+
{ type: 'command', command: `node "${path.join(scriptsDir, 'trailhead-commit-guard.js')}"` },
|
|
1703
|
+
] });
|
|
1704
|
+
fs.writeFileSync(hooksJsonFile, JSON.stringify(withLegacy, null, 2));
|
|
1705
|
+
runInstaller([`--codex`, `--dir=${mDir}`, '--uninstall']);
|
|
1706
|
+
const gone = fs.readFileSync(hooksJsonFile, 'utf8');
|
|
1707
|
+
ok('(#207) codex uninstall strips the merged entry and every legacy one',
|
|
1708
|
+
!gone.includes('trailhead-bash-guards.js') && !LEGACY_GUARD_RE.test(gone));
|
|
1709
|
+
runInstaller([`--codex`, `--dir=${mDir}`, '--uninstall']);
|
|
1710
|
+
ok('(#207) codex uninstall twice leaves the same hooks.json', fs.readFileSync(hooksJsonFile, 'utf8') === gone);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1345
1713
|
// (i) an unresolved env var on a registered target warns instead of failing.
|
|
1346
1714
|
{
|
|
1347
1715
|
const vDir = mktmp();
|
|
@@ -1481,6 +1849,12 @@ ok('source: manage SKILL.md routes audit to references/auditing.md', /\*\*`audit
|
|
|
1481
1849
|
const dispatcherSrc190 = fs.readFileSync(path.join(sourceSkillsDir, 'trailhead', 'SKILL.md'), 'utf8');
|
|
1482
1850
|
ok('source: dispatcher argument-hint lists audit', /argument-hint: "[^"]*\|audit\|[^"]*"/.test(dispatcherSrc190));
|
|
1483
1851
|
ok('source: dispatcher manage-cluster routing list includes audit', /manage cluster is built[^\n]*`prune`, and `audit`/.test(dispatcherSrc190));
|
|
1852
|
+
// #208: panel is a verb the Claude Code mod answers; the dispatcher lists it but routes it to no skill.
|
|
1853
|
+
ok('source: dispatcher argument-hint lists panel', /argument-hint: "[^"]*\|panel\|[^"]*"/.test(dispatcherSrc190));
|
|
1854
|
+
ok('source: dispatcher says panel is mod-only and routes it to no skill', /`panel`\*\* verb routes to no skill[^\n]*Claude Code[^\n]*mod/.test(dispatcherSrc190));
|
|
1855
|
+
const panelCmdSrc208 = fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'commands', 'panel.md'), 'utf8');
|
|
1856
|
+
ok('source: commands/panel.md invokes no skill', !/Skill tool|skill name|\$trailhead|trailhead-(view|work|chart|capture|manage)/.test(panelCmdSrc208));
|
|
1857
|
+
ok('source: commands/panel.md tells the model the panel is unavailable in one line', /2\.1\.287\+[^\n]*not available/.test(panelCmdSrc208));
|
|
1484
1858
|
|
|
1485
1859
|
const auditingPath190 = path.join(sourceSkillsDir, 'trailhead-manage', 'references', 'auditing.md');
|
|
1486
1860
|
const auditingSrc = fs.existsSync(auditingPath190) ? fs.readFileSync(auditingPath190, 'utf8') : '';
|
|
@@ -1684,6 +2058,129 @@ for (const doc of ['README.md', 'README.it.md', siteManageDoc]) {
|
|
|
1684
2058
|
}
|
|
1685
2059
|
}
|
|
1686
2060
|
|
|
2061
|
+
// #202: the plugin's own hooks.json declares exactly one mod module, and it exists.
|
|
2062
|
+
const pluginHooksJson = JSON.parse(fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'hooks', 'hooks.json'), 'utf8'));
|
|
2063
|
+
ok('source: hooks.json declares exactly one module (#202)', Array.isArray(pluginHooksJson.modules) && pluginHooksJson.modules.length === 1);
|
|
2064
|
+
ok('source: the declared module file exists, relative to hooks.json (#202)',
|
|
2065
|
+
fs.existsSync(path.resolve(repoRoot, 'plugins', 'trailhead', 'hooks', pluginHooksJson.modules[0])));
|
|
2066
|
+
ok('source: hooks.json keeps its command hooks next to the module (#202)',
|
|
2067
|
+
['PreToolUse', 'PostToolUse', 'SessionStart'].every((ev) => (pluginHooksJson.hooks?.[ev] || []).length > 0));
|
|
2068
|
+
|
|
2069
|
+
// --- #259: `trailhead:paused` label marks a paused ticket (pause adds, resume/claim drop) ---
|
|
2070
|
+
const firstUseLine = (substrateCommandsSource.match(/^1\. Create any missing labels.*$/m) || [''])[0];
|
|
2071
|
+
ok('source: first-use setup lists twenty labels including trailhead:paused and trailhead:uat',
|
|
2072
|
+
/all twenty:/.test(firstUseLine) && firstUseLine.includes('`trailhead:paused`') && firstUseLine.includes('`trailhead:uat`'));
|
|
2073
|
+
ok('source: no stale "eighteen" or "nineteen" label count remains in the shared skills',
|
|
2074
|
+
!/eighteen|nineteen/.test(substrateCommandsSource) && !/eighteen|nineteen/.test(substrateSource));
|
|
2075
|
+
ok('source: substrate-commands.md ensures trailhead:paused idempotently',
|
|
2076
|
+
/gh label create trailhead:paused --color FBCA04 --description "Paused: a PAUSED checkpoint holds its next step" 2>\/dev\/null \|\| true/.test(substrateCommandsSource));
|
|
2077
|
+
ok('source: substrate.md documents trailhead:paused as set by pause, cleared by resume or claim, off the frontier (#263)',
|
|
2078
|
+
/\*\*`trailhead:paused`\*\*[\s\S]{0,500}pause[\s\S]{0,200}resume[\s\S]{0,200}claim[\s\S]{0,400}excluded from the frontier/.test(substrateSource));
|
|
2079
|
+
const pauseBullet = (teamworkSource.match(/^- \*\*Pause\*\*.*$/m) || [''])[0];
|
|
2080
|
+
const resumeBullet = (teamworkSource.match(/^- \*\*Resume\*\*.*$/m) || [''])[0];
|
|
2081
|
+
ok('source: teamwork.md pause adds trailhead:paused (created idempotently) after the PAUSED comment',
|
|
2082
|
+
/post a \*\*`PAUSED`\*\* checkpoint comment[\s\S]{0,1200}add the `trailhead:paused` label[\s\S]{0,120}creating it idempotently first[\s\S]{0,200}after the `PAUSED` comment/.test(pauseBullet));
|
|
2083
|
+
ok('source: teamwork.md resume removes trailhead:paused only after the claim is confirmed',
|
|
2084
|
+
/gh issue edit <n> --remove-label trailhead:paused/.test(resumeBullet) &&
|
|
2085
|
+
/only after the claim is confirmed[\s\S]{0,300}a refused resume leaves the ticket visibly paused/.test(resumeBullet));
|
|
2086
|
+
ok('source: work SKILL.md claim step drops trailhead:paused from a claimed paused ticket',
|
|
2087
|
+
/\*\*Claim\*\* it:[\s\S]{0,1200}carries `trailhead:paused`[\s\S]{0,120}drops it at the claim/.test(workSkillSource));
|
|
2088
|
+
ok('source: work SKILL.md quick isolation bullet drops trailhead:paused at the claim',
|
|
2089
|
+
/^- \*\*Same isolation setup as `work`\.\*\*.*claimed ticket that carries `trailhead:paused` drops it at the claim/m.test(workSkillSource));
|
|
2090
|
+
|
|
2091
|
+
// --- #261: `trailhead:uat` label marks a pending UAT follow-up (marker, not a state) ---
|
|
2092
|
+
{
|
|
2093
|
+
ok('source: substrate-commands.md ensures trailhead:uat idempotently',
|
|
2094
|
+
/gh label create trailhead:uat --color [0-9A-Fa-f]{6} --description "Pending UAT: an acceptance test still to run" 2>\/dev\/null \|\| true/.test(substrateCommandsSource));
|
|
2095
|
+
{
|
|
2096
|
+
const helperSource = fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-uat-backfill.js'), 'utf8');
|
|
2097
|
+
const docColor = (substrateCommandsSource.match(/gh label create trailhead:uat --color ([0-9A-Fa-f]{6})/) || [])[1];
|
|
2098
|
+
const helperColor = (helperSource.match(/const COLOR = '([0-9A-Fa-f]{6})'/) || [])[1];
|
|
2099
|
+
ok('source: the backfill helper creates trailhead:uat with the same colour as substrate-commands.md',
|
|
2100
|
+
docColor !== undefined && docColor === helperColor);
|
|
2101
|
+
}
|
|
2102
|
+
ok('source: substrate-commands.md has no create-then-backfill chain and invokes the backfill helper',
|
|
2103
|
+
/trailhead-uat-backfill\.js/.test(substrateCommandsSource) && !/trailhead:uat[^\n]*&&[^\n]*backfill/.test(substrateCommandsSource));
|
|
2104
|
+
ok('source: first-use setup runs the backfill helper after the labels',
|
|
2105
|
+
/^1\. Create any missing labels[\s\S]{0,1500}?trailhead-uat-backfill\.js/m.test(substrateCommandsSource));
|
|
2106
|
+
ok('source: substrate.md documents trailhead:uat as a marker label right after the state labels, not a state, not a frontier filter',
|
|
2107
|
+
/\*\*`trailhead:uat`\*\*[\s\S]{0,700}marker[\s\S]{0,400}not a state[\s\S]{0,400}never a frontier filter/.test(substrateSource)
|
|
2108
|
+
&& substrateSource.indexOf('**`trailhead:uat`**') > substrateSource.indexOf('**`trailhead:paused`**')
|
|
2109
|
+
&& substrateSource.indexOf('**`trailhead:uat`**') < substrateSource.indexOf('**Frontier**'));
|
|
2110
|
+
ok('source: acceptance-testing.md option (b) opens the follow-up with trailhead:uat in the gh issue create --label list, label ensured first',
|
|
2111
|
+
/open a follow-up ticket before that close[\s\S]{0,900}--label[^\n]{0,200}`?trailhead:uat/.test(acceptanceTestingSource)
|
|
2112
|
+
&& /ensur\w* (the )?`?trailhead:uat`? (label )?idempotently/.test(acceptanceTestingSource));
|
|
2113
|
+
ok('source: ticket-engines.md UAT follow-ups section names the label',
|
|
2114
|
+
/UAT follow-up[\s\S]{0,1500}`trailhead:uat`/.test(ticketEnginesSource.split('\n').filter((l) => /^A ticket whose body carries/.test(l)).join('\n')));
|
|
2115
|
+
ok('source: work SKILL.md work-start runs the backfill helper once per clone, marker gated',
|
|
2116
|
+
/trailhead-uat-backfill\.js[^\n]*--marker[^\n]*\.trailhead\/uat-backfill/.test(workSkillSource));
|
|
2117
|
+
ok('source: work SKILL.md passes the backfill marker as an absolute path under the working root',
|
|
2118
|
+
/trailhead-uat-backfill\.js[^\n]*--marker <working-root>\/\.trailhead\/uat-backfill/.test(workSkillSource));
|
|
2119
|
+
ok('source: .gitignore ignores .trailhead/uat-backfill',
|
|
2120
|
+
/^\.trailhead\/uat-backfill$/m.test(fs.readFileSync(path.join(repoRoot, '.gitignore'), 'utf8')));
|
|
2121
|
+
ok('source: work SKILL.md mentions the uat-backfill marker as gitignored per-clone state',
|
|
2122
|
+
/`\.trailhead\/uat-backfill`[^\n]*(gitignored|never committed)/.test(workSkillSource));
|
|
2123
|
+
const producerSources = { 'acceptance-testing.md': acceptanceTestingSource, 'site acceptance-testing.md': siteAcceptanceSource };
|
|
2124
|
+
for (const [name, src] of Object.entries(producerSources)) {
|
|
2125
|
+
ok(`source: ${name} writes the pointer as UAT of: [<parent name>](<parent url>)`,
|
|
2126
|
+
src.includes('UAT of: [<parent name>](<parent url>)') && !/UAT of: <parent name>\(link\)|UAT of: #<n>/.test(src));
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
// --- #263: a paused ticket is off the frontier on every surface ---
|
|
2131
|
+
{
|
|
2132
|
+
const viewSkillSource = fs.readFileSync(path.join(sourceSkillsDir, 'trailhead-view', 'SKILL.md'), 'utf8');
|
|
2133
|
+
const siteDocs = path.join(repoRoot, 'site', 'src', 'content', 'docs', 'docs');
|
|
2134
|
+
const siteViewSource = fs.readFileSync(path.join(siteDocs, 'commands', 'view.md'), 'utf8');
|
|
2135
|
+
const siteWorkSource = fs.readFileSync(path.join(siteDocs, 'commands', 'work.md'), 'utf8');
|
|
2136
|
+
const changelogSource = fs.readFileSync(path.join(repoRoot, 'CHANGELOG.md'), 'utf8');
|
|
2137
|
+
const searchExprs = (src) => [...src.matchAll(/--search\s+"([^"]*no:assignee[^"]*)"/g)].map((m) => m[1]);
|
|
2138
|
+
const base = 'no:assignee -label:trailhead:blocked -label:trailhead:unverified -label:trailhead:paused';
|
|
2139
|
+
const wbExcl = `${base} -label:trailhead:whiteboard`;
|
|
2140
|
+
const tally = (exprs) => exprs.reduce((acc, e) => ({ ...acc, [e]: (acc[e] || 0) + 1 }), {});
|
|
2141
|
+
const sameTally = (got, want) => JSON.stringify(Object.entries(got).sort()) === JSON.stringify(Object.entries(want).sort());
|
|
2142
|
+
|
|
2143
|
+
ok('source: substrate-commands.md frontier queries each exclude paused (two repo-wide, four label-scoped or whiteboard or count)',
|
|
2144
|
+
sameTally(tally(searchExprs(substrateCommandsSource)), { [wbExcl]: 2, [base]: 4 }));
|
|
2145
|
+
ok('source: multi-map.md scoped frontier query excludes paused', sameTally(tally(searchExprs(multiMapSource)), { [base]: 1 }));
|
|
2146
|
+
ok('source: trailhead-view SKILL.md whiteboard frontier query excludes paused', sameTally(tally(searchExprs(viewSkillSource)), { [base]: 1 }));
|
|
2147
|
+
ok('source: the cookbook frontier comment says not paused',
|
|
2148
|
+
/# the frontier: open, unassigned, not blocked, not unverified, not paused, one query/.test(substrateCommandsSource));
|
|
2149
|
+
|
|
2150
|
+
ok('source: substrate.md Frontier definition excludes trailhead:paused',
|
|
2151
|
+
/\*\*Frontier\*\* = open, unassigned tickets that are \*\*not\*\* `trailhead:blocked`, \*\*not\*\* `trailhead:unverified` and \*\*not\*\* `trailhead:paused`/.test(substrateSource));
|
|
2152
|
+
const pausedBullet = (substrateSource.match(/^- \*\*`trailhead:paused`\*\*.*$/m) || [''])[0];
|
|
2153
|
+
ok('source: substrate.md paused bullet: excluded from the frontier, taken back with /trailhead:resume',
|
|
2154
|
+
/excluded from the frontier/.test(pausedBullet) && /\/trailhead:resume/.test(pausedBullet));
|
|
2155
|
+
ok('source: the old "does not change the frontier query" wording is gone',
|
|
2156
|
+
!/does not change the frontier query/.test(substrateSource));
|
|
2157
|
+
|
|
2158
|
+
const paused = /^- \*\*In progress\*\*[^\n]*\n- \*\*Paused\*\*[^\n]*\/trailhead:resume <n>`[^\n]*\n- \*\*Blocked\*\*/gm;
|
|
2159
|
+
ok('source: the map and whiteboard renders both list a Paused group between In progress and Blocked',
|
|
2160
|
+
(viewSkillSource.match(paused) || []).length === 2);
|
|
2161
|
+
ok('source: both render Frontier bullets say not paused',
|
|
2162
|
+
(viewSkillSource.match(/^- \*\*Frontier\*\*:[^\n]*not[^\n]*`trailhead:paused`/gm) || []).length === 2);
|
|
2163
|
+
|
|
2164
|
+
ok('source: multi-map.md park & switch resumes with /trailhead:resume only',
|
|
2165
|
+
/Resume later with `\/trailhead:resume`/.test(multiMapSource) && !/just `\/trailhead:work` once that map is active/.test(multiMapSource));
|
|
2166
|
+
|
|
2167
|
+
ok('source: work SKILL.md: bare work takes the paused-excluding frontier, a named work or quick still works a paused ticket',
|
|
2168
|
+
/Otherwise take a frontier ticket[\s\S]{0,600}never a `trailhead:paused` ticket/.test(workSkillSource) &&
|
|
2169
|
+
/\*\*named\*\* ticket may also be a `trailhead:paused` one, which `work <n>` \/ `quick <n>` takes back/.test(workSkillSource) &&
|
|
2170
|
+
/carries `trailhead:paused` drops it at the claim/.test(workSkillSource));
|
|
2171
|
+
|
|
2172
|
+
ok('source: auto.md resumes owned paused tickets in step 2 and no longer says they come back on the frontier',
|
|
2173
|
+
/\*\*each\*\* ticket you own[\s\S]{0,200}`trailhead:paused`/.test(autoSource) &&
|
|
2174
|
+
!/paused\/stuck tickets are back on the frontier/.test(autoSource) && !/paused[^.\n]{0,40}back on the frontier/.test(autoSource));
|
|
2175
|
+
ok('source: site work.md says a paused ticket is off the frontier until resumed',
|
|
2176
|
+
/off the frontier until (it is )?resumed/.test(siteWorkSource));
|
|
2177
|
+
ok('source: site view.md describes the Paused group off the frontier', /Paused/.test(siteViewSource) && /not paused|off the frontier/.test(siteViewSource));
|
|
2178
|
+
const conceptsSource = fs.readFileSync(path.join(siteDocs, 'concepts.md'), 'utf8');
|
|
2179
|
+
ok('source: concepts.md, README.md and README.it.md frontier definitions mention paused',
|
|
2180
|
+
/not paused/.test(conceptsSource) && /not paused/.test(readmeSource) && /in pausa/.test(readmeItSource));
|
|
2181
|
+
ok('source: CHANGELOG Unreleased > Changed has a #263 entry', /### Changed[\s\S]*?\(#263\)/.test(changelogSource.split('## 0.14.0')[0]));
|
|
2182
|
+
}
|
|
2183
|
+
|
|
1687
2184
|
for (const d of tmpDirs) {
|
|
1688
2185
|
fs.rmSync(d, { recursive: true, force: true });
|
|
1689
2186
|
}
|