@opengsd/gsd-core 1.6.0-rc.1 → 1.6.0-rc.3

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.
Files changed (151) hide show
  1. package/.claude-plugin/plugin.json +2 -1
  2. package/agents/gsd-planner.md +8 -57
  3. package/agents/gsd-roadmapper.md +6 -0
  4. package/agents/gsd-security-auditor.md +37 -18
  5. package/bin/install.js +243 -345
  6. package/commands/gsd/capture.md +5 -1
  7. package/gemini-extension.json +1 -1
  8. package/gsd-core/bin/gsd-tools.cjs +53 -6
  9. package/gsd-core/bin/lib/adr-parser.cjs +21 -6
  10. package/gsd-core/bin/lib/audit-command-router.cjs +52 -14
  11. package/gsd-core/bin/lib/capability-lifecycle.cjs +30 -7
  12. package/gsd-core/bin/lib/capability-registry.cjs +96 -83
  13. package/gsd-core/bin/lib/capability-validator.cjs +22 -0
  14. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +40 -2
  15. package/gsd-core/bin/lib/command-routing-hub.cjs +10 -3
  16. package/gsd-core/bin/lib/commands.cjs +247 -0
  17. package/gsd-core/bin/lib/config-loader.cjs +6 -0
  18. package/gsd-core/bin/lib/config-schema.cjs +1 -0
  19. package/gsd-core/bin/lib/config.cjs +70 -21
  20. package/gsd-core/bin/lib/coverage.cjs +464 -0
  21. package/gsd-core/bin/lib/frontmatter.cjs +7 -3
  22. package/gsd-core/bin/lib/graphify-command-router.cjs +53 -36
  23. package/gsd-core/bin/lib/init.cjs +8 -4
  24. package/gsd-core/bin/lib/install-profiles.cjs +6 -3
  25. package/gsd-core/bin/lib/intel-command-router.cjs +79 -60
  26. package/gsd-core/bin/lib/phase-id.cjs +25 -11
  27. package/gsd-core/bin/lib/phase.cjs +4 -4
  28. package/gsd-core/bin/lib/planning-workspace.cjs +157 -13
  29. package/gsd-core/bin/lib/probe-core.cjs +7 -0
  30. package/gsd-core/bin/lib/profile-output.cjs +18 -6
  31. package/gsd-core/bin/lib/prohibition-enforcement.cjs +59 -26
  32. package/gsd-core/bin/lib/roadmap-command-router.cjs +16 -3
  33. package/gsd-core/bin/lib/roadmap-parser.cjs +29 -8
  34. package/gsd-core/bin/lib/roadmap-upgrade.cjs +47 -17
  35. package/gsd-core/bin/lib/roadmap.cjs +5 -2
  36. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +473 -16
  37. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +77 -0
  38. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +22 -31
  39. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +15 -0
  40. package/gsd-core/bin/lib/runtime-name-policy.cjs +89 -0
  41. package/gsd-core/bin/lib/shell-command-projection.cjs +55 -1
  42. package/gsd-core/bin/lib/state.cjs +364 -52
  43. package/gsd-core/bin/lib/surface.cjs +47 -22
  44. package/gsd-core/bin/lib/update-context.cjs +2 -2
  45. package/gsd-core/bin/lib/validate.cjs +5 -2
  46. package/gsd-core/bin/lib/verify.cjs +11 -2
  47. package/gsd-core/bin/lib/worktree-safety.cjs +202 -0
  48. package/gsd-core/bin/shared/config-defaults.manifest.json +2 -1
  49. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  50. package/gsd-core/references/context-budget.md +8 -8
  51. package/gsd-core/references/execute-phase-context-guard.md +16 -0
  52. package/gsd-core/references/planner-guidance.md +66 -0
  53. package/gsd-core/references/planning-config.md +3 -2
  54. package/gsd-core/references/prohibition-probe.md +15 -9
  55. package/gsd-core/references/security-asvs-levels.md +27 -0
  56. package/gsd-core/templates/SECURITY.md +6 -4
  57. package/gsd-core/templates/summary-complex.md +4 -0
  58. package/gsd-core/templates/summary-minimal.md +3 -0
  59. package/gsd-core/templates/summary-standard.md +4 -0
  60. package/gsd-core/templates/summary.md +41 -0
  61. package/gsd-core/workflows/autonomous.md +33 -33
  62. package/gsd-core/workflows/diagnose-issues.md +6 -1
  63. package/gsd-core/workflows/execute-phase.md +8 -6
  64. package/gsd-core/workflows/execute-plan.md +5 -0
  65. package/gsd-core/workflows/help/modes/full.md +10 -0
  66. package/gsd-core/workflows/list-seeds.md +63 -0
  67. package/gsd-core/workflows/manager.md +37 -37
  68. package/gsd-core/workflows/new-project.md +4 -4
  69. package/gsd-core/workflows/plan-phase.md +15 -0
  70. package/gsd-core/workflows/pr-branch.md +156 -0
  71. package/gsd-core/workflows/quick.md +6 -1
  72. package/gsd-core/workflows/review.md +10 -2
  73. package/gsd-core/workflows/secure-phase.md +13 -7
  74. package/gsd-core/workflows/spec-phase.md +8 -3
  75. package/gsd-core/workflows/verify-phase.md +2 -2
  76. package/gsd-core/workflows/verify-work.md +30 -1
  77. package/package.json +7 -2
  78. package/scripts/gen-plugin-skills.cjs +117 -0
  79. package/scripts/lint-test-file-count.allowlist.json +2 -1
  80. package/scripts/prompt-injection-scan.sh +5 -0
  81. package/scripts/release-notes/conventional-title.cjs +88 -0
  82. package/scripts/release-notes/format-github-release-notes.cjs +4 -3
  83. package/skills/gsd-add-tests/SKILL.md +38 -0
  84. package/skills/gsd-ai-integration-phase/SKILL.md +37 -0
  85. package/skills/gsd-audit-fix/SKILL.md +33 -0
  86. package/skills/gsd-audit-milestone/SKILL.md +37 -0
  87. package/skills/gsd-audit-uat/SKILL.md +25 -0
  88. package/skills/gsd-autonomous/SKILL.md +51 -0
  89. package/skills/gsd-capture/SKILL.md +67 -0
  90. package/skills/gsd-cleanup/SKILL.md +24 -0
  91. package/skills/gsd-code-review/SKILL.md +59 -0
  92. package/skills/gsd-complete-milestone/SKILL.md +142 -0
  93. package/skills/gsd-config/SKILL.md +56 -0
  94. package/skills/gsd-debug/SKILL.md +53 -0
  95. package/skills/gsd-discuss-phase/SKILL.md +77 -0
  96. package/skills/gsd-docs-update/SKILL.md +49 -0
  97. package/skills/gsd-eval-review/SKILL.md +33 -0
  98. package/skills/gsd-execute-phase/SKILL.md +65 -0
  99. package/skills/gsd-explore/SKILL.md +28 -0
  100. package/skills/gsd-extract-learnings/SKILL.md +22 -0
  101. package/skills/gsd-fast/SKILL.md +31 -0
  102. package/skills/gsd-forensics/SKILL.md +56 -0
  103. package/skills/gsd-graphify/SKILL.md +204 -0
  104. package/skills/gsd-health/SKILL.md +31 -0
  105. package/skills/gsd-help/SKILL.md +29 -0
  106. package/skills/gsd-import/SKILL.md +46 -0
  107. package/skills/gsd-inbox/SKILL.md +39 -0
  108. package/skills/gsd-ingest-docs/SKILL.md +43 -0
  109. package/skills/gsd-manager/SKILL.md +45 -0
  110. package/skills/gsd-map-codebase/SKILL.md +83 -0
  111. package/skills/gsd-mempalace-capture/SKILL.md +71 -0
  112. package/skills/gsd-mempalace-recall/SKILL.md +102 -0
  113. package/skills/gsd-milestone-summary/SKILL.md +51 -0
  114. package/skills/gsd-mvp-phase/SKILL.md +45 -0
  115. package/skills/gsd-new-milestone/SKILL.md +45 -0
  116. package/skills/gsd-new-project/SKILL.md +47 -0
  117. package/skills/gsd-ns-context/SKILL.md +24 -0
  118. package/skills/gsd-ns-ideate/SKILL.md +23 -0
  119. package/skills/gsd-ns-manage/SKILL.md +35 -0
  120. package/skills/gsd-ns-project/SKILL.md +26 -0
  121. package/skills/gsd-ns-review/SKILL.md +28 -0
  122. package/skills/gsd-ns-workflow/SKILL.md +33 -0
  123. package/skills/gsd-pause-work/SKILL.md +43 -0
  124. package/skills/gsd-phase/SKILL.md +57 -0
  125. package/skills/gsd-plan-phase/SKILL.md +63 -0
  126. package/skills/gsd-plan-review-convergence/SKILL.md +60 -0
  127. package/skills/gsd-pr-branch/SKILL.md +26 -0
  128. package/skills/gsd-profile-user/SKILL.md +47 -0
  129. package/skills/gsd-progress/SKILL.md +49 -0
  130. package/skills/gsd-quick/SKILL.md +174 -0
  131. package/skills/gsd-resume-work/SKILL.md +31 -0
  132. package/skills/gsd-review/SKILL.md +42 -0
  133. package/skills/gsd-review-backlog/SKILL.md +63 -0
  134. package/skills/gsd-secure-phase/SKILL.md +36 -0
  135. package/skills/gsd-settings/SKILL.md +29 -0
  136. package/skills/gsd-ship/SKILL.md +24 -0
  137. package/skills/gsd-sketch/SKILL.md +60 -0
  138. package/skills/gsd-spec-phase/SKILL.md +63 -0
  139. package/skills/gsd-spike/SKILL.md +57 -0
  140. package/skills/gsd-stats/SKILL.md +20 -0
  141. package/skills/gsd-surface/SKILL.md +162 -0
  142. package/skills/gsd-thread/SKILL.md +24 -0
  143. package/skills/gsd-ui-phase/SKILL.md +35 -0
  144. package/skills/gsd-ui-review/SKILL.md +33 -0
  145. package/skills/gsd-ultraplan-phase/SKILL.md +34 -0
  146. package/skills/gsd-undo/SKILL.md +35 -0
  147. package/skills/gsd-update/SKILL.md +50 -0
  148. package/skills/gsd-validate-phase/SKILL.md +36 -0
  149. package/skills/gsd-verify-work/SKILL.md +39 -0
  150. package/skills/gsd-workspace/SKILL.md +53 -0
  151. package/skills/gsd-workstreams/SKILL.md +70 -0
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: gsd:capture
3
3
  description: Capture ideas, tasks, notes, and seeds to their destination
