@opengsd/gsd-core 1.12.0 → 1.13.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.
Files changed (286) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +12 -0
  4. package/agents/gsd-executor.md +63 -35
  5. package/agents/gsd-plan-checker.md +76 -57
  6. package/agents/gsd-planner.md +14 -0
  7. package/agents/gsd-ui-checker.md +19 -3
  8. package/agents/gsd-ui-researcher.md +29 -0
  9. package/agents/gsd-verifier.md +23 -1
  10. package/bin/install.js +239 -67
  11. package/commands/gsd/execute-phase.md +1 -1
  12. package/commands/gsd/ns-workflow.md +2 -1
  13. package/commands/gsd/phase.md +1 -1
  14. package/commands/gsd/quick-batch.md +105 -0
  15. package/commands/gsd/surface.md +18 -8
  16. package/gsd-core/bin/gsd-tools.cjs +195 -50
  17. package/gsd-core/bin/lib/capability-activation.cjs +27 -0
  18. package/gsd-core/bin/lib/capability-registry.cjs +514 -114
  19. package/gsd-core/bin/lib/capability-state.cjs +7 -1
  20. package/gsd-core/bin/lib/capability-validator.cjs +120 -4
  21. package/gsd-core/bin/lib/capability-writer.cjs +14 -4
  22. package/gsd-core/bin/lib/check-command-router.cjs +85 -2
  23. package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
  24. package/gsd-core/bin/lib/clusters.cjs +1 -0
  25. package/gsd-core/bin/lib/command-aliases.cjs +16 -0
  26. package/gsd-core/bin/lib/commands.cjs +337 -13
  27. package/gsd-core/bin/lib/config-loader.cjs +3 -0
  28. package/gsd-core/bin/lib/core-utils.cjs +34 -7
  29. package/gsd-core/bin/lib/decisions.cjs +213 -1
  30. package/gsd-core/bin/lib/edge-probe.cjs +14 -1
  31. package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
  32. package/gsd-core/bin/lib/frontmatter.cjs +137 -23
  33. package/gsd-core/bin/lib/gap-checker.cjs +22 -13
  34. package/gsd-core/bin/lib/git-base-branch.cjs +10 -2
  35. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
  36. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +54 -11
  37. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +75 -22
  38. package/gsd-core/bin/lib/host-integration.cjs +57 -5
  39. package/gsd-core/bin/lib/init-command-router.cjs +14 -0
  40. package/gsd-core/bin/lib/init.cjs +132 -15
  41. package/gsd-core/bin/lib/install-engine.cjs +184 -12
  42. package/gsd-core/bin/lib/install-model-override-resolver.cjs +45 -0
  43. package/gsd-core/bin/lib/install-profiles.cjs +22 -14
  44. package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
  45. package/gsd-core/bin/lib/io.cjs +35 -0
  46. package/gsd-core/bin/lib/loop-resolver.cjs +14 -8
  47. package/gsd-core/bin/lib/markdown-table.cjs +123 -0
  48. package/gsd-core/bin/lib/milestone.cjs +22 -2
  49. package/gsd-core/bin/lib/phase-command-router.cjs +13 -6
  50. package/gsd-core/bin/lib/phase-id.cjs +251 -9
  51. package/gsd-core/bin/lib/phase.cjs +774 -35
  52. package/gsd-core/bin/lib/plan-document.cjs +10 -0
  53. package/gsd-core/bin/lib/planning-snapshot.cjs +147 -20
  54. package/gsd-core/bin/lib/planning-workspace.cjs +103 -28
  55. package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
  56. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
  57. package/gsd-core/bin/lib/quick-batch.cjs +840 -0
  58. package/gsd-core/bin/lib/review-lane-descriptor.cjs +53 -5
  59. package/gsd-core/bin/lib/review-lane-invocation.cjs +73 -1
  60. package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
  61. package/gsd-core/bin/lib/roadmap-parser.cjs +499 -26
  62. package/gsd-core/bin/lib/roadmap.cjs +187 -58
  63. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +233 -33
  64. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +16 -17
  65. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +286 -108
  66. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +215 -43
  67. package/gsd-core/bin/lib/shell-command-projection.cjs +4 -0
  68. package/gsd-core/bin/lib/smart-entry.cjs +7 -9
  69. package/gsd-core/bin/lib/state-document.cjs +30 -5
  70. package/gsd-core/bin/lib/state-md-schema.cjs +23 -13
  71. package/gsd-core/bin/lib/state-transition.cjs +333 -44
  72. package/gsd-core/bin/lib/state.cjs +684 -125
  73. package/gsd-core/bin/lib/surface.cjs +23 -8
  74. package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
  75. package/gsd-core/bin/lib/uat.cjs +1419 -515
  76. package/gsd-core/bin/lib/update-context.cjs +6 -2
  77. package/gsd-core/bin/lib/validate.cjs +230 -12
  78. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  79. package/gsd-core/bin/lib/verification.cjs +273 -12
  80. package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
  81. package/gsd-core/bin/lib/verify.cjs +346 -16
  82. package/gsd-core/bin/lib/workstream-inventory.cjs +20 -2
  83. package/gsd-core/bin/lib/worktree-safety.cjs +8 -0
  84. package/gsd-core/bin/shared/config-schema.manifest.json +8 -0
  85. package/gsd-core/bin/verify-reapply-patches.cjs +70 -3
  86. package/gsd-core/references/agent-contracts.md +3 -3
  87. package/gsd-core/references/edge-probe.md +17 -13
  88. package/gsd-core/references/execute-mvp-tdd.md +18 -16
  89. package/gsd-core/references/execute-phase-response-language.md +6 -0
  90. package/gsd-core/references/executor-examples.md +42 -0
  91. package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
  92. package/gsd-core/references/mvp-concepts.md +2 -2
  93. package/gsd-core/references/plan-checker-examples.md +41 -0
  94. package/gsd-core/references/planner-antipatterns.md +25 -0
  95. package/gsd-core/references/planner-chunked.md +5 -1
  96. package/gsd-core/references/planner-coupling.md +42 -0
  97. package/gsd-core/references/planner-quick-batch.md +71 -0
  98. package/gsd-core/references/planner-reviews.md +47 -0
  99. package/gsd-core/references/planner-revision.md +75 -2
  100. package/gsd-core/references/planning-config.md +2 -1
  101. package/gsd-core/references/response-language-directive.md +9 -0
  102. package/gsd-core/references/revision-loop.md +118 -11
  103. package/gsd-core/references/tdd.md +14 -9
  104. package/gsd-core/references/verifier-evidence-gate.md +160 -0
  105. package/gsd-core/templates/phase-prompt.md +4 -0
  106. package/gsd-core/templates/verification-report.md +5 -0
  107. package/gsd-core/workflows/add-backlog.md +2 -0
  108. package/gsd-core/workflows/add-phase.md +2 -0
  109. package/gsd-core/workflows/add-tests.md +1 -1
  110. package/gsd-core/workflows/add-todo.md +1 -1
  111. package/gsd-core/workflows/ai-integration-phase.md +1 -1
  112. package/gsd-core/workflows/analyze-dependencies.md +2 -0
  113. package/gsd-core/workflows/audit-fix.md +2 -0
  114. package/gsd-core/workflows/audit-milestone.md +2 -0
  115. package/gsd-core/workflows/audit-uat.md +2 -0
  116. package/gsd-core/workflows/autonomous.md +2 -0
  117. package/gsd-core/workflows/check-todos.md +1 -1
  118. package/gsd-core/workflows/cleanup.md +1 -1
  119. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +15 -13
  120. package/gsd-core/workflows/code-review-fix.md +2 -0
  121. package/gsd-core/workflows/code-review.md +73 -31
  122. package/gsd-core/workflows/complete-milestone.md +13 -4
  123. package/gsd-core/workflows/debug.md +1 -1
  124. package/gsd-core/workflows/diagnose-issues.md +5 -1
  125. package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -0
  126. package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
  127. package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
  128. package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
  129. package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
  130. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -0
  131. package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
  132. package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
  133. package/gsd-core/workflows/discuss-phase/modes/text.md +2 -0
  134. package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
  135. package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
  136. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  137. package/gsd-core/workflows/discuss-phase-power.md +2 -0
  138. package/gsd-core/workflows/discuss-phase.md +1 -1
  139. package/gsd-core/workflows/do.md +43 -13
  140. package/gsd-core/workflows/docs-update.md +1 -1
  141. package/gsd-core/workflows/edit-phase.md +2 -0
  142. package/gsd-core/workflows/eval-review.md +1 -1
  143. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +2 -0
  144. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +17 -1
  145. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +8 -2
  146. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -0
  147. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
  148. package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
  149. package/gsd-core/workflows/execute-phase.md +32 -14
  150. package/gsd-core/workflows/execute-plan.md +8 -8
  151. package/gsd-core/workflows/explore.md +2 -0
  152. package/gsd-core/workflows/extract-learnings.md +2 -0
  153. package/gsd-core/workflows/fast.md +6 -0
  154. package/gsd-core/workflows/forensics.md +2 -0
  155. package/gsd-core/workflows/graduation.md +1 -1
  156. package/gsd-core/workflows/health.md +1 -1
  157. package/gsd-core/workflows/help/modes/brief.md +2 -0
  158. package/gsd-core/workflows/help/modes/default.md +2 -0
  159. package/gsd-core/workflows/help/modes/full.md +12 -0
  160. package/gsd-core/workflows/help/modes/topic.md +2 -0
  161. package/gsd-core/workflows/help.md +2 -0
  162. package/gsd-core/workflows/import.md +3 -3
  163. package/gsd-core/workflows/inbox.md +1 -1
  164. package/gsd-core/workflows/ingest-docs.md +1 -1
  165. package/gsd-core/workflows/insert-phase.md +2 -0
  166. package/gsd-core/workflows/list-phase-assumptions.md +2 -0
  167. package/gsd-core/workflows/list-seeds.md +2 -0
  168. package/gsd-core/workflows/list-workspaces.md +2 -0
  169. package/gsd-core/workflows/manager.md +3 -3
  170. package/gsd-core/workflows/map-codebase.md +2 -0
  171. package/gsd-core/workflows/milestone-summary.md +2 -0
  172. package/gsd-core/workflows/mvp-phase.md +1 -1
  173. package/gsd-core/workflows/new-milestone.md +1 -1
  174. package/gsd-core/workflows/new-project.md +5 -3
  175. package/gsd-core/workflows/new-workspace.md +1 -1
  176. package/gsd-core/workflows/next.md +2 -0
  177. package/gsd-core/workflows/node-repair.md +2 -0
  178. package/gsd-core/workflows/note.md +2 -0
  179. package/gsd-core/workflows/onboard.md +1 -1
  180. package/gsd-core/workflows/pause-work.md +19 -4
  181. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
  182. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -0
  183. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +9 -0
  184. package/gsd-core/workflows/plan-phase.md +130 -12
  185. package/gsd-core/workflows/plan-review-convergence.md +102 -10
  186. package/gsd-core/workflows/plant-seed.md +1 -1
  187. package/gsd-core/workflows/pr-branch.md +11 -3
  188. package/gsd-core/workflows/profile-user.md +1 -1
  189. package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
  190. package/gsd-core/workflows/progress.md +25 -3
  191. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +37 -2
  192. package/gsd-core/workflows/quick/steps/research-phase.md +3 -3
  193. package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
  194. package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
  195. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
  196. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
  197. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
  198. package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
  199. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
  200. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
  201. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
  202. package/gsd-core/workflows/quick-batch.md +203 -0
  203. package/gsd-core/workflows/quick.md +13 -3
  204. package/gsd-core/workflows/reapply-patches.md +2 -0
  205. package/gsd-core/workflows/remove-phase.md +2 -0
  206. package/gsd-core/workflows/remove-workspace.md +1 -1
  207. package/gsd-core/workflows/resume-project.md +6 -2
  208. package/gsd-core/workflows/review.md +215 -10
  209. package/gsd-core/workflows/scan.md +2 -0
  210. package/gsd-core/workflows/section-manifest.json +12 -0
  211. package/gsd-core/workflows/secure-phase.md +1 -1
  212. package/gsd-core/workflows/session-report.md +2 -0
  213. package/gsd-core/workflows/settings-advanced.md +2 -0
  214. package/gsd-core/workflows/settings-integrations.md +9 -8
  215. package/gsd-core/workflows/settings.md +1 -1
  216. package/gsd-core/workflows/ship.md +10 -10
  217. package/gsd-core/workflows/sketch-wrap-up.md +2 -0
  218. package/gsd-core/workflows/sketch.md +1 -1
  219. package/gsd-core/workflows/smart-entry.md +1 -1
  220. package/gsd-core/workflows/spec-phase.md +24 -19
  221. package/gsd-core/workflows/spike-wrap-up.md +2 -0
  222. package/gsd-core/workflows/spike.md +1 -1
  223. package/gsd-core/workflows/stats.md +2 -0
  224. package/gsd-core/workflows/sync-skills.md +12 -4
  225. package/gsd-core/workflows/thread.md +2 -0
  226. package/gsd-core/workflows/transition.md +2 -0
  227. package/gsd-core/workflows/ui-phase.md +26 -5
  228. package/gsd-core/workflows/ui-review.md +1 -1
  229. package/gsd-core/workflows/ultraplan-phase.md +2 -0
  230. package/gsd-core/workflows/undo.md +1 -1
  231. package/gsd-core/workflows/update.md +41 -38
  232. package/gsd-core/workflows/validate-phase.md +1 -1
  233. package/gsd-core/workflows/verify-work.md +49 -3
  234. package/hooks/dist/gsd-check-update-worker.js +19 -2
  235. package/hooks/dist/gsd-context-monitor.js +283 -12
  236. package/hooks/dist/gsd-node-runner.sh +1 -0
  237. package/hooks/dist/gsd-prompt-guard.js +30 -5
  238. package/hooks/dist/gsd-read-guard.js +2 -0
  239. package/hooks/dist/gsd-read-injection-scanner.js +5 -5
  240. package/hooks/dist/gsd-secret-read-guard.js +1079 -0
  241. package/hooks/dist/gsd-statusline.js +7 -3
  242. package/hooks/dist/gsd-validate-commit.sh +444 -7
  243. package/hooks/dist/gsd-workflow-guard.js +2 -1
  244. package/hooks/dist/lib/git-cmd.js +210 -1
  245. package/hooks/dist/lib/injection-patterns.js +36 -6
  246. package/hooks/dist/managed-hooks-registry.cjs +1 -0
  247. package/hooks/gsd-check-update-worker.js +19 -2
  248. package/hooks/gsd-context-monitor.js +283 -12
  249. package/hooks/gsd-node-runner.sh +1 -0
  250. package/hooks/gsd-prompt-guard.js +30 -5
  251. package/hooks/gsd-read-guard.js +2 -0
  252. package/hooks/gsd-read-injection-scanner.js +5 -5
  253. package/hooks/gsd-secret-read-guard.js +1079 -0
  254. package/hooks/gsd-statusline.js +7 -3
  255. package/hooks/gsd-validate-commit.sh +444 -7
  256. package/hooks/gsd-workflow-guard.js +2 -1
  257. package/hooks/hooks.json +6 -0
  258. package/hooks/lib/git-cmd.js +210 -1
  259. package/hooks/lib/injection-patterns.js +36 -6
  260. package/hooks/managed-hooks-registry.cjs +1 -0
  261. package/package.json +5 -5
  262. package/scripts/build-hooks.js +11 -4
  263. package/scripts/ci-test-scope.cjs +7 -0
  264. package/scripts/docs-guard-registry.cjs +10 -0
  265. package/scripts/gen-loop-host-contract.cjs +67 -15
  266. package/scripts/lib/shellcheck-fetch.cjs +247 -0
  267. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
  268. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  269. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  270. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +5 -0
  271. package/scripts/lint-phase-enumeration-drift.cjs +24 -6
  272. package/scripts/lint-phase-id-drift.cjs +133 -8
  273. package/scripts/lint-portable-grep.cjs +176 -0
  274. package/scripts/lint-response-language-coverage.cjs +524 -0
  275. package/scripts/lint-test-file-count.allowlist.json +3 -1
  276. package/scripts/lint-workflow-shellcheck-baseline.json +1027 -0
  277. package/scripts/lint-workflow-shellcheck.cjs +614 -0
  278. package/scripts/npm-audit-baseline.cjs +376 -0
  279. package/scripts/prompt-injection-scan.sh +8 -0
  280. package/scripts/require-issue-link-policy.cjs +16 -1
  281. package/skills/gsd-execute-phase/SKILL.md +1 -1
  282. package/skills/gsd-ns-workflow/SKILL.md +1 -0
  283. package/skills/gsd-phase/SKILL.md +1 -1
  284. package/skills/gsd-quick-batch/SKILL.md +105 -0
  285. package/skills/gsd-surface/SKILL.md +18 -8
  286. package/vscode/package.json +1 -1
