@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
@@ -122,6 +122,7 @@ function readEffectiveGitConfig(cwd, deps) {
122
122
  config = {
123
123
  base_branch: _readGitKey(normalized, 'base_branch'),
124
124
  protected_branches: _readGitNested(normalized, 'protected_branches'),
125
+ allow_default_branch_commits: _readGitNested(normalized, 'allow_default_branch_commits'),
125
126
  };
126
127
  }
127
128
  }
@@ -161,7 +162,10 @@ function readEffectiveGitConfig(cwd, deps) {
161
162
  // Not a list at all — contributes no names, but is still a misconfiguration.
162
163
  rejectedProtectedBranches.push(renderRejected(rawProtectedBranches));
163
164
  }
164
- return { baseBranch, protectedBranches, rejectedProtectedBranches };
165
+ // #3819: nested-only read (mirrors protected_branches — new key, no legacy
166
+ // flat form, so a top-level spelling must not become an undocumented alias).
167
+ const allowDefaultBranchCommits = config.allow_default_branch_commits === true;
168
+ return { baseBranch, protectedBranches, rejectedProtectedBranches, allowDefaultBranchCommits };
165
169
  }
166
170
  /**
167
171
  * Try `git symbolic-ref --short refs/remotes/origin/HEAD` (no network).
@@ -302,13 +306,17 @@ function resolveBaseBranch(cwd, deps) {
302
306
  function resolveProtectedBranchStatus(cwd, currentBranch, deps) {
303
307
  const effectiveConfig = readEffectiveGitConfig(cwd, deps);
304
308
  const { branch: baseBranch, verified } = resolveBaseBranchDiagnosticsWithConfig(cwd, effectiveConfig.baseBranch, deps);
305
- const protectedBranches = [...new Set([baseBranch, ...effectiveConfig.protectedBranches])];
309
+ const protectedBranches = [...new Set([
310
+ ...(effectiveConfig.allowDefaultBranchCommits ? [] : [baseBranch]),
311
+ ...effectiveConfig.protectedBranches,
312
+ ])];
306
313
  return {
307
314
  baseBranch,
308
315
  protectedBranches,
309
316
  rejectedProtectedBranches: effectiveConfig.rejectedProtectedBranches,
310
317
  isProtected: protectedBranches.includes(currentBranch),
311
318
  verified,
319
+ allowDefaultBranchCommits: effectiveConfig.allowDefaultBranchCommits,
312
320
  };
313
321
  }
314
322
  /**
@@ -56,7 +56,13 @@ const healthDiagnosticMod = require("../health-diagnostic-types.cjs");
56
56
  const { SEVERITY, adviseRemedy } = healthDiagnosticMod;
57
57
  // eslint-disable-next-line @typescript-eslint/no-require-imports
58
58
  const validateMod = require("../validate.cjs");
59
- const { phaseDirNameRe } = validateMod;
59
+ // #612: `isPhaseDirName` is the convention-SELECTED shape test wrapping
60
+ // `phaseDirNameRe`. Handed no convention it delegates to that very regex, so a
61
+ // legacy repo's W005 reading is byte-identical; handed 'bracket' it also
62
+ // recognizes `{CODE}.{MM}-{PP}-slug`, which `phaseDirNameRe` rejects outright —
63
+ // left un-threaded, W005 fires on EVERY phase directory of a repo that opted
64
+ // into the convention, i.e. the check inverts on exactly the repos PR-2 widens.
65
+ const { isPhaseDirName } = validateMod;
60
66
  // eslint-disable-next-line @typescript-eslint/no-require-imports
61
67
  const phaseIdMod = require("../phase-id.cjs");
62
68
  const { extractPhaseToken, normalizePhaseName, comparePhaseNum } = phaseIdMod;
@@ -64,7 +70,7 @@ const { extractPhaseToken, normalizePhaseName, comparePhaseNum } = phaseIdMod;
64
70
  function checkW005(snapshot) {
65
71
  const diagnostics = [];
66
72
  for (const name of snapshot.phaseDirs.value) {
67
- if (!name.match(phaseDirNameRe)) {
73
+ if (!isPhaseDirName(name, snapshot.phaseIdConvention)) {
68
74
  diagnostics.push({
69
75
  code: 'W005',
70
76
  severity: SEVERITY.WARNING,
@@ -116,8 +116,16 @@ const { phaseVariants } = validateMod;
116
116
  * does the roadmap claim, for W007) call this — one matcher, reused, per the
117
117
  * file-level comment.
118
118
  */