4
- argument-hint: "[--note | --backlog | --seed | --list] [text]"
4
+ argument-hint: "[--note | --backlog | --seed | --list | --list-seeds] [text]"
5
5
  allowed-tools:
6
6
  - Read
7
7
  - Write
@@ -21,6 +21,7 @@ Mode routing:
21
21
  - **--backlog**: Add an idea to the backlog parking lot (999.x numbering) → add-backlog workflow
22
22
  - **--seed**: Capture a forward-looking idea with trigger conditions → plant-seed workflow
23
23
  - **--list**: List pending todos and select one to work on → check-todos workflow
24
+ - **--list-seeds**: List/audit captured seeds (optional status filter) → list-seeds workflow
24
25
  </objective>
25
26
 
26
27
  <routing>
@@ -32,6 +33,7 @@ Mode routing:
32
33
  | --backlog | ROADMAP.md backlog section (999.x) | add-backlog |
33
34
  | --seed | .planning/seeds/SEED-NNN-slug.md | plant-seed |
34
35
  | --list | Interactive todo browser + action router | check-todos |
36
+ | --list-seeds | Read-only seed list/audit (optional status filter) | list-seeds |
35
37
 
36
38
  </routing>
37
39
 
@@ -41,6 +43,7 @@ Mode routing:
41
43
  @~/.claude/gsd-core/workflows/add-backlog.md