@@ -114,6 +114,50 @@ function readGsdEffectiveModelOverrides(targetDir = null, options = {}) {
114
114
  // Per-project wins on conflict; preserve non-conflicting global keys.
115
115
  return { ...(global || {}), ...(projectOverrides || {}) };
116
116
  }
117
+ function readGsdAgentTools(config) {
118
+ const raw = config?.agent_tools;
119
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
120
+ return null;
121
+ const result = {};
122
+ for (const [selector, value] of Object.entries(raw)) {
123
+ // An explicitly present selector always has a verdict. Invalid values are
124
+ // empty so a project config cannot accidentally restore a global grant.
125
+ result[selector] = Array.isArray(value)
126
+ ? value.filter((entry) => typeof entry === 'string')
127
+ .map((entry) => entry.trim())
128
+ .filter((entry) => entry.length > 0
129
+ && !entry.endsWith(':')
130
+ && !/[\s\u0000-\u001F\u007F-\u009F,#"'\u2028\u2029]/.test(entry))
131
+ : [];
132
+ }
133
+ return result;
134
+ }
135
+ /**
136
+ * Resolve valid install-time `agent_tools` grants from global defaults and
137
+ * the nearest project config. Project selectors replace only matching global
138
+ * selectors; malformed whole files remain harmless absence like the existing
139
+ * model override resolver.
140
+ */
141
+ function readGsdEffectiveAgentTools(targetDir = null, options = {}) {
142
+ const home = options.homedir ? options.homedir() : node_os_1.default.homedir();
143
+ const globalConfig = _readGsdConfigFile(node_path_1.default.join(home, '.gsd', 'defaults.json'), 'global defaults');
144
+ let projectConfig = null;
145
+ if (targetDir) {
146
+ const candidate = _findAncestorGsdConfigPath(targetDir);
147
+ if (candidate) {
148
+ projectConfig = _readGsdConfigFile(candidate, 'project config');
149
+ }
150
+ }
151
+ const global = readGsdAgentTools(globalConfig);
152
+ const project = readGsdAgentTools(projectConfig);
153
+ if (projectConfig
154
+ && Object.prototype.hasOwnProperty.call(projectConfig, 'agent_tools')
155
+ && project === null)
156
+ return {};
157
+ if (!global && !project)
158
+ return null;
159
+ return { ...(global || {}), ...(project || {}) };
160
+ }
117
161
  /**
118
162
  * Build a runtime-aware tier resolver for the install path (#2517).
119
163
  *
@@ -230,6 +274,7 @@ function resolveAgentModelOverride(agentName, modelOverrides, runtimeResolver) {
230
274
  module.exports = {
231
275
  readGsdGlobalModelOverrides,
232
276
  readGsdEffectiveModelOverrides,
277
+ readGsdEffectiveAgentTools,
233
278
  readGsdRuntimeProfileResolver,
234
279
  resolveAgentModelOverride,
235
280
  };
@@ -32,7 +32,7 @@ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs")
32
32
  const external_descriptor_trust_cjs_1 = require("./external-descriptor-trust.cjs");
33
33
  // eslint-disable-next-line @typescript-eslint/no-require-imports
34
34
  const conversionModule = require("./runtime-artifact-conversion.cjs");
35
- const { applyAgentPathRewrites: _applyAgentPathRewrites, processAttribution: _processAttribution, normalizeAgentBodyForRuntime: _normalizeAgentBodyForRuntime, readGsdCommandNames: _readGsdCommandNames, deriveAgentName: _deriveAgentName, applyAgentFrontmatterExtensions: _applyAgentFrontmatterExtensions, } = conversionModule;
35
+ const { applyAgentPathRewrites: _applyAgentPathRewrites, processAttribution: _processAttribution, normalizeAgentBodyForRuntime: _normalizeAgentBodyForRuntime, readGsdCommandNames: _readGsdCommandNames, deriveAgentName: _deriveAgentName, applyAgentFrontmatterExtensions: _applyAgentFrontmatterExtensions, appendAgentTools: _appendAgentTools, } = conversionModule;
36
36
  // #2995 (epic #1671 Phase 6.4): agent bodies join the fragment model. Markers are
37
37
  // stripped at emit BEFORE any path rewrite or converter runs, so a `.claude/` ->
38
38
  // `.windsurf/` regex can never reach inside a marker attribute and corrupt it —
@@ -40,6 +40,8 @@ const { applyAgentPathRewrites: _applyAgentPathRewrites, processAttribution: _pr
40
40
  // eslint-disable-next-line @typescript-eslint/no-require-imports
41
41
  const workflowFragmentsModule = require("./workflow-fragments.cjs");
42
42
  const { composeWorkflow: _composeWorkflow } = workflowFragmentsModule;
43
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
44
+ const installModelOverrideResolver = require("./install-model-override-resolver.cjs");
43
45
  // ---------------------------------------------------------------------------
44
46
  // Profile definitions
45
47
  // ---------------------------------------------------------------------------
@@ -896,14 +898,14 @@ function stageSkillsForRuntimeAsSkills(srcCommandsDir, resolvedProfile, converte
896
898
  * agent loop in bin/install.js exactly:
897
899
  * 1. applyAgentPathRewrites (4 base ~/.claude/ regexes; skipped for copilot/antigravity)
898
900
  * 2. processAttribution (Co-Authored-By policy)
899
- * 3. converter (runtime-specific frontmatter/body transform)
900
- * 4. applyAgentFrontmatterExtensions (#2875 Part 2: effort/disallowedTools,
901
+ * 3. appendAgentTools (#4032: validated agent_tools grants, before host conversion)
902
+ * 4. converter (runtime-specific frontmatter/body transform)
903
+ * 5. applyAgentFrontmatterExtensions (#2875 Part 2: effort/disallowedTools,
901
904
  * gated by hostBehaviors.agentFrontmatterExtensions — no-op for a runtime
902
905
  * that declares nothing, e.g. every non-Claude runtime today)
903
- * 5. normalizeAgentBodyForRuntime (colon→hyphen refs; no-op for trivial group)
904
- * When `agentCtx` is absent, only the converter is applied (backward-compat for
905
- * the feat-1173 synthetic-descriptor tests and the copilot/antigravity paths
906
- * that handle cross-cutting inside their converters).
906
+ * 6. normalizeAgentBodyForRuntime (colon→hyphen refs; no-op for trivial group)
907
+ * When `agentCtx` is absent, only global `agent_tools` augmentation and the
908
+ * converter run; other cross-cutting remains absent for backward compatibility.
907
909
  *
908
910
  * @param srcAgentsDir source agents directory (e.g. agents/)
909
911
  * @param resolvedProfile profile filter from resolveProfile()
@@ -917,8 +919,7 @@ function stageSkillsForRuntimeAsSkills(srcCommandsDir, resolvedProfile, converte
917
919
  * every OTHER converter's contract — converters that don't declare a
918
920
  * 3rd parameter simply never read it.
919
921
  * @param isGlobal install scope passed through to the converter
920
- * @param agentCtx optional cross-cutting context (ADR-1235 §1); when absent,
921
- * only the converter is applied (backward compat)
922
+ * @param agentCtx optional cross-cutting context (ADR-1235 §1)
922
923
  */
923
924
  function stageAgentsForRuntimeWithConverter(srcAgentsDir, resolvedProfile, converter, isGlobal = false, agentCtx) {
924
925
  if (!installFs().existsSync(srcAgentsDir))
@@ -950,6 +951,7 @@ function stageAgentsForRuntimeWithConverter(srcAgentsDir, resolvedProfile, conve
950
951
  try {
951
952
  // Resolve cmdNames once per staging call (not per file) for performance.
952
953
  const cmdNames = agentCtx ? _readGsdCommandNames() : [];
954
+ const agentTools = installModelOverrideResolver.readGsdEffectiveAgentTools(agentCtx?.projectDir ?? agentCtx?.targetDir ?? null);
953
955
  for (const entry of entries) {
954
956
  if (!entry.isFile())
955
957
  continue;
@@ -969,25 +971,31 @@ function stageAgentsForRuntimeWithConverter(srcAgentsDir, resolvedProfile, conve
969
971
  // throws loudly naming the file for a malformed marker, never emitting a
970
972
  // half-composed agent.
971
973
  content = _composeWorkflow(content, { sourcePath: agentSourcePath });
974
+ const agentName = _deriveAgentName(entry.name);
975
+ const grants = [
976
+ ...(agentTools?.['*'] || []),
977
+ ...(agentTools?.[agentName] || []),
978
+ ];
972
979
  if (agentCtx) {
973
980
  // #2875 Part 2 / row I3: derived exactly as the inline loop does —
974
981
  // single-sourced via deriveAgentName (runtime-artifact-conversion.cts).
975
- const agentName = _deriveAgentName(entry.name);
976
982
  // ADR-1235 §1: pre-converter cross-cutting (matches inline loop order exactly)
977
983
  // Step 1: path rewrites (4 base ~/.claude/ regexes; skipped for copilot/antigravity)
978
984
  content = _applyAgentPathRewrites(content, agentCtx.runtime, agentCtx.pathPrefix);
979
985
  // Step 2: attribution
980
986
  content = _processAttribution(content, agentCtx.attribution);
981
- // Step 3: converter (runtime-specific frontmatter/body transform)
987
+ // Step 3: validated canonical grants, before host conversion.
988
+ content = _appendAgentTools(content, grants);
989
+ // Step 4: converter (runtime-specific frontmatter/body transform)
982
990
  content = converter(content, isGlobal, { agentName });
983
- // Step 4: frontmatter extensions (effort/disallowedTools; no-op unless
991
+ // Step 5: frontmatter extensions (effort/disallowedTools; no-op unless
984
992
  // the runtime declares hostBehaviors.agentFrontmatterExtensions)
985
993
  content = _applyAgentFrontmatterExtensions(content, { runtime: agentCtx.runtime, agentName, targetDir: agentCtx.targetDir });
986
- // Step 5: normalize colon→hyphen refs (no-op for trivial group)
994
+ // Step 6: normalize colon→hyphen refs (no-op for trivial group)
987
995
  content = _normalizeAgentBodyForRuntime(content, agentCtx.runtime, cmdNames);
988
996
  }
989
997
  else {
990
- // Backward-compat: only apply the converter (no cross-cutting)
998
+ content = _appendAgentTools(content, grants);
991
999
  content = converter(content, isGlobal);
992
1000
  }
993
1001
  installFs().writeFileSync(node_path_1.default.join(stageDir, entry.name), content, 'utf8');
@@ -55,6 +55,7 @@ exports.BUNDLED_GSD_HOOK_FILES = Object.freeze(new Set([
55
55
  'hooks/gsd-prompt-guard.js',
56
56
  'hooks/gsd-read-guard.js',
57
57
  'hooks/gsd-read-injection-scanner.js',
58
+ 'hooks/gsd-secret-read-guard.js',
58
59
  'hooks/gsd-session-state.sh',
59
60
  'hooks/gsd-statusline.js',
60
61
  'hooks/gsd-update-banner.js',
@@ -205,6 +205,40 @@ function output(result, raw, rawValue) {
205
205
  // and skipping process.exit() lets the event loop drain naturally.
206
206
  writeAllSync(1, data);
207
207
  }
208
+ /**
209
+ * Single owner of the "a no-op decline reports the real condition" pairing:
210
+ * a `[gsd-tools] WARNING:` stderr disclosure plus the matching
211
+ * `{ [flagKey]: false, ...computed, reason }` JSON payload (#3957, epic
212
+ * #3473 B9). Before this helper, each CLI command's no-op arm hand-wrote
213
+ * both halves independently, and #3957's own sweep found sites where one
214
+ * half drifted: a decline that discarded values it had already computed, a
215
+ * `reason` string naming a condition that hadn't actually fired, or no
216
+ * stderr disclosure at all (missing the `[gsd-tools] WARNING:` convention
217
+ * #3217, ADR-3180 §7.6 rule 4, established for exactly this situation —
218
+ * a JSON `reason` field alone is easy for a caller piping stdout through
219
+ * `--json` to never read). Routing every no-op decline through one call
220
+ * site makes the convention structural — a call site, not a habit a fifth
221
+ * arm can independently forget — rather than leaving it to per-site
222
+ * discipline.
223
+ *
224
+ * `disclosure` is written to stderr VERBATIM. Like `error()`'s `message`
225
+ * argument (see that function's own doc comment above `formatDiagnosticToken`),
226
+ * this function stays a dumb, faithful writer and does not sanitize it — a
227
+ * caller that interpolates an UNTRUSTED substring (a caller-supplied
228
+ * blocker/session-field string, an argv token) into `disclosure` MUST pass
229
+ * that substring through `formatDiagnosticToken` first. Skipping that step
230
+ * lets an embedded `\n` forge a second, attacker-authored
231
+ * `[gsd-tools] WARNING:` line — the same risk `error()`'s doc comment
232
+ * documents for its own stderr write.
233
+ */
234
+ function declineNoOp(raw, flagKey, reason, disclosure, computed = {}) {
235
+ // `process.stderr.write`, matching the `[gsd-tools] WARNING:` call sites
236
+ // this helper replaces (state.cts's stateReplaceFieldWithFallback and the
237
+ // cmdStateUpdateProgress decline arms) — not the `writeAllSync(2, …)`
238
+ // primitive `error()` uses, which is reserved for the fatal-exit path.
239
+ process.stderr.write(`[gsd-tools] WARNING: ${disclosure}\n`);
240
+ output({ [flagKey]: false, ...computed, reason }, raw, 'false');
241
+ }
208
242
  /**
209
243
  * Frozen enum of typed reason codes used by error() for structured errors.
210
244
  * Each subcommand contributes its own codes; the enum exists so tests can
@@ -392,6 +426,7 @@ module.exports = {
392
426
  ensureGsdTempDir,
393
427
  reapStaleTempFiles,
394
428
  output,
429
+ declineNoOp,
395
430
  serializeForOutput,
396
431
  ERROR_REASON,
397
432
  setJsonErrorMode,
@@ -40,7 +40,7 @@ const { resolveCapabilityRuntimeState } = capabilityStateModule;
40
40
  // ─── Capability-activation engine (single owner for config-key precedence) ────
41
41
  // eslint-disable-next-line @typescript-eslint/no-require-imports
42
42
  const capabilityActivationModule = require("./capability-activation.cjs");
43
- const { _getNestedConfigValue, _readRawConfigKey, _resolveActivationValue, resolveConfigKey } = capabilityActivationModule;
43
+ const { _getNestedConfigValue, _readRawConfigKey, _resolveActivationValue, _resolvePointGate, resolveConfigKey } = capabilityActivationModule;
44
44
  // ─── Canonical points (derived from LOOP_HOST_CONTRACT — authoritative 12) ───
45
45
  // FIX 2: Derive the authoritative canonical set from LOOP_HOST_CONTRACT so it
46
46
  // cannot drift from the host contract. CANONICAL_POINTS_FALLBACK is kept as an
@@ -125,13 +125,15 @@ function resolveLoopHooks(input) {
125
125
  // Helper: check activation using single-key precedence resolver (FIX 1 + FIX 3)
126
126
  function isActive(hook) {
127
127
  const when = hook['when'];
128
- // No `when` → unconditional hook, always active
129
- if (when === undefined || when === null)
130
- return true;
131
- // FIX 3: `when` present but not a non-empty string → malformed registry data → INACTIVE
132
- if (typeof when !== 'string' || when.length === 0)
133
- return false;
134
- return _resolveActivationValue(when, config, cwd, registry);
128
+ if (when !== undefined && when !== null) {
129
+ // FIX 3: `when` present but not a non-empty string → malformed registry data → INACTIVE
130
+ if (typeof when !== 'string' || when.length === 0)
131
+ return false;
132
+ if (!_resolveActivationValue(when, config, cwd, registry))
133
+ return false;
134
+ }
135
+ // #3661: optional point-selection gate — see capability-activation.cts.
136
+ return _resolvePointGate(hook['pointFrom'], point, config, cwd, registry);
135
137
  }
136
138
  function isCapabilityActive(capId) {
137
139
  if (!capabilityStatesById)
@@ -555,6 +557,10 @@ module.exports = {
555
557
  // Re-exported for identity parity guard (FIX 2: resolveConfigValues in this module
556
558
  // calls resolveConfigKey; exporting it here makes the single-owner contract testable).
557
559
  resolveConfigKey,
560
+ // #3661: re-exported for the same identity parity guard — isActive calls
561
+ // _resolvePointGate; exporting it here makes the single-owner contract testable
562
+ // (see tests/capability-precedence-parity.test.cjs's identity guard describe block).
563
+ _resolvePointGate,
558
564
  CANONICAL_POINTS_FALLBACK,
559
565
  CANONICAL_POINTS,
560
566
  };
@@ -23,6 +23,7 @@ exports.findTableBySchema = findTableBySchema;
23
23
  exports.findTableWithColumns = findTableWithColumns;
24
24
  exports.escapeCell = escapeCell;
25
25
  exports.appendQuickTaskRow = appendQuickTaskRow;
26
+ exports.migrateQuickTasksTable = migrateQuickTasksTable;
26
27
  exports.resetQuickTaskRows = resetQuickTaskRows;
27
28
  const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
28
29
  // ─── Schema registry ──────────────────────────────────────────────────────────
@@ -776,6 +777,128 @@ function appendQuickTaskRow(stateContent, fields) {
776
777
  const content = (0, markdown_sectionizer_cjs_1.replaceSection)(stateContent, section, newBody);
777
778
  return { ok: true, value: { content, row, variant: match.label } };
778
779
  }
780
+ // ─── migrateQuickTasksTable (#3730 option b) ───────────────────────────────
781
+ /**
782
+ * Column-name → canonical-cell mapping for migrating a legacy Quick Tasks table
783
+ * (#3730). Name-based, case-insensitive; every column NOT in this map lands in
784
+ * the Description bucket (joined with ' · ' in original column order, unknown
785
+ * names kept as `name: value`) so no historical datum is dropped.
786
+ */
787
+ const QUICK_TASKS_COLUMN_ALIASES = {
788
+ '#': '#', id: '#',
789
+ date: 'Date', when: 'Date',
790
+ commit: 'Commit', sha: 'Commit',
791
+ status: 'Status',
792
+ directory: 'Directory', artifacts: 'Directory', path: 'Directory', dir: 'Directory',
793
+ description: 'Description', task: 'Description', summary: 'Description',
794
+ slug: 'Description', scope: 'Description', title: 'Description',
795
+ };
796
+ /**
797
+ * Migrate STATE.md's "Quick Tasks Completed" table onto the canonical
798
+ * `with-status` schema (#3730, maintainer decision 2026-09-02: option b).
799
+ *
800
+ * The pre-registry prose template licensed arbitrary column shapes GSD itself
801
+ * emitted, and `appendQuickTaskRow` fails loud on every one of them — leaving a
802
+ * project permanently unappendable. This is the supported repair path: the
803
+ * `gsd-tools quick-tasks-migrate` subcommand, plus an automatic check on the
804
+ * first `quick`/`fast` run (the workflow calls this before appending), which is
805
+ * a silent no-op when the section is absent, the table is already canonical, or
806
+ * the user never runs quick at all.
807
+ *
808
+ * Same section-selection, parse, and fail-loud posture as `appendQuickTaskRow`
809
+ * (ADR-2143 §7 upheld — append still rejects; this REPAIRS). Returns
810
+ * `{ok:true, value:{content, migrated:false}}` for the no-op cases so callers
811
+ * can stay silent, and `{migrated:true, from, rows}` when a rewrite happened so
812
+ * the CLI can report exactly what moved.
813
+ */
814
+ function migrateQuickTasksTable(stateContent) {
815
+ const section = selectQuickTasksSection(stateContent);
816
+ if (!section) {
817
+ return { ok: true, value: { content: stateContent, migrated: false } };
818
+ }
819
+ const parsed = parseMarkdownTable(section.body);
820
+ if (!parsed.ok) {
821
+ return { ok: false, reason: `quick-tasks table: ${parsed.reason}` };
822
+ }
823
+ const match = matchTableSchema(parsed.value.columns);
824
+ if (match && match.id === 'QuickTasks') {
825
+ return { ok: true, value: { content: stateContent, migrated: false } };
826
+ }
827
+ const canonical = exports.TABLE_SCHEMAS.QuickTasks.find((v) => v.label === 'with-status').columns;
828
+ // Map each legacy column to its canonical target; unmapped names keep their
829
+ // identity so the Description bucket can render them losslessly.
830
+ const targets = parsed.value.columns.map((c) => QUICK_TASKS_COLUMN_ALIASES[c.trim().toLowerCase()] ?? null);
831
+ const rows = parsed.value.rows.map((row, rowIdx) => {
832
+ const bucket = {
833
+ '#': '', Description: '', Date: '', Commit: '', Status: '', Directory: '',
834
+ };
835
+ const descriptionParts = [];
836
+ // parseMarkdownTable hands back name-keyed records (rows[i][columnName]).
837
+ parsed.value.columns.forEach((col, i) => {
838
+ const raw = (row[col] ?? '').trim();
839
+ const target = targets[i];
840
+ if (target === 'Description') {
841
+ if (raw)
842
+ descriptionParts.push(raw);
843
+ }
844
+ else if (target) {
845
+ // Collision-safe (#3730 review): two legacy columns aliasing the same
846
+ // canonical target must not silently overwrite — the displaced value
847
+ // joins the Description bucket under its own name, same convention as
848
+ // an unknown column, so no historical datum is dropped.
849
+ if (raw && bucket[target]) {
850
+ descriptionParts.push(`${col.trim()}: ${raw}`);
851
+ }
852
+ else if (raw) {
853
+ bucket[target] = raw;
854
+ }
855
+ }
856
+ else if (raw) {
857
+ descriptionParts.push(`${col.trim()}: ${raw}`);
858
+ }
859
+ });
860
+ bucket['#'] = bucket['#'] || String(rowIdx + 1);
861
+ bucket.Description = descriptionParts.join(' · ');
862
+ bucket.Date = bucket.Date || '—';
863
+ bucket.Commit = bucket.Commit || '—';
864
+ bucket.Status = bucket.Status || '—';
865
+ bucket.Directory = bucket.Directory || '—';
866
+ return `| ${canonical.map((c) => escapeCell(bucket[c])).join(' | ')} |`;
867
+ });
868
+ const eol = /\r\n/.test(section.body) ? '\r\n' : '\n';
869
+ const lines = section.body.split(/\r?\n/);
870
+ // CONTIGUOUS-run bound (#3730 review), mirroring resetQuickTaskRows: the
871
+ // section body may carry a SECOND table (a `#### Notes` subsection or a
872
+ // trailing table after a blank line) that parseMarkdownTable never read —
873
+ // scanning for the last pipe line anywhere in the body would splice the
874
+ // rewrite across it and silently destroy it. Only the first CONTIGUOUS run
875
+ // of pipe lines — the table that was parsed — is replaced.
876
+ const firstTableLineIdx = lines.findIndex((l) => l.trim().startsWith('|'));
877
+ let lastTableLineIdx = firstTableLineIdx;
878
+ while (lastTableLineIdx + 1 < lines.length && lines[lastTableLineIdx + 1].trim().startsWith('|')) {
879
+ lastTableLineIdx++;
880
+ }
881
+ const header = `| ${canonical.join(' | ')} |`;
882
+ // Widths match workflows/quick.md's canonical template byte-for-byte
883
+ // (#3730 review): its delimiter is max(3, len+2) per column.
884
+ const delimiter = `| ${canonical.map((c) => '-'.repeat(Math.max(3, c.length + 2))).join(' | ')} |`;
885
+ const newBody = [
886
+ ...lines.slice(0, firstTableLineIdx),
887
+ header,
888
+ delimiter,
889
+ ...rows,
890
+ ...lines.slice(lastTableLineIdx + 1),
891
+ ].join(eol);
892
+ return {
893
+ ok: true,
894
+ value: {
895
+ content: (0, markdown_sectionizer_cjs_1.replaceSection)(stateContent, section, newBody),
896
+ migrated: true,
897
+ from: parsed.value.columns,
898
+ rows: rows.length,
899
+ },
900
+ };
901
+ }
779
902
  // ─── resetQuickTaskRows (#2142) ────────────────────────────────────────────
780
903
  /**
781
904
  * Clear every DATA row from STATE.md's "Quick Tasks Completed" table, leaving
@@ -55,7 +55,7 @@ const { scanPhasePlans } = planScanMod;
55
55
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-locator.cjs is an export= CommonJS module
56
56
  const phaseLocatorMod = require("./phase-locator.cjs");
57
57
  const { listMilestonePhaseDirs } = phaseLocatorMod;
58
- const { planningPaths } = planningWorkspace;
58
+ const { planningPaths, resolvePhaseIdConvention } = planningWorkspace;
59
59
  const { extractFrontmatter } = frontmatterMod;
60
60
  // ADR-3408 §8.3 / #3469: `writeStateMd` gets sync and NO preservation — the
61
61
  // same #3374-shaped exposure the milestone-complete write used to carry (a
@@ -702,6 +702,22 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
702
702
  let totalPlans = 0;
703
703
  let totalTasks = 0;
704
704
  const accomplishments = [];
705
+ // #2761 (round-11 BLOCKER): resolved ONCE, ambiently (no explicit `ws` —
706
+ // this function has none of its own and already resolves everything else
707
+ // off `cwd` via planningPaths(cwd), matching the ambient-workstream
708
+ // contract `resolvePhaseIdConvention`/`planningDir` share), and threaded
709
+ // into the single `listMilestonePhaseDirs` call directly below. Left
710
+ // unthreaded, `phaseIdConvention` stays `undefined` and
711
+ // `getMilestonePhaseFilter` silently resolves it FROM CONFIG itself —
712
+ // which is exactly the behavior the changeset originally (and wrongly)
713
+ // claimed this PR does not reach: on a bracket-convention project that
714
+ // lazily-resolved convention changes `milestonePhaseDirs`/
715
+ // `milestonePhaseScope` below, and this function goes on to ARCHIVE
716
+ // (rename/move) whatever that set names. Resolving explicitly here removes
717
+ // the silent-inherit path without changing behavior for `null` /
718
+ // `milestone-prefixed` projects, whose resolved convention is the same
719
+ // value `undefined` would have lazily produced anyway.
720
+ const phaseConvention = resolvePhaseIdConvention(cwd);
705
721
  // #3597 (ADR-3180 Decision 2): SINGLE resolution of "which phase
706
722
  // directories belong to the current milestone" AND the SCOPE discriminator
707
723
  // that resolution came from — shared verbatim by the read-only stats loop
@@ -715,7 +731,11 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
715
731
  // disk, same as before #3597. Only the destructive archive pass (and its
716
732
  // --dry-run preview) refuses to act on a non-COMPLETE (non-answer) scope;
717
733
  // see the guard built from `milestonePhaseScope` further down.
718
- const { value: milestonePhaseDirs, scope: milestonePhaseScope } = listMilestonePhaseDirs(phasesDir, { cwd, versionOverride: version });
734
+ const { value: milestonePhaseDirs, scope: milestonePhaseScope } = listMilestonePhaseDirs(phasesDir, {
735
+ cwd,
736
+ versionOverride: version,
737
+ phaseIdConvention: phaseConvention,
738
+ });
719
739
  try {
720
740
  for (const dir of milestonePhaseDirs) {
721
741
  phaseCount++;
@@ -6,7 +6,7 @@
6
6
  * Unsupported in this router:
7
7
  * - scaffold: routed through top-level scaffold command.
8
8
  *
9
- * CJS-only subcommands: mvp-mode (dispatched directly, before hub).
9
+ * CJS-only subcommands: mvp-mode, tdd-applicable (dispatched directly, before hub).
10
10
  *
11
11
  * #3788: dispatch is mediated by CommandRoutingHub. The public entry point
12
12
  * and observable CLI behaviour are unchanged.
@@ -57,6 +57,13 @@ function routePhaseCommand({ phase, args, cwd, raw, error }) {
57
57
  phase.cmdPhaseMvpMode(cwd, args.slice(2), raw);
58
58
  return;
59
59
  }
60
+ // `tdd-applicable` (#4273): same CJS-native dispatch shape as `mvp-mode`
61
+ // immediately above — a precedence cascade over plan/task/config sources
62
+ // with its own typed JSON result, not routed through the SDK query layer.
63
+ if (subcommand === 'tdd-applicable') {
64
+ phase.cmdPhaseTddApplicable(cwd, args.slice(2), raw);
65
+ return;
66
+ }
60
67
  // ── Build the CJS registry ──────────────────────────────────────────────────
61
68
  // Each handler receives a ctx object from the hub and must return a HubResult.
62
69
  const cjsRegistry = {
@@ -224,14 +231,14 @@ function routePhaseCommand({ phase, args, cwd, raw, error }) {
224
231
  // ── Build manifest (available subcommands for UnknownCommand detection) ─────
225
232
  // `availableSubcommands` is what the error message shows. It excludes
226
233
  // unsupported commands (already handled above) but does NOT include 'mvp-mode'
227
- // because it was absent from PHASE_SUBCOMMANDS in the original and was not
228
- // shown in the "Available:" list there either.
234
+ // or 'tdd-applicable' (#4273) because both are absent from PHASE_SUBCOMMANDS
235
+ // and were not shown in the "Available:" list there either.
229
236
  //
230
237
  // `manifestSubcommands` is the full routing set for the hub — it includes
231
- // 'mvp-mode' (which the original code routed via a handler even without a
232
- // manifest entry) so the hub's UnknownCommand check passes for it.
238
+ // 'mvp-mode' and 'tdd-applicable' (both routed via a handler even without a
239
+ // manifest entry) so the hub's UnknownCommand check passes for them.
233
240
  const availableSubcommands = command_aliases_cjs_1.PHASE_SUBCOMMANDS.filter(s => !UNSUPPORTED[s]);
234
- const manifestSubcommands = ['mvp-mode', ...availableSubcommands];
241
+ const manifestSubcommands = ['mvp-mode', 'tdd-applicable', ...availableSubcommands];
235
242
  const manifest = { phase: manifestSubcommands };
236
243
  // ── Construct hub ──────────────────────────────────────────────────────────
237
244
  // #175: Hub is CJS-only — no mode param, no sdkLoader.