119
- function dirsForPhase(dirs, phaseId) {
120
- const canonical = matchPhaseDirs(dirs, normalizePhaseName(phaseId)).matches;
119
+ function dirsForPhase(dirs, phaseId, convention) {
120
+ // #612: `convention` reaches BOTH operands of this pairing or it pairs
121
+ // nothing. The phases come from a bracket-aware heading scan and the dirs
122
+ // from a bracket-aware disk scan, so a convention-less selector here leaves
123
+ // W006's existence check and W007's `claimedDirs` empty on exactly the repos
124
+ // both scans just widened — every bracket phase reads as "no directory on
125
+ // disk" AND every bracket directory reads as unclaimed. `matchPhaseDirs`
126
+ // forwards it to its primary `phaseTokenMatches`; handed nothing it is
127
+ // byte-identical, so no legacy repo's pairing changes.
128
+ const canonical = matchPhaseDirs(dirs, normalizePhaseName(phaseId), convention).matches;
121
129
  if (canonical.length > 0)
122
130
  return canonical;
123
131
  // Bug 2 (#3309 W006/W007 migration cluster, found while fixing the
@@ -139,7 +147,12 @@ function dirsForPhase(dirs, phaseId) {
139
147
  // alone misses without re-deriving a second matcher.
140
148
  const variants = phaseVariants(phaseId);
141
149
  return dirs.filter((d) => {
142
- const token = extractPhaseToken(d).toUpperCase();
150
+ // #612: the same convention the primary matcher above got. This fallback
151
+ // compares a DIRECTORY-derived token against phase-id variants, so left
152
+ // convention-less it reads `GSD.02-05-slug` with the legacy grammar and can
153
+ // never agree with a bracket phase id — the fallback would then be dead on
154
+ // precisely the convention it is reached for.
155
+ const token = extractPhaseToken(d, convention).toUpperCase();
143
156
  for (const variant of variants) {
144
157
  if (token === variant.toUpperCase())
145
158
  return true;
@@ -173,15 +186,40 @@ function checkW006(snapshot) {
173
186
  if (snapshot.roadmapDeclaredPhases.scope !== SCOPE.COMPLETE)
174
187
  return [];
175
188
  const dirs = snapshot.allPhaseDirNames.value;
189
+ const convention = snapshot.phaseIdConvention;
176
190
  const archivedTokens = new Set(snapshot.archivedPhaseTokens.value);
191
+ // No separate scope guard: `roadmapSentinelPhaseTokens` and
192
+ // `roadmapDeclaredPhases` are produced by ONE builder call
193
+ // (`buildRoadmapDeclaredPhasesField`) from ONE `buildRoadmapPhaseVariants`
194
+ // result, so their scopes are yoked by construction — the COMPLETE check
195
+ // above covers both. Stated rather than left implicit, because reading the
196
+ // sentinel set unconditionally would be a latent bug the day the two fields
197
+ // acquire independent builders: an UNREADABLE sentinel set silently degrades
198
+ // to "nothing is a sentinel," which ADDS W006 warnings rather than dropping
199
+ // them.
200
+ const sentinelTokens = new Set(snapshot.roadmapSentinelPhaseTokens.value);
177
201
  const checkboxes = snapshot.roadmapPhaseCheckboxes.value;
178
202
  const diagnostics = [];
179
203
  for (const { phaseId } of snapshot.roadmapDeclaredPhases.value) {
180
- // #3225: sentinel phase ids (999.x/0.x) are never-on-roadmap by
181
- // convention; a sentinel heading shouldn't demand a directory.
182
- if (isSentinelPhaseId(phaseId))
204
+ // A backlog/icebox phase legitimately has no directory. The two guards are
205
+ // DISJOINT, which is why both are needed and neither subsumes the other:
206
+ //
207
+ // #612 `roadmapSentinelPhaseTokens` — bracket sentinels. Under the
208
+ // bracket convention the sentinel-ness lives in the MILESTONE, not
209
+ // the token: the heading `### [GSD.999] 07:` yields phase id `07`,
210
+ // so a bare-token test cannot see it. The snapshot's
211
+ // `buildRoadmapPhaseVariants` call carries the bracket id alongside
212
+ // and reports the token here. Empty unless the bracket convention
213
+ // is active, so no legacy repo's reading changes.
214
+ // #3225 `isSentinelPhaseId` — the legacy/bare leading-int rule (999.x/0.x),
215
+ // which sees `### Phase 999:` and is blind to `[GSD.999] 07`.
216
+ //
217
+ // Upstream's call is kept one-arg verbatim: every `phaseId` here is a bare
218
+ // token, never a `{CODE}.{MM}`-qualified id, so a convention argument could
219
+ // not change its result.
220
+ if (sentinelTokens.has(phaseId) || isSentinelPhaseId(phaseId))
183
221
  continue;
184
- if (dirsForPhase(dirs, phaseId).length > 0)
222
+ if (dirsForPhase(dirs, phaseId, convention).length > 0)
185
223
  continue;
186
224
  // Bug 1 (#3309 W006/W007 migration cluster): a phase whose ONLY
187
225
  // directory lives under a milestone archive
@@ -209,10 +247,10 @@ function checkW006(snapshot) {
209
247
  // ─── W007 — an on-disk phase directory has no matching ROADMAP entry ──────
210
248
  // (verify.cts:2086-2101)
211
249
  /** Every directory in `dirs` that ANY declared roadmap phase resolves to. */
212
- function computeClaimedDirs(dirs, declaredPhases) {
250
+ function computeClaimedDirs(dirs, declaredPhases, convention) {
213
251
  const claimed = new Set();
214
252
  for (const { phaseId } of declaredPhases) {
215
- for (const dir of dirsForPhase(dirs, phaseId))
253
+ for (const dir of dirsForPhase(dirs, phaseId, convention))
216
254
  claimed.add(dir);
217
255
  }
218
256
  return claimed;
@@ -222,14 +260,19 @@ function checkW007(snapshot) {
222
260
  if (snapshot.roadmapDeclaredPhases.scope !== SCOPE.COMPLETE)
223
261
  return [];
224
262
  const dirs = snapshot.allPhaseDirNames.value;
225
- const claimedDirs = computeClaimedDirs(dirs, snapshot.roadmapDeclaredPhases.value);
263
+ const convention = snapshot.phaseIdConvention;
264
+ const claimedDirs = computeClaimedDirs(dirs, snapshot.roadmapDeclaredPhases.value, convention);
226
265
  const diagnostics = [];
227
266
  for (const dirName of dirs) {
228
267
  // `extractPhaseToken` is the phase-id.cts owner `PHASE_TOKEN_FROM_DIR_RE`
229
268
  // (`src/validate.cts:73-76`) is documented to match exactly
230
269
  // (verify.cts's original `p` key from `collectDiskPhaseEntries`,
231
270
  // `verify.cts:1373-1397`) — same token, relocated read, not reinvented.
232
- const token = extractPhaseToken(dirName);
271
+ // #612: `collectDiskPhaseEntries` keyed that map through the
272
+ // convention-aware extractor, so the token this message names must be
273
+ // derived the same way — otherwise a bracket repo's unclaimed
274
+ // `GSD.02-05-slug` is reported as phase `GSD.02` rather than `05`.
275
+ const token = extractPhaseToken(dirName, convention);
233
276
  // #3225: a sentinel dir on disk (999-interim, 0-drafts) is defined as
234
277
  // never-on-roadmap; it must not trigger W007. #3639: judged on the DIR
235
278
  // NAME via the dir-aware recognizer — the extracted token is
@@ -111,7 +111,13 @@ const RULE_W024 = {
111
111
  function buildValidPhaseSet(snapshot) {
112
112
  const valid = new Set();
113
113
  for (const dir of snapshot.phaseDirs.value) {
114
- const token = extractPhaseToken(dir);
114
+ // #612: the pre-migration source of this third was
115
+ // `collectDiskPhases(planBase, convention)`, whose keys came from the
116
+ // convention-aware extractor. Left convention-less here, a bracket repo's
117
+ // `GSD.02-05-slug` contributes `GSD.02` instead of `05`, so every STATE.md
118
+ // reference to a real phase falls outside the valid set and W002 fires on
119
+ // all of them.
120
+ const token = extractPhaseToken(dir, snapshot.phaseIdConvention);
115
121
  if (token)
116
122
  valid.add(token);
117
123
  }
@@ -233,31 +239,71 @@ const RULE_W021 = {
233
239
  description: "Phase's integer prefix implies a different milestone than its ROADMAP section (phase_id_convention: milestone-prefixed)",
234
240
  repairable: false,
235
241
  check: (snapshot) => {
236
- const convention = snapshot.config.value?.['phase_id_convention'];
237
- if (convention !== 'milestone-prefixed')
238
- return [];
239
242
  const diagnostics = [];
240
- for (const entry of snapshot.roadmapDeclaredPhases.value) {
241
- // `entry.milestone === null` means the builder never found this phase
242
- // heading inside any versioned (`v\d+\.\d+`) section — the original
243
- // `checkMilestonePrefixMismatches` only ever iterates phases found
244
- // WITHIN a section, so a phase outside any section is equivalently
245
- // never checked here.
246
- if (entry.milestone === null)
247
- continue;
248
- const expectedMilestone = getMilestoneFromPhaseId(entry.phaseId);
249
- if (expectedMilestone === null || expectedMilestone === entry.milestone)
250
- continue;
251
- diagnostics.push({
252
- code: 'W021',
253
- severity: SEVERITY.WARNING,
254
- message: `Phase ${entry.phaseId}: integer prefix implies ${expectedMilestone} but listed under ${entry.milestone}`,
255
- remedy: adviseRemedy('gsd-tools roadmap upgrade --convention milestone-prefixed'),
256
- });
243
+ // Two INDEPENDENT gates, evaluated in the order the pre-migration
244
+ // `cmdValidateHealth` block emitted them (milestone-prefixed first, then
245
+ // bracket). They are deliberately NOT written as if/else: the two read
246
+ // DIFFERENT config bases and can both be true at once.
247
+ //
248
+ // milestone-prefixed — `snapshot.config` (ROOT config only), exactly as
249
+ // this gate has always read it. Federating THIS one silently moved a
250
+ // legacy convention's answer in BOTH directions on workstream repos: a
251
+ // W021 that fires at base vanishing, and one silent at base firing.
252
+ // bracket — `snapshot.phaseIdConvention` (federated workstream -> root),
253
+ // because the bracket reads this half reports on are themselves
254
+ // federated; gating it on the root answer would split the check against
255
+ // the reads it describes.
256
+ //
257
+ // A workstream that overrides a `milestone-prefixed` ROOT to `bracket` is
258
+ // the case that makes the distinction observable, and it is the reason an
259
+ // early `return` here would be a bug: the root-configured M-NN gate must
260
+ // still fire, or the repo looks healthier than it is.
261
+ const rootConvention = snapshot.config.value?.['phase_id_convention'];
262
+ if (rootConvention === 'milestone-prefixed') {
263
+ diagnostics.push(...checkMilestonePrefixW021(snapshot));
264
+ }
265
+ // #612: an ADDITIVE sibling branch — the milestone-prefixed gate above is
266
+ // untouched. Gated strictly on the ACTIVE convention, never on
267
+ // project_code presence: reads are selected per convention, and a CHECK
268
+ // that can fail a repo runs only for the convention that repo opted into.
269
+ if (snapshot.phaseIdConvention === 'bracket') {
270
+ for (const mm of snapshot.roadmapBracketIncoherences.value) {
271
+ diagnostics.push({
272
+ code: 'W021',
273
+ severity: SEVERITY.WARNING,
274
+ message: mm.kind === 'missing-bracket'
275
+ ? `Phase ${mm.phaseId}: heading is not in bracket form under the 'bracket' convention (expected \`[CODE.${mm.sectionMilestone}] ${mm.phaseId}:\`)`
276
+ : `Phase ${mm.phaseId}: bracket milestone ${mm.phaseMilestone} does not match its section milestone ${mm.sectionMilestone}`,
277
+ remedy: adviseRemedy('Bracket migration lands with the #612 migrator (PR-3); until then, manually align the bracket milestone in this heading to match the enclosing section.'),
278
+ });
279
+ }
257
280
  }
258
281
  return diagnostics;
259
282
  },
260
283
  };
284
+ /** The milestone-prefixed half of W021, unchanged by #612. */
285
+ function checkMilestonePrefixW021(snapshot) {
286
+ const diagnostics = [];
287
+ for (const entry of snapshot.roadmapDeclaredPhases.value) {
288
+ // `entry.milestone === null` means the builder never found this phase
289
+ // heading inside any versioned (`v\d+\.\d+`) section — the original
290
+ // `checkMilestonePrefixMismatches` only ever iterates phases found
291
+ // WITHIN a section, so a phase outside any section is equivalently
292
+ // never checked here.
293
+ if (entry.milestone === null)
294
+ continue;
295
+ const expectedMilestone = getMilestoneFromPhaseId(entry.phaseId);
296
+ if (expectedMilestone === null || expectedMilestone === entry.milestone)
297
+ continue;
298
+ diagnostics.push({
299
+ code: 'W021',
300
+ severity: SEVERITY.WARNING,
301
+ message: `Phase ${entry.phaseId}: integer prefix implies ${expectedMilestone} but listed under ${entry.milestone}`,
302
+ remedy: adviseRemedy('gsd-tools roadmap upgrade --convention milestone-prefixed'),
303
+ });
304
+ }
305
+ return diagnostics;
306
+ }
261
307
  // ─── W026 — STATE says milestone complete but ROADMAP lists unstarted phase ─
262
308
  const RULE_W026 = {
263
309
  code: 'W026',
@@ -282,7 +328,14 @@ const RULE_W026 = {
282
328
  // unwindowed `phaseDirNames2` (`verify.cts:2372-2382`, a direct
283
329
  // `readdirSync` of the phases dir), not the current-milestone-windowed
284
330
  // `phaseDirs`.
285
- const hasDirectory = matchPhaseDirs(snapshot.allPhaseDirNames.value, normalized).matches.length > 0;
331
+ // #612: the directory read widens with the heading read that produced
332
+ // `currentMilestoneRoadmapPhaseIds`, or every bracket phase resolves to
333
+ // nothing, reads as unstarted, and this UNGATED warning false-fires on
334
+ // any bracket repo whose STATE says the milestone is complete. #2528
335
+ // moved the read onto the shared selector; the convention rides along,
336
+ // since the selector's primary match IS the `phaseTokenMatches` call this
337
+ // line used to make directly.
338
+ const hasDirectory = matchPhaseDirs(snapshot.allPhaseDirNames.value, normalized, snapshot.phaseIdConvention).matches.length > 0;
286
339
  if (!hasDirectory)
287
340
  unstarted.push(phaseId);
288
341
  }
@@ -72,7 +72,7 @@ const INTERFACE_POINTS = Object.freeze(['command', 'dispatch', 'model', 'hooks',
72
72
  const SAFE_DEFAULTS = {
73
73
  embeddingMode: 'declarative',
74
74
  commandSurface: 'prose-only',
75
- dispatch: { namedDispatch: false, nested: false, maxDepth: 0, background: false, subagentToolkit: 'read-only', backgroundDispatch: false, isolation: 'none' },
75
+ dispatch: { namedDispatch: false, nested: false, maxDepth: 0, background: false, subagentToolkit: 'read-only', backgroundDispatch: false, isolation: 'none', maxConcurrency: 1 },
76
76
  modelMode: 'passive',
77
77
  hookBus: 'none',
78
78
  stateIO: 'session-log-append',
@@ -84,7 +84,7 @@ const PROFILE_BASELINES = Object.freeze({
84
84
  'programmatic-cli': Object.freeze({
85
85
  embeddingMode: 'imperative',
86
86
  commandSurface: 'slash-file',
87
- dispatch: Object.freeze({ namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: true, isolation: 'none' }),
87
+ dispatch: Object.freeze({ namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: true, isolation: 'none', maxConcurrency: 1 }),
88
88
  modelMode: 'passive',
89
89
  hookBus: 'host',
90
90
  stateIO: 'filesystem',
@@ -95,7 +95,7 @@ const PROFILE_BASELINES = Object.freeze({
95
95
  'declarative-cli': Object.freeze({
96
96
  embeddingMode: 'declarative',
97
97
  commandSurface: 'slash-file',
98
- dispatch: Object.freeze({ namedDispatch: true, nested: false, maxDepth: 1, background: false, subagentToolkit: 'full', backgroundDispatch: false, isolation: 'none' }),
98
+ dispatch: Object.freeze({ namedDispatch: true, nested: false, maxDepth: 1, background: false, subagentToolkit: 'full', backgroundDispatch: false, isolation: 'none', maxConcurrency: 1 }),
99
99
  modelMode: 'passive',
100
100
  hookBus: 'host',
101
101
  stateIO: 'filesystem',
@@ -106,7 +106,7 @@ const PROFILE_BASELINES = Object.freeze({
106
106
  'ide': Object.freeze({
107
107
  embeddingMode: 'imperative',
108
108
  commandSurface: 'palette',
109
- dispatch: Object.freeze({ namedDispatch: true, nested: true, maxDepth: 5, background: true, subagentToolkit: 'full', backgroundDispatch: true, isolation: 'none' }),
109
+ dispatch: Object.freeze({ namedDispatch: true, nested: true, maxDepth: 5, background: true, subagentToolkit: 'full', backgroundDispatch: true, isolation: 'none', maxConcurrency: 1 }),
110
110
  modelMode: 'active',
111
111
  hookBus: 'engine',
112
112
  stateIO: 'sandboxed-storage',
@@ -235,7 +235,7 @@ const DEFAULT_ENGINE = {
235
235
  axes: {
236
236
  embeddingMode: 'imperative',
237
237
  commandSurface: 'slash-file',
238
- dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: true, isolation: 'none' },
238
+ dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: true, isolation: 'none', maxConcurrency: 1 },
239
239
  modelMode: 'active',
240
240
  hookBus: 'host',
241
241
  stateIO: 'filesystem',
@@ -246,6 +246,19 @@ const DEFAULT_ENGINE = {
246
246
  known: HOST_INTEGRATION_AXES,
247
247
  };
248
248
  // ---------------------------------------------------------------------------
249
+ // isPositiveSafeInteger — shared predicate (#3673)
250
+ // ---------------------------------------------------------------------------
251
+ /**
252
+ * True iff `v` is a positive (>0) JS safe integer. Single source of truth for
253
+ * the dispatch.maxConcurrency contract: used by negotiateHostCapabilities
254
+ * below, and by gsd-core/bin/gsd-tools.cjs's routeDispatchCapacity (which
255
+ * requires this module's compiled output rather than reimplementing the
256
+ * predicate), so the two consumers cannot silently diverge.
257
+ */
258
+ function isPositiveSafeInteger(v) {
259
+ return typeof v === 'number' && Number.isSafeInteger(v) && v > 0;
260
+ }
261
+ // ---------------------------------------------------------------------------
249
262
  // negotiateHostCapabilities
250
263
  // ---------------------------------------------------------------------------
251
264
  /**
@@ -335,6 +348,7 @@ function negotiateHostCapabilities(host, engine = DEFAULT_ENGINE) {
335
348
  let effectiveSubagentToolkit;
336
349
  let effectiveMaxDepth;
337
350
  let effectiveIsolation;
351
+ let effectiveMaxConcurrency;
338
352
  if (hostDispatch === null) {
339
353
  // Host didn't declare dispatch at all — fail-closed to most-restrictive values
340
354
  warnings.push(`host did not declare 'dispatch'`);
@@ -345,6 +359,7 @@ function negotiateHostCapabilities(host, engine = DEFAULT_ENGINE) {
345
359
  effectiveSubagentToolkit = 'read-only';
346
360
  effectiveMaxDepth = 0;
347
361
  effectiveIsolation = 'none';
362
+ effectiveMaxConcurrency = 1;
348
363
  }
349
364
  else {
350
365
  // N1: observability warnings for 'undocumented' sentinel on dispatch fields
@@ -408,6 +423,41 @@ function negotiateHostCapabilities(host, engine = DEFAULT_ENGINE) {
408
423
  const eDepthNum = engineDispatch.maxDepth < 0 ? Infinity : engineDispatch.maxDepth;
409
424
  const minDepth = Math.min(hDepthNum, eDepthNum);
410
425
  effectiveMaxDepth = minDepth === Infinity ? -1 : minDepth;
426
+ // maxConcurrency (#3673, ADR-1239 Phase 1): a positive-safe-integer
427
+ // passthrough with a fail-closed floor of 1. UNLIKE maxDepth, there is no
428
+ // min(host, engine) reduction here — the design doc explicitly rejects an
429
+ // engine-side ceiling for this field (no competing intrinsic worker
430
+ // ceiling to cap against at the negotiation layer); a `--jobs N` cap is
431
+ // applied later, at the Phase 4 scheduler.
432
+ const hostMaxConcurrency = hostDispatch.maxConcurrency;
433
+ if (hostMaxConcurrency === UNDOCUMENTED) {
434
+ warnings.push(`dispatch.maxConcurrency is undocumented — degraded closed (1)`);
435
+ effectiveMaxConcurrency = 1;
436
+ }
437
+ else if (isPositiveSafeInteger(hostMaxConcurrency)) {
438
+ effectiveMaxConcurrency = hostMaxConcurrency;
439
+ }
440
+ else if (hostMaxConcurrency === undefined) {
441
+ warnings.push(`host did not declare 'dispatch.maxConcurrency' — treating as 1`);
442
+ effectiveMaxConcurrency = 1;
443
+ }
444
+ else if (typeof hostMaxConcurrency !== 'number') {
445
+ warnings.push(`host dispatch.maxConcurrency is not a number — treating as 1`);
446
+ effectiveMaxConcurrency = 1;
447
+ }
448
+ else if (!Number.isSafeInteger(hostMaxConcurrency)) {
449
+ if (Number.isInteger(hostMaxConcurrency)) {
450
+ warnings.push(`host dispatch.maxConcurrency is an unsafe integer — treating as 1`);
451
+ }
452
+ else {
453
+ warnings.push(`host dispatch.maxConcurrency is not an integer — treating as 1`);
454
+ }
455
+ effectiveMaxConcurrency = 1;
456
+ }
457
+ else {
458
+ warnings.push(`host dispatch.maxConcurrency is non-positive — treating as 1`);
459
+ effectiveMaxConcurrency = 1;
460
+ }
411
461
  // If namedDispatch is false, cap maxDepth/nested/background/backgroundDispatch to 0/false/false/false (struct consistency)
412
462
  if (!effectiveNamedDispatch) {
413
463
  effectiveMaxDepth = 0;
@@ -424,6 +474,7 @@ function negotiateHostCapabilities(host, engine = DEFAULT_ENGINE) {
424
474
  subagentToolkit: effectiveSubagentToolkit,
425
475
  backgroundDispatch: effectiveBackgroundDispatch,
426
476
  isolation: effectiveIsolation,
477
+ maxConcurrency: effectiveMaxConcurrency,
427
478
  };
428
479
  // ---------------------------------------------------------------------------
429
480
  // Assemble effective axes
@@ -753,6 +804,7 @@ module.exports = {
753
804
  EXTENSION_EVENT_SURFACES,
754
805
  degradationFor,
755
806
  profileOf,
807
+ isPositiveSafeInteger,
756
808
  negotiateHostCapabilities,
757
809
  shouldFlattenDispatch,
758
810
  resolveDispatchType,
@@ -158,6 +158,20 @@ function routeInitCommand({ init, args, cwd, raw, error }) {
158
158
  full: namedArgs['full'],
159
159
  });
160
160
  },
161
+ // #3676 (Phase 4, epic #3344): `init.quick-batch` supplies model
162
+ // profiles/commit_docs/roadmap-existence/section_manifest — batch
163
+ // creation itself is the `quick-batch create` verb's job (wraps
164
+ // `createBatch`, src/quick-batch.cts). No free-text description to
165
+ // strip: `--research`/`--validate` are the only recognized flags
166
+ // (`--discuss`/`--full` are rejected upstream by `parseQuickBatchArgs`
167
+ // before this init bundle is ever reached).
168
+ 'quick-batch': () => {
169
+ const namedArgs = (0, command_arg_projection_cjs_1.parseNamedArgsOrExit)(args, { booleanFlags: ['research', 'validate'], positionals: 2 }, error);
170
+ init.cmdInitQuickBatch(cwd, raw, {
171
+ research: namedArgs['research'],
172
+ validate: namedArgs['validate'],
173
+ });
174
+ },
161
175
  'ingest-docs': () => init.cmdInitIngestDocs(cwd, raw),
162
176
  resume: () => init.cmdInitResume(cwd, raw),
163
177
  // ADR-3473 §8.4 / #3358 gap: these handlers read args[2] positionally