42
44
  @~/.claude/gsd-core/workflows/plant-seed.md
43
45
  @~/.claude/gsd-core/workflows/check-todos.md
46
+ @~/.claude/gsd-core/workflows/list-seeds.md
44
47
  @~/.claude/gsd-core/references/ui-brand.md
45
48
  </execution_context>
46
49
 
@@ -51,6 +54,7 @@ Parse the first token of $ARGUMENTS:
51
54
  - If it is `--note`: strip the flag, pass remainder to note workflow
52
55
  - If it is `--backlog`: strip the flag, pass remainder to add-backlog workflow
53
56
  - If it is `--seed`: strip the flag, pass remainder to plant-seed workflow
57
+ - If it is `--list-seeds`: strip the flag, pass remainder (optional status filter) to list-seeds workflow
54
58
  - If it is `--list`: pass remainder (optional area filter) to check-todos workflow
55
59
  - Otherwise: pass all of $ARGUMENTS to add-todo workflow
56
60
  </context>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-core",
3
- "version": "1.6.0-rc.1",
3
+ "version": "1.6.0-rc.3",
4
4
  "description": "GSD Core — a meta-prompting, context engineering, and spec-driven development system for AI coding agents. Loads gsd's operating context into every Gemini CLI session.",
5
5
  "contextFileName": "GEMINI.md"
6
6
  }
@@ -25,6 +25,7 @@
25
25
  * generate-slug <text> Convert text to URL-safe slug
26
26
  * current-timestamp [format] Get timestamp (full|date|filename)
27
27
  * list-todos [area] Count and enumerate pending todos
28
+ * list-seeds [status] List captured seeds (optional status filter)
28
29
  * verify-path-exists <path> Check file/directory existence
29
30
  * config-ensure-section Initialize .planning/config.json
30
31
  * history-digest Aggregate all SUMMARY.md data
@@ -84,6 +85,7 @@
84
85
  * UAT Audit:
85
86
  * audit-uat Scan all phases for unresolved UAT/verification items
86
87
  * uat render-checkpoint --file <path> Render the current UAT checkpoint block
88
+ * uat classify-coverage --summary <path> Classify a SUMMARY coverage block into auto-passed vs human-UAT (#1602)
87
89
  *
88
90
  * Open Artifact Audit:
89
91
  * audit-open [--json] Scan all .planning/ artifact types for unresolved items
@@ -631,13 +633,13 @@ async function main() {
631
633
  // discovery; previously it was a partial subset that didn't include
632
634
  // phase / roadmap / milestone / progress / etc.
633
635
  const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--ws <name>] [--json-errors]\n' +
634
- 'Commands: agent, agent-skills, audit-open, audit-uat, check, check-commit, commit, commit-to-subrepo, ' +
636
+ 'Commands: agent, agent-skills, audit-open, audit-uat, check, check-commit, commit, commit-to-subrepo, pr-subrepo, ' +
635
637
  'config-ensure-section, config-get, config-new-project, config-path, config-set, migrate-config, ' +
636
638
  'current-timestamp, detect-custom-files, docs-init, drift-guard, effort, extract-messages, find-phase, ' +
637
639
  'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
638
640
  'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
639
- 'capability, classify-confidence, git, learnings, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, profile-questionnaire, ' +
640
- 'profile-sample, progress, prompt-budget, requirements, research-plan, research-store, resolve-granularity, resolve-model, roadmap, scaffold, state, ' +
641
+ 'capability, classify-confidence, git, learnings, list-seeds, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, profile-questionnaire, ' +
642
+ 'profile-sample, progress, project-instruction-file, prompt-budget, requirements, research-plan, research-store, resolve-granularity, resolve-model, roadmap, scaffold, state, ' +
641
643
  'task, template, user-story, validate, verify, verify-path-exists, verify-summary, workstream, worktree\n\n' +
642
644
  'Global flags:\n' +
643
645
  ' --raw Emit raw output without post-processing\n' +
@@ -688,6 +690,10 @@ async function main() {
688
690
  'worktree', 'prompt-budget',
689
691
  'research-store', 'research-plan', 'package-legitimacy', 'classify-confidence',
690
692
  'user-story', // pure string validation — no .planning/ access needed
693
+ // #1529: pure runtime→filename projection via getProjectInstructionFile; no
694
+ // .planning/ access needed, and resolving project root would break workflow
695
+ // invocations that run before .planning/ exists (new-project Step 1).
696
+ 'project-instruction-file',
691
697
  ]);
692
698
  if (!SKIP_ROOT_RESOLUTION.has(command)) {
693
699
  cwd = findProjectRoot(cwd);
@@ -959,6 +965,13 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
959
965
  break;
960
966
  }
961
967
 
968
+ case 'pr-subrepo': {
969
+ const message = args[1];
970
+ const { repo, branch } = parseNamedArgs(args, ['repo', 'branch']);
971
+ commands.cmdPrSubrepo(cwd, repo, branch, message, raw);
972
+ break;
973
+ }
974
+
962
975
  case 'verify-summary': {
963
976
  const summaryPath = args[1];
964
977
  const countIndex = args.indexOf('--check-count');
@@ -1095,11 +1108,39 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1095
1108
  break;
1096
1109
  }
1097
1110
 
1111
+ case 'project-instruction-file': {
1112
+ // #1529: pure runtime→filename projection. Backs the
1113
+ // `gsd_run query project-instruction-file --runtime <r>` call in
1114
+ // new-project.md so the bash workflow and profile-output.cjs share one
1115
+ // source of truth (getProjectInstructionFile in runtime-name-policy.cjs).
1116
+ // No SDK bridge — pure local lookup, runs before .planning/ exists.
1117
+ const { getProjectInstructionFile } = require('./lib/runtime-name-policy.cjs');
1118
+ // Parse --runtime <value> (space or = form); default to empty so the
1119
+ // safe AGENTS.md cross-agent default applies.
1120
+ const pifArgs = args.slice(1);
1121
+ let pifRuntime = '';
1122
+ for (let i = 0; i < pifArgs.length; i++) {
1123
+ const a = pifArgs[i];
1124
+ if (a === '--runtime' && pifArgs[i + 1] !== undefined) { pifRuntime = pifArgs[++i]; continue; }
1125
+ if (a.startsWith('--runtime=')) { pifRuntime = a.slice('--runtime='.length); continue; }
1126
+ // First positional that isn't a flag also works (lenient); otherwise ignore unknown flags.
1127
+ if (!a.startsWith('-') && !pifRuntime) { pifRuntime = a; }
1128
+ }
1129
+ const filename = getProjectInstructionFile(pifRuntime);
1130
+ process.stdout.write(filename + '\n');
1131
+ break;
1132
+ }
1133
+
1098
1134
  case 'list-todos': {
1099
1135
  commands.cmdListTodos(cwd, args[1], raw);
1100
1136
  break;
1101
1137
  }
1102
1138
 
1139
+ case 'list-seeds': {
1140
+ commands.cmdListSeeds(cwd, args[1], raw);
1141
+ break;
1142
+ }
1143
+
1103
1144
  case 'verify-path-exists': {
1104
1145
  commands.cmdVerifyPathExists(cwd, args[1], raw);
1105
1146
  break;
@@ -1320,12 +1361,16 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1320
1361
 
1321
1362
  case 'uat': {
1322
1363
  const subcommand = args[1];
1323
- const uat = require('./lib/uat.cjs');
1324
1364
  if (subcommand === 'render-checkpoint') {
1365
+ const uat = require('./lib/uat.cjs');
1325
1366
  const options = parseNamedArgs(args, ['file']);
1326
1367
  uat.cmdRenderCheckpoint(cwd, options, raw);
1368
+ } else if (subcommand === 'classify-coverage') {
1369
+ const coverage = require('./lib/coverage.cjs');
1370
+ const options = parseNamedArgs(args, ['summary', 'file']);
1371
+ coverage.cmdClassify(cwd, options, raw);
1327
1372
  } else {
1328
- error('Unknown uat subcommand. Available: render-checkpoint', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1373
+ error('Unknown uat subcommand. Available: render-checkpoint, classify-coverage', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1329
1374
  }
1330
1375
  break;
1331
1376
  }
@@ -2128,6 +2173,8 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
2128
2173
  const worktreeSafety = require('./lib/worktree-safety.cjs');
2129
2174
  if (subcommand === 'cleanup-wave') {
2130
2175
  worktreeSafety.cmdWorktreeCleanupWave(cwd, args.slice(2));
2176
+ } else if (subcommand === 'record-agent') {
2177
+ worktreeSafety.cmdWorktreeRecordAgent(cwd, args.slice(2));
2131
2178
  } else if (subcommand === 'reap-orphans') {
2132
2179
  worktreeSafety.cmdWorktreeReapOrphans(cwd);
2133
2180
  } else if (subcommand === 'base-check') {
@@ -2135,7 +2182,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
2135
2182
  } else if (subcommand === 'set-baseref') {
2136
2183
  require('./lib/worktree-base-ref.cjs').cmdWorktreeSetBaseRef(cwd, args.slice(2));
2137
2184
  } else {
2138
- error('Unknown worktree subcommand. Available: cleanup-wave, reap-orphans, base-check, set-baseref', ERROR_REASON.SDK_UNKNOWN_COMMAND);
2185
+ error('Unknown worktree subcommand. Available: cleanup-wave, record-agent, reap-orphans, base-check, set-baseref', ERROR_REASON.SDK_UNKNOWN_COMMAND);
2139
2186
  }
2140
2187
  break;
2141
2188
  }
@@ -58,7 +58,6 @@ const CANONICAL_HEADERS = {
58
58
  'candidates',
59
59
  'approaches considered',
60
60
  'variants',
61
- 'trade-offs',
62
61
  'pros and cons of the options',
63
62
  'discussion',
64
63
  ],
@@ -190,15 +189,31 @@ function normalizeAdrHeader(raw) {
190
189
  .replace(/[^\w\s]/g, '')
191
190
  .trim();
192
191
  }
193
- function classifyHeader(normalizedHeader) {
192
+ // Normalized synonym index (audit M7). classifyHeader receives an ALREADY-normalized
193
+ // header (via normalizeAdrHeader), but historically compared it against the RAW synonym
194
+ // strings. Because normalizeAdrHeader collapses [\s:._-]+ to a space and strips [^\w\s],
195
+ // any synonym carrying a hyphen/apostrophe/etc. ('trade-offs', "won't do", 'post-grilling')
196
+ // could never match a normalized header — it was silently dead, and its ADR section went
197
+ // unmapped. Normalizing BOTH sides closes that abstraction asymmetry once, so every synonym
198
+ // (current and future) is reachable regardless of punctuation. Precomputed at module load to
199
+ // avoid re-normalizing the whole table per call; insertion order is preserved so first-match-
200
+ // wins and the exact-then-prefix precedence stay identical to the prior raw-compare loop.
201
+ const _NORMALIZED_SYNONYM_INDEX = (() => {
202
+ const index = [];
194
203
  for (const [canonical, synonyms] of Object.entries(CANONICAL_HEADERS)) {
195
204
  for (const synonym of synonyms) {
196
- if (normalizedHeader === synonym)
197
- return canonical;
198
- if (normalizedHeader.startsWith(`${synonym} `))
199
- return canonical;
205
+ index.push([normalizeAdrHeader(synonym), canonical]);
200
206
  }
201
207
  }
208
+ return index;
209
+ })();
210
+ function classifyHeader(normalizedHeader) {
211
+ for (const [synonym, canonical] of _NORMALIZED_SYNONYM_INDEX) {
212
+ if (normalizedHeader === synonym)
213
+ return canonical;
214
+ if (normalizedHeader.startsWith(`${synonym} `))
215
+ return canonical;
216
+ }
202
217
  return null;
203
218
  }
204
219
  function splitEntries(blockText) {
@@ -25,6 +25,10 @@
25
25
  */
26
26
  // eslint-disable-next-line @typescript-eslint/no-require-imports
27
27
  const io = require("./io.cjs");
28
+ // Phase 2 (#1646): route through the Hub per ADR-959 §III(B) line 75.
29
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
30
+ const cjsCommandRouterAdapter = require("./cjs-command-router-adapter.cjs");
31
+ const { routeHubCommandFamily } = cjsCommandRouterAdapter;
28
32
  // ─── routeAuditUat ────────────────────────────────────────────────────────────
29
33
  function routeAuditUat({ args, cwd, raw, error, _uat }) {
30
34
  // Suppress unused-variable warnings for args/error — this command has no
@@ -33,27 +37,61 @@ function routeAuditUat({ args, cwd, raw, error, _uat }) {
33
37
  void error;
34
38
  // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
35
39
  const u = _uat ?? require('./uat.cjs');
36
- u.cmdAuditUat(cwd, raw);
40
+ // Phase 2 (#1646): routes through the Hub for uniform observability and
41
+ // HandlerFailure taxonomy. audit-uat has no subcommands — a synthetic 'run'
42
+ // defaultSubcommand gives the Hub a single-handler manifest. The dispatch is
43
+ // trivial but the observability seam (DispatchEvent, GSD_AUDIT=1 trace) is
44
+ // now consistent with graphify/intel/host routers.
45
+ routeHubCommandFamily({
46
+ family: 'audit-uat',
47
+ args,
48
+ subcommands: ['run'],
49
+ defaultSubcommand: 'run',
50
+ handlers: {
51
+ run: () => u.cmdAuditUat(cwd, raw),
52
+ },
53
+ unknownMessage: (subcommand) => `Unknown audit-uat subcommand: "${subcommand}". audit-uat takes no subcommands.`,
54
+ error,
55
+ cwd,
56
+ raw,
57
+ });
37
58
  }
38
59
  // ─── routeAuditOpen ──────────────────────────────────────────────────────────
39
60
  function routeAuditOpen({ args, cwd, raw, error, _audit, _core }) {
40
- // Suppress unused-variable warning for error — audit-open has no subcommand
41
- // dispatch that would call error(); only flag parsing occurs here.
42
- void error;
43
61
  // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
44
62
  const a = _audit ?? require('./audit.cjs');
45
63
  const c = _core ?? io;
64
+ // Phase 2 (#1646): routes through the Hub for uniform observability.
65
+ // `--json` is a flag, not a subcommand — capture it in the closure and strip
66
+ // it from args before Hub dispatch so it isn't mistaken for a subcommand by
67
+ // the manifest check. The handler then branches on wantJson for the two
68
+ // output shapes (JSON object vs human-readable formatted report).
46
69
  const wantJson = args.includes('--json');
47
- const result = a.auditOpenArtifacts(cwd);
48
- if (wantJson) {
49
- // io.output JSON-stringifies its first arg; pass the object directly.
50
- c.output(result, raw);
51
- }
52
- else {
53
- // Human-readable report must bypass JSON encoding — use the rawValue
54
- // form (third arg) which io.output emits verbatim.
55
- c.output(null, true, a.formatAuditReport(result));
56
- }
70
+ const hubArgs = wantJson ? args.filter((arg) => arg !== '--json') : args;
71
+ routeHubCommandFamily({
72
+ family: 'audit-open',
73
+ args: hubArgs,
74
+ subcommands: ['run'],
75
+ defaultSubcommand: 'run',
76
+ handlers: {
77
+ run: () => {
78
+ const result = a.auditOpenArtifacts(cwd);
79
+ if (wantJson) {
80
+ // io.output JSON-stringifies its first arg; pass the object directly.
81
+ c.output(result, raw);
82
+ }
83
+ else {
84
+ // Human-readable report must bypass JSON encoding — use the rawValue
85
+ // form (third arg) which io.output emits verbatim.
86
+ c.output(null, true, a.formatAuditReport(result));
87
+ }
88
+ },
89
+ },
90
+ unknownMessage: (subcommand) => `Unknown audit-open subcommand: "${subcommand}". audit-open takes no subcommands (use --json for JSON output).`,
91
+ error,
92
+ cwd,
93
+ raw,
94
+ });
57
95
  }
58
96
  module.exports = {
59
97
  routeAuditUat,
@@ -280,6 +280,20 @@ function isSafeHookScriptPath(script) {
280
280
  function shellSingleQuote(value) {
281
281
  return "'" + value.replace(/'/g, "'\\''") + "'";
282
282
  }
283
+ /**
284
+ * #1634: build the emitted hook `command` for an ABSOLUTE confined script path. For `.js`-family
285
+ * hooks (`.js`/`.cjs`/`.mjs`) prefix with `node` so the hook runs regardless of the source's
286
+ * executable bit — a `git`/tarball source that lost `+x` would otherwise yield
287
+ * `/bin/sh: Permission denied` on every matching call (defect #2). This mirrors first-party hooks
288
+ * (`node "${CLAUDE_PLUGIN_ROOT}/hooks/x.js"`). The path stays POSIX single-quoted (#1460 (R) HIGH)
289
+ * so a space-containing install prefix cannot word-split or inject. Non-JS scripts (e.g. `.sh`)
290
+ * keep the bare single-quoted absolute path (unchanged) — they remain responsible for their own
291
+ * executability, exactly as before; per-runtime command projection is a separate concern (ADR-857 D8).
292
+ */
293
+ const JS_HOOK_EXT_RE = /\.(?:js|cjs|mjs)$/;
294
+ function runnableHookCommand(absScript) {
295
+ return JS_HOOK_EXT_RE.test(absScript) ? 'node ' + shellSingleQuote(absScript) : shellSingleQuote(absScript);
296
+ }
283
297
  /**
284
298
  * #1460 CONF-1: resolve a hook `script` (declared RELATIVE to the bundle) against the capability's
285
299
  * own install dir and CONFINE it via realpath, returning the ABSOLUTE confined path or null when it
@@ -459,6 +473,11 @@ function applyCapabilitySharedEdits(args) {
459
473
  const script = typeof rec['script'] === 'string' ? rec['script'] : '';
460
474
  if (!event || !script || isUnsafeKey(event))
461
475
  continue;
476
+ // #1634: optional tool-scoping `matcher` (a settings.json concept — entry-level sibling of
477
+ // `hooks`). Absent => match-all (field OMITTED so the existing shipped capabilities' wiring
478
+ // is byte-for-byte unchanged, Hyrum's Law). The validator gates this to a non-empty string.
479
+ const matcherRaw = rec['matcher'];
480
+ const matcher = typeof matcherRaw === 'string' && matcherRaw.length > 0 ? matcherRaw : null;
462
481
  // #1460 CONF-1: resolve the declared (relative) script against the capability's OWN install
463
482
  // dir and CONFINE via realpath, then write the ABSOLUTE confined path as the hook command —
464
483
  // never the raw relative path (which would resolve against the CWD at hook-exec time and could
@@ -467,14 +486,18 @@ function applyCapabilitySharedEdits(args) {
467
486
  const absScript = confinedBundleScript(capDir(runtimeDir, capId), script);
468
487
  if (absScript === null)
469
488
  continue;
470
- // #1460 (R) HIGH: the hook `command` is consumed by a shell (first-party hooks emit
471
- // `node "${CLAUDE_PLUGIN_ROOT}/hooks/x.js"`). The absolute path begins with the
472
- // (non-manifest) install-prefix, which commonly contains spaces — emit it POSIX
473
- // single-quoted so the prefix cannot word-split or inject. The script BASENAME is
474
- // already restricted to a shell-safe allowlist by isSafeHookScriptPath above.
475
- const command = shellSingleQuote(absScript);
489
+ // #1460 (R) HIGH + #1634: the hook `command` is consumed by a shell. `runnableHookCommand`
490
+ // emits a `node`-prefixed POSIX-single-quoted absolute path for `.js`-family hooks (runs
491
+ // without `+x`; mirrors first-party) and a bare single-quoted path otherwise. Single-quoting
492
+ // keeps a space-containing install prefix as one shell token (cannot word-split or inject).
493
+ const command = runnableHookCommand(absScript);
476
494
  const arr = Array.isArray(hooksObj[event]) ? hooksObj[event] : [];
477
- arr.push({ [CAP_MARKER]: capId, hooks: [{ type: 'command', command }] });
495
+ // #1634: stamp the marker so the entry is surgically strippable, and carry the declared
496
+ // `matcher` (entry-level sibling of `hooks`) only when the author declared one.
497
+ const entry = { [CAP_MARKER]: capId, hooks: [{ type: 'command', command }] };
498
+ if (matcher !== null)
499
+ entry['matcher'] = matcher;
500
+ arr.push(entry);
478
501
  hooksObj[event] = arr;
479
502
  touched = true;
480
503
  }