@opengsd/gsd-core 1.11.0 → 1.12.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 (395) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-code-fixer.md +1 -1
  4. package/agents/gsd-debug-session-manager.md +1 -1
  5. package/agents/gsd-debugger.md +1 -1
  6. package/agents/gsd-dom-verifier.md +169 -0
  7. package/agents/gsd-eval-auditor.md +1 -1
  8. package/agents/gsd-executor.md +17 -9
  9. package/agents/gsd-framework-selector.md +1 -3
  10. package/agents/gsd-intel-updater.md +1 -1
  11. package/agents/gsd-mempalace-curator.md +0 -1
  12. package/agents/gsd-pattern-mapper.md +11 -0
  13. package/agents/gsd-phase-researcher.md +3 -1
  14. package/agents/gsd-plan-checker.md +15 -55
  15. package/agents/gsd-planner.md +6 -4
  16. package/agents/gsd-project-researcher.md +1 -1
  17. package/agents/gsd-research-synthesizer.md +2 -2
  18. package/agents/gsd-roadmapper.md +15 -11
  19. package/agents/gsd-ui-checker.md +63 -4
  20. package/agents/gsd-ui-researcher.md +41 -3
  21. package/agents/gsd-verifier.md +1 -1
  22. package/bin/install.js +609 -134
  23. package/commands/gsd/discuss-phase.md +1 -1
  24. package/commands/gsd/import.md +1 -1
  25. package/commands/gsd/quick.md +8 -4
  26. package/gsd-core/bin/gsd-tools.cjs +567 -51
  27. package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
  28. package/gsd-core/bin/lib/adr-parser.cjs +13 -7
  29. package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
  30. package/gsd-core/bin/lib/api-coverage.cjs +30 -9
  31. package/gsd-core/bin/lib/artifacts.cjs +2 -0
  32. package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
  33. package/gsd-core/bin/lib/audit.cjs +163 -41
  34. package/gsd-core/bin/lib/broken-windows.cjs +306 -28
  35. package/gsd-core/bin/lib/capability-lock.cjs +10 -4
  36. package/gsd-core/bin/lib/capability-registry.cjs +336 -95
  37. package/gsd-core/bin/lib/capability-state.cjs +18 -3
  38. package/gsd-core/bin/lib/capability-validator.cjs +205 -18
  39. package/gsd-core/bin/lib/check-command-router.cjs +145 -5
  40. package/gsd-core/bin/lib/cli-exit.cjs +496 -10
  41. package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
  42. package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
  43. package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
  44. package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
  45. package/gsd-core/bin/lib/commands.cjs +543 -44
  46. package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
  47. package/gsd-core/bin/lib/config-loader.cjs +118 -29
  48. package/gsd-core/bin/lib/config.cjs +92 -2
  49. package/gsd-core/bin/lib/configuration.cjs +129 -37
  50. package/gsd-core/bin/lib/core-utils.cjs +84 -7
  51. package/gsd-core/bin/lib/edge-probe.cjs +9 -1
  52. package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
  53. package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
  54. package/gsd-core/bin/lib/frontmatter.cjs +840 -305
  55. package/gsd-core/bin/lib/gap-checker.cjs +27 -3
  56. package/gsd-core/bin/lib/git-base-branch.cjs +174 -39
  57. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
  58. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +6 -3
  59. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
  60. package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
  61. package/gsd-core/bin/lib/host-integration.cjs +39 -6
  62. package/gsd-core/bin/lib/init-command-router.cjs +118 -21
  63. package/gsd-core/bin/lib/init.cjs +120 -41
  64. package/gsd-core/bin/lib/install-engine.cjs +68 -3
  65. package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -1
  66. package/gsd-core/bin/lib/install-profiles.cjs +78 -4
  67. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  68. package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
  69. package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
  70. package/gsd-core/bin/lib/intel.cjs +101 -26
  71. package/gsd-core/bin/lib/io.cjs +160 -15
  72. package/gsd-core/bin/lib/learnings.cjs +85 -14
  73. package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
  74. package/gsd-core/bin/lib/markdown-table.cjs +52 -4
  75. package/gsd-core/bin/lib/milestone.cjs +90 -5
  76. package/gsd-core/bin/lib/model-catalog.cjs +177 -19
  77. package/gsd-core/bin/lib/model-resolver.cjs +10 -28
  78. package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
  79. package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
  80. package/gsd-core/bin/lib/phase-id.cjs +70 -4
  81. package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
  82. package/gsd-core/bin/lib/phase-locator.cjs +138 -17
  83. package/gsd-core/bin/lib/phase.cjs +405 -84
  84. package/gsd-core/bin/lib/plan-document.cjs +263 -0
  85. package/gsd-core/bin/lib/plan-scan.cjs +13 -2
  86. package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
  87. package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
  88. package/gsd-core/bin/lib/planning-snapshot.cjs +18 -14
  89. package/gsd-core/bin/lib/planning-workspace.cjs +56 -0
  90. package/gsd-core/bin/lib/probe-core.cjs +4 -1
  91. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
  92. package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
  93. package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
  94. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
  95. package/gsd-core/bin/lib/review-lane-descriptor.cjs +9 -9
  96. package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
  97. package/gsd-core/bin/lib/roadmap-parser.cjs +79 -16
  98. package/gsd-core/bin/lib/roadmap.cjs +74 -19
  99. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +96 -8
  100. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +34 -1
  101. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +287 -55
  102. package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
  103. package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
  104. package/gsd-core/bin/lib/shell-command-projection.cjs +71 -8
  105. package/gsd-core/bin/lib/smart-entry.cjs +12 -22
  106. package/gsd-core/bin/lib/spec-section.cjs +12 -7
  107. package/gsd-core/bin/lib/state-command-router.cjs +47 -18
  108. package/gsd-core/bin/lib/state-contract.cjs +359 -0
  109. package/gsd-core/bin/lib/state-document.cjs +186 -0
  110. package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
  111. package/gsd-core/bin/lib/state-transition.cjs +517 -101
  112. package/gsd-core/bin/lib/state.cjs +946 -163
  113. package/gsd-core/bin/lib/surface.cjs +10 -2
  114. package/gsd-core/bin/lib/task-command-router.cjs +111 -1
  115. package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
  116. package/gsd-core/bin/lib/teams-status.cjs +4 -1
  117. package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
  118. package/gsd-core/bin/lib/uat.cjs +1376 -125
  119. package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
  120. package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
  121. package/gsd-core/bin/lib/unusable-input.cjs +13 -0
  122. package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
  123. package/gsd-core/bin/lib/vendor/README.md +43 -5
  124. package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
  125. package/gsd-core/bin/lib/verification.cjs +14 -1
  126. package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
  127. package/gsd-core/bin/lib/verify.cjs +95 -40
  128. package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
  129. package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
  130. package/gsd-core/bin/lib/worktree-safety.cjs +177 -21
  131. package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
  132. package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
  133. package/gsd-core/bin/shared/exit-codes.json +8 -0
  134. package/gsd-core/bin/shared/exit-codes.sh +20 -0
  135. package/gsd-core/bin/shared/model-catalog.json +8 -1
  136. package/gsd-core/references/agent-contracts.md +3 -2
  137. package/gsd-core/references/api-coverage.md +24 -2
  138. package/gsd-core/references/autonomous-smart-discuss.md +3 -3
  139. package/gsd-core/references/checkpoints.md +37 -19
  140. package/gsd-core/references/decimal-phase-calculation.md +5 -5
  141. package/gsd-core/references/edge-probe.md +8 -0
  142. package/gsd-core/references/execute-mvp-tdd.md +1 -3
  143. package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
  144. package/gsd-core/references/execute-phase-wave-guard.md +11 -9
  145. package/gsd-core/references/failing-direction.md +78 -0
  146. package/gsd-core/references/gate-prompts.md +1 -1
  147. package/gsd-core/references/git-integration.md +5 -5
  148. package/gsd-core/references/git-planning-commit.md +3 -3
  149. package/gsd-core/references/gsd-run-resolver.md +1 -1
  150. package/gsd-core/references/loop-hook-dispatch.md +22 -0
  151. package/gsd-core/references/model-profiles.md +1 -1
  152. package/gsd-core/references/nyquist-compliance.md +74 -0
  153. package/gsd-core/references/offer-next.md +3 -5
  154. package/gsd-core/references/phase-argument-parsing.md +3 -3
  155. package/gsd-core/references/planner-failing-direction.md +53 -0
  156. package/gsd-core/references/planner-human-verify-mode.md +15 -1
  157. package/gsd-core/references/planner-revision.md +1 -1
  158. package/gsd-core/references/planner-verify-command-grounding.md +17 -0
  159. package/gsd-core/references/planning-config.md +37 -8
  160. package/gsd-core/references/reviewer-instances.md +31 -0
  161. package/gsd-core/references/runtime-aware-dispatch.md +1 -1
  162. package/gsd-core/references/tdd.md +1 -3
  163. package/gsd-core/references/ui-brand.md +65 -21
  164. package/gsd-core/references/ui-consideration-probe.md +1 -1
  165. package/gsd-core/references/universal-anti-patterns.md +2 -2
  166. package/gsd-core/references/verify-command-path-resolvability.md +42 -0
  167. package/gsd-core/references/verify-mvp-mode.md +1 -1
  168. package/gsd-core/references/workstream-flag.md +11 -11
  169. package/gsd-core/templates/README.md +1 -1
  170. package/gsd-core/templates/SECURITY.md +3 -3
  171. package/gsd-core/templates/UI-SPEC.md +25 -3
  172. package/gsd-core/templates/VALIDATION.md +3 -3
  173. package/gsd-core/templates/phase-prompt.md +3 -0
  174. package/gsd-core/templates/state.md +7 -0
  175. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  176. package/gsd-core/workflows/add-backlog.md +1 -1
  177. package/gsd-core/workflows/add-phase.md +3 -3
  178. package/gsd-core/workflows/add-tests.md +3 -8
  179. package/gsd-core/workflows/add-todo.md +1 -1
  180. package/gsd-core/workflows/ai-integration-phase.md +4 -9
  181. package/gsd-core/workflows/audit-fix.md +12 -3
  182. package/gsd-core/workflows/audit-milestone.md +9 -9
  183. package/gsd-core/workflows/audit-uat.md +17 -2
  184. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
  185. package/gsd-core/workflows/autonomous.md +10 -26
  186. package/gsd-core/workflows/check-todos.md +1 -1
  187. package/gsd-core/workflows/cleanup.md +2 -2
  188. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
  189. package/gsd-core/workflows/code-review-fix.md +1 -1
  190. package/gsd-core/workflows/code-review.md +121 -40
  191. package/gsd-core/workflows/complete-milestone.md +15 -10
  192. package/gsd-core/workflows/debug.md +5 -3
  193. package/gsd-core/workflows/diagnose-issues.md +12 -6
  194. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  195. package/gsd-core/workflows/discuss-phase/modes/chain.md +3 -7
  196. package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
  197. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
  198. package/gsd-core/workflows/discuss-phase-assumptions.md +2 -2
  199. package/gsd-core/workflows/discuss-phase.md +1 -1
  200. package/gsd-core/workflows/do.md +3 -6
  201. package/gsd-core/workflows/docs-update.md +5 -4
  202. package/gsd-core/workflows/edit-phase.md +1 -1
  203. package/gsd-core/workflows/eval-review.md +4 -9
  204. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  205. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +113 -11
  206. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  207. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  208. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  209. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +22 -4
  210. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
  211. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
  212. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -2
  213. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
  214. package/gsd-core/workflows/execute-phase.md +38 -54
  215. package/gsd-core/workflows/execute-plan.md +17 -12
  216. package/gsd-core/workflows/explore.md +1 -1
  217. package/gsd-core/workflows/extract-learnings.md +1 -1
  218. package/gsd-core/workflows/fast.md +2 -2
  219. package/gsd-core/workflows/forensics.md +1 -1
  220. package/gsd-core/workflows/graduation.md +5 -5
  221. package/gsd-core/workflows/health.md +3 -6
  222. package/gsd-core/workflows/import.md +14 -11
  223. package/gsd-core/workflows/inbox.md +4 -5
  224. package/gsd-core/workflows/ingest-docs.md +44 -11
  225. package/gsd-core/workflows/insert-phase.md +5 -5
  226. package/gsd-core/workflows/list-seeds.md +5 -3
  227. package/gsd-core/workflows/list-workspaces.md +1 -1
  228. package/gsd-core/workflows/manager.md +12 -23
  229. package/gsd-core/workflows/map-codebase.md +1 -1
  230. package/gsd-core/workflows/milestone-summary.md +1 -1
  231. package/gsd-core/workflows/mvp-phase.md +2 -2
  232. package/gsd-core/workflows/new-milestone.md +9 -21
  233. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  234. package/gsd-core/workflows/new-project.md +12 -26
  235. package/gsd-core/workflows/new-workspace.md +1 -1
  236. package/gsd-core/workflows/next.md +2 -2
  237. package/gsd-core/workflows/pause-work.md +1 -1
  238. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
  239. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
  240. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
  241. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
  242. package/gsd-core/workflows/plan-phase.md +121 -42
  243. package/gsd-core/workflows/plan-review-convergence.md +46 -9
  244. package/gsd-core/workflows/plant-seed.md +2 -2
  245. package/gsd-core/workflows/pr-branch.md +187 -51
  246. package/gsd-core/workflows/profile-user.md +16 -14
  247. package/gsd-core/workflows/progress.md +27 -12
  248. package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
  249. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -3
  250. package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
  251. package/gsd-core/workflows/quick/steps/research-phase.md +2 -4
  252. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
  253. package/gsd-core/workflows/quick.md +20 -29
  254. package/gsd-core/workflows/remove-phase.md +4 -4
  255. package/gsd-core/workflows/remove-workspace.md +2 -2
  256. package/gsd-core/workflows/resume-project.md +8 -12
  257. package/gsd-core/workflows/review.md +193 -15
  258. package/gsd-core/workflows/scan.md +1 -1
  259. package/gsd-core/workflows/secure-phase.md +2 -2
  260. package/gsd-core/workflows/settings-advanced.md +7 -9
  261. package/gsd-core/workflows/settings-integrations.md +64 -31
  262. package/gsd-core/workflows/settings.md +3 -5
  263. package/gsd-core/workflows/ship.md +12 -6
  264. package/gsd-core/workflows/sketch-wrap-up.md +11 -17
  265. package/gsd-core/workflows/sketch.md +12 -18
  266. package/gsd-core/workflows/smart-entry.md +3 -5
  267. package/gsd-core/workflows/spec-phase.md +23 -1
  268. package/gsd-core/workflows/spike-wrap-up.md +7 -11
  269. package/gsd-core/workflows/spike.md +20 -31
  270. package/gsd-core/workflows/stats.md +2 -2
  271. package/gsd-core/workflows/sync-skills.md +1 -1
  272. package/gsd-core/workflows/thread.md +11 -7
  273. package/gsd-core/workflows/transition.md +5 -5
  274. package/gsd-core/workflows/ui-phase.md +10 -16
  275. package/gsd-core/workflows/ui-review.md +6 -10
  276. package/gsd-core/workflows/ultraplan-phase.md +5 -13
  277. package/gsd-core/workflows/undo.md +8 -16
  278. package/gsd-core/workflows/update.md +6 -10
  279. package/gsd-core/workflows/validate-phase.md +2 -2
  280. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
  281. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  282. package/gsd-core/workflows/verify-work.md +57 -18
  283. package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
  284. package/hooks/dist/gsd-config-reload.js +18 -12
  285. package/hooks/dist/gsd-context-monitor.js +19 -10
  286. package/hooks/dist/gsd-cursor-post-tool.js +3 -1
  287. package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
  288. package/hooks/dist/gsd-cursor-session-start.js +2 -1
  289. package/hooks/dist/gsd-cursor-stop.js +2 -1
  290. package/hooks/dist/gsd-cursor-subagent-start.js +28 -23
  291. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
  292. package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
  293. package/hooks/dist/gsd-graphify-update.sh +22 -18
  294. package/hooks/dist/gsd-node-runner.sh +76 -0
  295. package/hooks/dist/gsd-phase-boundary.sh +1 -0
  296. package/hooks/dist/gsd-prompt-guard.js +16 -7
  297. package/hooks/dist/gsd-read-guard.js +16 -7
  298. package/hooks/dist/gsd-read-injection-scanner.js +17 -8
  299. package/hooks/dist/gsd-session-state.sh +1 -0
  300. package/hooks/dist/gsd-statusline.js +215 -26
  301. package/hooks/dist/gsd-validate-commit.sh +80 -6
  302. package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
  303. package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
  304. package/hooks/dist/gsd-workflow-guard.js +34 -16
  305. package/hooks/dist/gsd-worktree-path-guard.js +36 -21
  306. package/hooks/dist/gsd-write-guard.js +35 -25
  307. package/hooks/dist/lib/cli-exit.js +560 -0
  308. package/hooks/dist/lib/exit-code-registry.js +98 -0
  309. package/hooks/dist/lib/git-probe.js +84 -0
  310. package/hooks/dist/lib/hook-exit.js +81 -0
  311. package/hooks/dist/managed-hooks-registry.cjs +3 -0
  312. package/hooks/gsd-agent-isolation-guard.js +77 -38
  313. package/hooks/gsd-config-reload.js +18 -12
  314. package/hooks/gsd-context-monitor.js +19 -10
  315. package/hooks/gsd-cursor-post-tool.js +3 -1
  316. package/hooks/gsd-cursor-pre-tool.js +3 -1
  317. package/hooks/gsd-cursor-session-start.js +2 -1
  318. package/hooks/gsd-cursor-stop.js +2 -1
  319. package/hooks/gsd-cursor-subagent-start.js +28 -23
  320. package/hooks/gsd-cursor-subagent-stop.js +3 -1
  321. package/hooks/gsd-ensure-canonical-path.js +2 -1
  322. package/hooks/gsd-graphify-update.sh +22 -18
  323. package/hooks/gsd-node-runner.sh +76 -0
  324. package/hooks/gsd-phase-boundary.sh +1 -0
  325. package/hooks/gsd-prompt-guard.js +16 -7
  326. package/hooks/gsd-read-guard.js +16 -7
  327. package/hooks/gsd-read-injection-scanner.js +17 -8
  328. package/hooks/gsd-session-state.sh +1 -0
  329. package/hooks/gsd-statusline.js +215 -26
  330. package/hooks/gsd-validate-commit.sh +80 -6
  331. package/hooks/gsd-windsurf-pre-command.js +16 -11
  332. package/hooks/gsd-windsurf-pre-write.js +22 -13
  333. package/hooks/gsd-workflow-guard.js +34 -16
  334. package/hooks/gsd-worktree-path-guard.js +36 -21
  335. package/hooks/gsd-write-guard.js +35 -25
  336. package/hooks/lib/cli-exit.js +560 -0
  337. package/hooks/lib/exit-code-registry.js +98 -0
  338. package/hooks/lib/git-probe.js +84 -0
  339. package/hooks/lib/hook-exit.js +81 -0
  340. package/hooks/managed-hooks-registry.cjs +3 -0
  341. package/package.json +12 -7
  342. package/scripts/base64-scan.sh +74 -12
  343. package/scripts/build-hooks.js +5 -0
  344. package/scripts/check-glossary-refs.cjs +77 -15
  345. package/scripts/check-mutation-score-ratchet.cjs +156 -0
  346. package/scripts/ci-check-job-near-cap.cjs +49 -0
  347. package/scripts/ci-pr-mergeability.cjs +262 -0
  348. package/scripts/ci-test-scope.cjs +45 -12
  349. package/scripts/ci-timeout-report.cjs +230 -0
  350. package/scripts/docs-guard-registry.cjs +396 -0
  351. package/scripts/gen-capability-registry.cjs +8 -6
  352. package/scripts/gen-exit-code-docs.cjs +318 -0
  353. package/scripts/gen-exit-code-registry.cjs +891 -0
  354. package/scripts/gen-features.cjs +836 -0
  355. package/scripts/gen-hooks-cli-exit.cjs +239 -0
  356. package/scripts/gen-install-tree-fixtures.cjs +2 -2
  357. package/scripts/gen-loop-host-contract.cjs +134 -1
  358. package/scripts/gen-scripts-cli-exit.cjs +185 -0
  359. package/scripts/gen-state-md-docs.cjs +727 -0
  360. package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
  361. package/scripts/lib/ci-job-timing.cjs +72 -0
  362. package/scripts/lib/cli-exit.cjs +546 -44
  363. package/scripts/lib/drift-scan.cjs +32 -2
  364. package/scripts/lib/exit-code-registry.cjs +98 -0
  365. package/scripts/lib/ndjson-reporter.cjs +119 -0
  366. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  367. package/scripts/lint-docs-guard-registration.cjs +495 -0
  368. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +193 -0
  369. package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
  370. package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
  371. package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
  372. package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
  373. package/scripts/lint-phase-enumeration-drift.cjs +21 -8
  374. package/scripts/lint-planning-prompt-drift.cjs +38 -1
  375. package/scripts/lint-removed-but-needed.cjs +184 -16
  376. package/scripts/lint-seam-enforcement.cjs +182 -0
  377. package/scripts/lint-slug-derivation-drift.cjs +921 -0
  378. package/scripts/lint-source-test-name-collision.cjs +241 -0
  379. package/scripts/lint-state-write-path-drift.cjs +337 -432
  380. package/scripts/lint-test-file-count.allowlist.json +122 -4
  381. package/scripts/lint-test-file-count.cjs +25 -3
  382. package/scripts/lint-unreachable-guard-drift.cjs +51 -64
  383. package/scripts/lint-vendored-deps.cjs +208 -35
  384. package/scripts/mutation-matrix.cjs +599 -50
  385. package/scripts/prompt-injection-scan.sh +75 -14
  386. package/scripts/secret-scan.sh +75 -13
  387. package/scripts/select-docs-guards.cjs +56 -0
  388. package/scripts/sync-runtime-launcher.cjs +22 -3
  389. package/skills/gsd-discuss-phase/SKILL.md +1 -1
  390. package/skills/gsd-import/SKILL.md +1 -1
  391. package/skills/gsd-quick/SKILL.md +8 -4
  392. package/vscode/package.json +1 -1
  393. package/bin/lib/ui-safety-gate.cjs +0 -109
  394. package/scripts/lint-emitted-drift-ack.cjs +0 -344
  395. package/scripts/state-write-path-drift-baseline.json +0 -19
@@ -14,6 +14,17 @@
14
14
  * it imports `stripBOM`/`scanTomlLines` from here and its regression suite
15
15
  * (`tests/agent-install-check.test.cjs`) is the proof.
16
16
  *
17
+ * #3897 rung 3 amendment: `deriveCodexSandboxMode`/`CODEX_SANDBOX_HOLDS`/
18
+ * `validateCodexSandboxHolds` also live here now (moved from `bin/install.js`).
19
+ * That IS a policy (which `sandbox_mode` a role's tool contract derives), a
20
+ * narrow exception to this module's "document model, not policy" charter above
21
+ * — made because `bin/install.js` cannot be the shared owner: requiring it for
22
+ * its side effect on `require()` (the CLI banner print) corrupts every
23
+ * stdout-JSON caller (`agent-install-check.cts`'s `checkCodexSandboxPosture`).
24
+ * This module was already the single fs/path-free-parsing home both callers
25
+ * shared; `fs`/`path` are imported below ONLY for `validateCodexSandboxHolds`'s
26
+ * roster check — still node builtins only, no third-party or bin/lib dependency.
27
+ *
17
28
  * ── The reconciliation (40-design.md) ──────────────────────────────────────
18
29
  *
19
30
  * Phase 2's reader and this phase's writer disagree on how to handle an
@@ -31,8 +42,11 @@
31
42
  * One block-range detector, two call sites, two policies — never two detectors
32
43
  * that could silently drift from each other.
33
44
  */
45
+ var __importDefault = (this && this.__importDefault) || function (mod) {
46
+ return (mod && mod.__esModule) ? mod : { "default": mod };
47
+ };
34
48
  Object.defineProperty(exports, "__esModule", { value: true });
35
- exports.PARSE_REASON = void 0;
49
+ exports.CODEX_SANDBOX_HOLDS = exports.PARSE_REASON = void 0;
36
50
  exports.stripBOM = stripBOM;
37
51
  exports.unquoteTomlValue = unquoteTomlValue;
38
52
  exports.findDeveloperInstructionsBlockRange = findDeveloperInstructionsBlockRange;
@@ -41,6 +55,13 @@ exports.parseCodexAgentToml = parseCodexAgentToml;
41
55
  exports.renderCodexAgentToml = renderCodexAgentToml;
42
56
  exports.stripModel = stripModel;
43
57
  exports.stripReasoningEffort = stripReasoningEffort;
58
+ exports.normalizeSandboxIdentity = normalizeSandboxIdentity;
59
+ exports.isSandboxHeld = isSandboxHeld;
60
+ exports.extractToolsValue = extractToolsValue;
61
+ exports.deriveCodexSandboxMode = deriveCodexSandboxMode;
62
+ exports.validateCodexSandboxHolds = validateCodexSandboxHolds;
63
+ const node_fs_1 = __importDefault(require("node:fs"));
64
+ const node_path_1 = __importDefault(require("node:path"));
44
65
  /** Frozen reason enum for a failed {@link parseCodexAgentToml}. */
45
66
  exports.PARSE_REASON = Object.freeze({
46
67
  UNTERMINATED_BLOCK: 'unterminated_block',
@@ -108,11 +129,15 @@ function findDeveloperInstructionsBlockRange(lines) {
108
129
  // lenient reader, boolean-only for reasoning effort) and parseCodexAgentToml
109
130
  // (the strict writer, which also needs the effort's value and both keys' line
110
131
  // indices so stripModel/stripReasoningEffort can remove exactly one line).
132
+ // `sandbox_mode` (#3897 rung 4 MINOR finding 2) rides the same block-aware
133
+ // pass as `model`/`model_reasoning_effort` — one scanner, never a second,
134
+ // naive whole-file regex that could match prose inside the block.
111
135
  function scanHeaderLines(lines, blockStart, blockEnd) {
112
136
  let model = null;
113
137
  let modelLineIndex = null;
114
138
  let reasoningEffort = null;
115
139
  let reasoningEffortLineIndex = null;
140
+ let sandboxMode = null;
116
141
  for (let i = 0; i < lines.length; i++) {
117
142
  if (blockStart !== -1 && i >= blockStart && i <= blockEnd)
118
143
  continue;
@@ -132,8 +157,11 @@ function scanHeaderLines(lines, blockStart, blockEnd) {
132
157
  reasoningEffort = unquoteTomlValue(rawValue);
133
158
  reasoningEffortLineIndex = i;
134
159
  }
160
+ else if (key === 'sandbox_mode') {
161
+ sandboxMode = unquoteTomlValue(rawValue);
162
+ }
135
163
  }
136
- return { model, modelLineIndex, reasoningEffort, reasoningEffortLineIndex };
164
+ return { model, modelLineIndex, reasoningEffort, reasoningEffortLineIndex, sandboxMode };
137
165
  }
138
166
  /**
139
167
  * The LENIENT reader entry point (Phase 2, moved verbatim in behavior). Never
@@ -144,8 +172,8 @@ function scanHeaderLines(lines, blockStart, blockEnd) {
144
172
  function scanTomlLines(content) {
145
173
  const lines = content.split(/\r?\n/);
146
174
  const { start, end } = findDeveloperInstructionsBlockRange(lines);
147
- const { model, reasoningEffort } = scanHeaderLines(lines, start, end);
148
- return { model, hasReasoningEffort: reasoningEffort !== null };
175
+ const { model, reasoningEffort, sandboxMode } = scanHeaderLines(lines, start, end);
176
+ return { model, hasReasoningEffort: reasoningEffort !== null, sandboxMode };
149
177
  }
150
178
  // Splits `content` into `{lines, terminators}` where `terminators[i]` is the
151
179
  // terminator that FOLLOWS `lines[i]` (`'\r\n'`, `'\r'`, `'\n'`, or `''` for a
@@ -327,3 +355,381 @@ function stripReasoningEffort(doc) {
327
355
  return doc;
328
356
  return removeLine(doc, doc.reasoningEffortLineIndex, 'reasoningEffort');
329
357
  }
358
+ // ── Codex sandbox_mode derivation (#3897 rung 3, ADR-3473 §8.3) ────────────
359
+ //
360
+ // Moved here from `bin/install.js` (fix for the CAUSE A regression this rung
361
+ // introduced): `agent-install-check.cts`'s `checkCodexSandboxPosture` used to
362
+ // lazily `require(bin/install.js)` to reach this derivation — but requiring
363
+ // `bin/install.js` runs its whole top-level script, including the ASCII
364
+ // banner print to stdout, which corrupted every stdout-JSON caller downstream
365
+ // of `checkCodexSandboxPosture` (`gsd-tools validate agents`). This module is
366
+ // a genuine leaf with no top-level side effects, so both `bin/install.js` and
367
+ // `agent-install-check.cts` import the derivation from here instead — ONE
368
+ // owner, no second predicate (routing `src/` through `bin/install.js` was
369
+ // backwards layering to begin with).
370
+ //
371
+ // This module does NOT parse frontmatter (there is no third copy of that
372
+ // extraction here — two already exist, `bin/install.js` and
373
+ // `runtime-artifact-conversion.cts`). `deriveCodexSandboxMode` below takes
374
+ // the already-resolved `tools:` value as a plain string parameter: every
375
+ // caller already has it (or the raw frontmatter to pull it from) in hand
376
+ // before calling in, so this module stays a pure predicate over data
377
+ // supplied by the caller — never a document reader itself. This also means
378
+ // the module never needs the full YAML-backed `frontmatter.cts` engine
379
+ // (vendored js-yaml + anchor/alias refusal + comment-channel plumbing, built
380
+ // for a much broader contract than a single `tools:` line lookup) — it has
381
+ // no frontmatter-parsing need at all anymore.
382
+ /**
383
+ * The 17 roles measured as widening under derivation (declare Write/Edit,
384
+ * never in the pre-#3897 `CODEX_AGENT_SANDBOX` map, so the old
385
+ * `|| 'read-only'` fallback silently under-granted them). Pinned to
386
+ * `read-only` pending the open question of whether Codex enforces
387
+ * `sandbox_mode` or treats it as advisory (HALT.md). This list is CLOSED and
388
+ * SHRINK-ONLY: a new writing role never lands here (S6, T26); it is validated
389
+ * against the live tool contract every time it is consulted
390
+ * ({@link _deriveCodexSandboxModeFromTools}) and against the real
391
+ * `agents/` roster by a dedicated test (`tests/codex-config.test.cjs` T24/T25)
392
+ * so a stale or orphaned entry fails loudly instead of being silently
393
+ * honored forever.
394
+ *
395
+ * `gsd-nyquist-auditor` is the 17th entry, added by the list-form `tools:`
396
+ * parse fix (#3897 follow-up): its `tools:` frontmatter uses YAML block-list
397
+ * form (`tools:` + indented `- Item` lines) and declares both Write and
398
+ * Edit, but the original `extractToolsLine`/`extractFrontmatterField`
399
+ * readers only ever saw the first list item (`- Read`) and derived
400
+ * `read-only` by accident, not by design. {@link extractToolsValue} now
401
+ * parses the list correctly, so this role genuinely derives
402
+ * `workspace-write` from its tool contract — HALT.md's original 16-role
403
+ * count measured against the pre-fix (single-line) readers and undercounted
404
+ * this role. It is held here for the same reason as the other 16: pending
405
+ * Codex's `sandbox_mode` enforcement decision, not because the derivation is
406
+ * wrong.
407
+ */
408
+ exports.CODEX_SANDBOX_HOLDS = Object.freeze({
409
+ 'gsd-ai-researcher': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
410
+ 'gsd-code-fixer': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
411
+ 'gsd-code-reviewer': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
412
+ 'gsd-debug-session-manager': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
413
+ 'gsd-doc-classifier': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
414
+ 'gsd-doc-synthesizer': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
415
+ 'gsd-doc-verifier': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
416
+ 'gsd-doc-writer': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
417
+ 'gsd-dom-verifier': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
418
+ 'gsd-domain-researcher': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
419
+ 'gsd-eval-auditor': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
420
+ 'gsd-eval-planner': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
421
+ 'gsd-intel-updater': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
422
+ 'gsd-pattern-mapper': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
423
+ 'gsd-ui-auditor': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
424
+ 'gsd-ui-researcher': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
425
+ 'gsd-nyquist-auditor': 'declares Write/Edit (YAML list-form tools:, surfaced by the list-form parse fix); pending Codex sandbox_mode enforcement decision',
426
+ });
427
+ // True iff a `tools:` frontmatter value declares Write or Edit as a whole
428
+ // token (never a substring match, so a hypothetical "Edith"-named tool could
429
+ // never collide). Single predicate owner for both the emitter
430
+ // ({@link _deriveCodexSandboxModeFromTools}) and the posture check
431
+ // (`agent-install-check.cts`'s `checkCodexSandboxPosture`, via
432
+ // {@link deriveCodexSandboxMode}).
433
+ //
434
+ // #3897 security review F5: a negation form — `All tools except Agent,
435
+ // Write, Edit` — used to derive `workspace-write` because the plain
436
+ // comma-tokenizer below saw the literal token `Write` and never noticed it
437
+ // was named as an EXCLUSION, not a grant. That is fail-OPEN: the value's
438
+ // stated meaning is "everything except these", so a `Write`/`Edit` name
439
+ // after `except` means the role does NOT get them. No shipped roster agent
440
+ // uses this form today, but a wrong answer here widens silently, so it is
441
+ // handled: once the literal word `except` (case-insensitive) appears
442
+ // anywhere in the value, this is a "<grant> except <exclusions>" statement
443
+ // and only the tokens AFTER `except` decide the verdict — `Write`/`Edit`
444
+ // declare broader iff `except` is ABSENT, or present but does not name them.
445
+ function _codexToolsDeclareWriteOrEdit(toolsRaw) {
446
+ const raw = String(toolsRaw || '');
447
+ const exceptIndex = raw.search(/\bexcept\b/i);
448
+ if (exceptIndex !== -1) {
449
+ const exclusionsText = raw.slice(exceptIndex).replace(/^\S*\s*except\b/i, '');
450
+ const exclusions = exclusionsText.split(',').map((t) => t.trim()).filter(Boolean);
451
+ const excludesWriteOrEdit = exclusions.includes('Write') || exclusions.includes('Edit');
452
+ return !excludesWriteOrEdit;
453
+ }
454
+ const tokens = raw.split(',').map((t) => t.trim());
455
+ return tokens.includes('Write') || tokens.includes('Edit');
456
+ }
457
+ // #3897 security review F1/F3: control-character/whitespace class used by
458
+ // {@link normalizeSandboxIdentity} to trim more than plain ASCII whitespace
459
+ // from BOTH edges of a candidate identity before it is compared against the
460
+ // hold set. A bare .trim() only strips the ASCII whitespace \s already
461
+ // covers; the hold bypass this closes (a trailing space, \n/\r, or NBSP
462
+ // after `gsd-doc-writer`) is exactly a value that survives .trim() untouched.
463
+ // Spelled entirely as \u escapes (never a literal invisible codepoint in
464
+ // the source), matching this module's own BOM_CHAR convention above: NBSP
465
+ // (U+00A0), the zero-width space/non-joiner/joiner + LTR/RTL marks
466
+ // (U+200B-U+200F), the BOM/ZWNBSP (U+FEFF), and the full C0 control range
467
+ // + DEL (U+0000-U+001F, U+007F), alongside ASCII whitespace (\s).
468
+ const SANDBOX_IDENTITY_EDGE_RE = new RegExp('^[\\s\\u0000-\\u001f\\u007f\\u00a0\\u200b-\\u200f\\ufeff]+' +
469
+ '|[\\s\\u0000-\\u001f\\u007f\\u00a0\\u200b-\\u200f\\ufeff]+$', 'g');
470
+ // Anything outside this set, AFTER normalization, is "suspicious" — see
471
+ // {@link isSandboxHeld}'s docblock for why fail-closed (never enumerate
472
+ // confusables) is the only tractable answer here.
473
+ const SANDBOX_IDENTITY_SUSPICIOUS_RE = /[^a-z0-9._-]/;
474
+ /**
475
+ * Canonicalizes a single candidate sandbox-hold identity (a source filename
476
+ * stem OR a frontmatter-derived `name:` value) into the same lowercase ASCII
477
+ * shape {@link CODEX_SANDBOX_HOLDS}'s keys are written in, so both candidate
478
+ * identities for one emitted artifact (see {@link isSandboxHeld}) can be
479
+ * compared against the hold set on equal footing.
480
+ *
481
+ * #3897 security review F1/F3 — pipeline, in this exact order:
482
+ * 1. basename it (strip through the LAST `/` or `\`) — a value carrying a
483
+ * path separator (`../agents/x`, `./x`) is reduced to its final segment
484
+ * before anything else runs, so a path-traversal-shaped candidate can
485
+ * never dodge the hold lookup by hiding behind a directory prefix.
486
+ * 2. trim ASCII whitespace AND the NBSP/zero-width/control-char class
487
+ * ({@link SANDBOX_IDENTITY_EDGE_RE}) from both ends.
488
+ * 3. strip trailing dots (`gsd-doc-writer.` collapses to the same key).
489
+ * 4. `String.prototype.normalize('NFKC')` — canonicalizes compatibility
490
+ * variants (e.g. fullwidth `g` U+FF47 folds to ASCII `g`) to their
491
+ * standard form so a widened-by-Unicode-lookalike name collapses onto
492
+ * the real held key instead of merely looking similar to it.
493
+ * 5. lowercase — ASCII recasing already matched the hold set before this
494
+ * rung; this keeps that property after the above steps.
495
+ *
496
+ * Returns `null` (never throws) for a non-string input or when the fully
497
+ * normalized result is empty, so callers can treat `null` as "not a real
498
+ * identity, cannot be held, but see the `suspicious` companion signal".
499
+ */
500
+ function normalizeSandboxIdentity(raw) {
501
+ if (typeof raw !== 'string')
502
+ return null;
503
+ const lastSlash = Math.max(raw.lastIndexOf('/'), raw.lastIndexOf('\\'));
504
+ let value = lastSlash === -1 ? raw : raw.slice(lastSlash + 1);
505
+ value = value.replace(SANDBOX_IDENTITY_EDGE_RE, '');
506
+ value = value.replace(/\.+$/, '');
507
+ value = value.normalize('NFKC');
508
+ value = value.toLowerCase();
509
+ return value === '' ? null : value;
510
+ }
511
+ /**
512
+ * `held` is true iff ANY candidate identity, once normalized, names a real
513
+ * {@link CODEX_SANDBOX_HOLDS} entry. `suspicious` is true iff any candidate,
514
+ * after normalization, still contains a character outside `[a-z0-9._-]`.
515
+ *
516
+ * #3897 security review F1 (the blocker): the sandbox decision must be safe
517
+ * for the ARTIFACT IT LANDS ON, not for whichever single identity happens to
518
+ * be handy at the call site. `bin/install.js`'s emit loop has TWO candidate
519
+ * identities for one `.toml` artifact — the source filename stem (what the
520
+ * hold lookup used to be keyed on) and the frontmatter `name:` value (what
521
+ * the OUTPUT PATH is actually keyed on) — and an attacker who controls
522
+ * frontmatter can make them disagree (rename the file, or plant a sibling
523
+ * file whose `name:` collides with a held role). Deciding over only one of
524
+ * the two and applying the result to whichever path the OTHER one names is
525
+ * exactly the regression this closes: this predicate takes every candidate
526
+ * that can influence either the derivation or the emitted path and takes the
527
+ * MOST RESTRICTIVE answer — `held` if any one of them is held.
528
+ *
529
+ * #3897 security review F3: `suspicious` is the fail-CLOSED half of this
530
+ * predicate, and it is deliberately not an attempt to enumerate Unicode
531
+ * confusables (Turkish dotted/dotless I, fullwidth forms, NFD combining
532
+ * marks, ...). Every one of the 35 shipped roster files is pure ASCII
533
+ * (`gsd-[a-z-]+`), so a non-ASCII-after-normalization identity cannot be a
534
+ * legitimate shipped role — flagging it `suspicious` (which
535
+ * {@link deriveCodexSandboxMode} turns into `read-only`) has zero false
536
+ * positives against real content, and refuses to widen on anything this
537
+ * module does not recognize instead of chasing an open-ended confusables
538
+ * list that will always be one codepoint behind the next lookalike.
539
+ */
540
+ function isSandboxHeld(...candidates) {
541
+ let held = false;
542
+ let suspicious = false;
543
+ for (const candidate of candidates) {
544
+ const values = Array.isArray(candidate) ? candidate : [candidate];
545
+ for (const value of values) {
546
+ const normalized = normalizeSandboxIdentity(value);
547
+ if (normalized === null)
548
+ continue;
549
+ if (Object.prototype.hasOwnProperty.call(exports.CODEX_SANDBOX_HOLDS, normalized)) {
550
+ held = true;
551
+ }
552
+ if (SANDBOX_IDENTITY_SUSPICIOUS_RE.test(normalized)) {
553
+ suspicious = true;
554
+ }
555
+ }
556
+ }
557
+ return { held, suspicious };
558
+ }
559
+ // #3897 CAUSE A fix: a single-purpose `tools:`-VALUE reader, NOT a general
560
+ // frontmatter parser (`extractFrontmatterAndBody`/`extractFrontmatterField`
561
+ // were deliberately deleted from this module's ancestor — see the module
562
+ // header's "document model, not policy" charter). This module is the one
563
+ // genuinely side-effect-free leaf `agent-install-check.cts`'s
564
+ // `checkCodexSandboxPosture` already imports from for
565
+ // {@link deriveCodexSandboxMode}; giving it this reader too avoids importing
566
+ // `runtime-artifact-conversion.cjs` there just to pull one frontmatter field
567
+ // — that module's own dependency chain (`command-roster.cjs` →
568
+ // `scripts/fix-slash-commands.cjs`, a dev-only repo script) does not resolve
569
+ // from an installed tree, which is exactly what broke
570
+ // `tests/agent-install-check.test.cjs` against a synthetic install dir.
571
+ // Scoped to ONLY the `tools:` key: finds the leading `---`-delimited
572
+ // frontmatter block and returns its `tools:` value (quotes stripped), or
573
+ // `''` when there is no frontmatter block or no `tools:` key at all.
574
+ //
575
+ // #3897 rung 4/list-form fix: handles BOTH shapes a `tools:` key can take —
576
+ // (a) inline: `tools: Read, Write, Edit` on the same line as the key, and
577
+ // (b) YAML block-list form: `tools:` alone, followed by indented `- Item`
578
+ // lines (`agents/gsd-nyquist-auditor.md`, `agents/gsd-security-auditor.md`
579
+ // are the only two roster files using this shape today). The former
580
+ // implementation was a single `/^tools:\s*(.+)$/m` regex — `\s*` swallows the
581
+ // newline after a bare `tools:` key, so it silently matched into the FIRST
582
+ // list item's own line and returned just `"- Read"`, reading a real
583
+ // Write/Edit DECLARATION as an absence. List items are joined with `, ` so
584
+ // the result feeds {@link _codexToolsDeclareWriteOrEdit}'s comma-tokenizer
585
+ // unchanged. The list terminates at the first line that is not an indented
586
+ // `- Item` (a new frontmatter key, `---`, a blank line, or EOF) — it never
587
+ // grows into a general YAML parser (that boundary is deliberate, see above).
588
+ //
589
+ // #3897 security review F4: TOTAL for any input, not just a well-formed
590
+ // string — a Buffer, `undefined`, or `null` returns `undefined` rather than
591
+ // throwing on `.startsWith`, matching this module's "never throws" charter
592
+ // (see {@link deriveCodexSandboxMode}'s own totality note).
593
+ function extractToolsValue(agentContent) {
594
+ if (typeof agentContent !== 'string')
595
+ return undefined;
596
+ if (!agentContent.startsWith('---'))
597
+ return '';
598
+ const endIndex = agentContent.indexOf('---', 3);
599
+ if (endIndex === -1)
600
+ return '';
601
+ const frontmatter = agentContent.substring(3, endIndex);
602
+ const lines = frontmatter.split(/\r?\n/);
603
+ const toolsLineIndex = lines.findIndex((line) => /^tools:/.test(line));
604
+ if (toolsLineIndex === -1)
605
+ return '';
606
+ const inlineMatch = lines[toolsLineIndex].match(/^tools:[ \t]*(\S.*)$/);
607
+ if (inlineMatch) {
608
+ return inlineMatch[1].trim().replace(/^['"]|['"]$/g, '');
609
+ }
610
+ // Bare `tools:` key (nothing but optional trailing whitespace on its own
611
+ // line) — read the YAML block-list form that follows: indented `- Item`
612
+ // lines, one item per line, stopping at the first line that is not one.
613
+ const items = [];
614
+ for (let i = toolsLineIndex + 1; i < lines.length; i++) {
615
+ const itemMatch = lines[i].match(/^[ \t]+-[ \t]*(.+)$/);
616
+ if (!itemMatch)
617
+ break;
618
+ items.push(itemMatch[1].trim().replace(/^['"]|['"]$/g, ''));
619
+ }
620
+ return items.join(', ');
621
+ }
622
+ /**
623
+ * The single owner of `sandbox_mode` derivation: `workspace-write` iff the
624
+ * role's own frontmatter `tools:` declares Write/Edit, UNLESS the role is
625
+ * held ({@link CODEX_SANDBOX_HOLDS}) at `read-only`.
626
+ *
627
+ * #3897 CAUSE C fix: this function is TOTAL — it never throws, for any
628
+ * (identity, toolsValue) pair. It used to throw when a held role's CONTENT
629
+ * (whatever the caller handed it) did not derive broader than its pin, on
630
+ * the theory that a stale hold should "fail loudly at the point the
631
+ * derivation runs". That reasoning does not survive contact with this
632
+ * function's actual contract: it is a pure predicate over whatever content
633
+ * the caller supplies, and tests legitimately pass synthetic fixtures for
634
+ * held role names, so the throw fired on arbitrary input, not just the real
635
+ * roster. It is also redundant even for real input — if a held role's
636
+ * content does not derive broader, applying the hold pins `read-only` and
637
+ * derivation would return `read-only` anyway; the hold is a no-op, so there
638
+ * is nothing to fail about at derivation time. The STALENESS invariant (S4)
639
+ * is a property of the real `agents/` roster, not of an arbitrary call's
640
+ * input, so it belongs — and stays enforced — only at the roster level: see
641
+ * {@link validateCodexSandboxHolds} and `tests/codex-config.test.cjs`
642
+ * T24/T25, which assert it against the real roster directly.
643
+ *
644
+ * `agentName` here MUST be every identity the content's own frontmatter
645
+ * could disagree with — i.e. the source `.md` FILENAME stem AND (when the
646
+ * caller has one) the frontmatter-derived `name:` display value, passed
647
+ * together as an array. Accepting only one of the two and applying the
648
+ * result to an ARTIFACT keyed on the other is the #3897 security review F1
649
+ * regression this function closes: `bin/install.js`'s emit loop decides
650
+ * `sandbox_mode` for the filename stem but writes the `.toml` at a path keyed
651
+ * on `name`, so a renamed file (stem drifts, `name:` stays pinned) or a
652
+ * sibling file whose `name:` collides with a held role could make the two
653
+ * identities disagree and land a held role's own artifact with
654
+ * `workspace-write`. See {@link isSandboxHeld} for the most-restrictive-wins
655
+ * rule across every candidate identity.
656
+ *
657
+ * `toolsRaw` is the already-resolved `tools:` frontmatter VALUE (e.g.
658
+ * `"Read, Write, Edit"`), not the frontmatter block or the full agent
659
+ * content — see {@link deriveCodexSandboxMode}'s doc for why.
660
+ */
661
+ function _deriveCodexSandboxModeFromTools(agentName, toolsRaw) {
662
+ const derivesBroader = _codexToolsDeclareWriteOrEdit(toolsRaw || '');
663
+ const { held, suspicious } = isSandboxHeld(agentName);
664
+ if (held || suspicious) {
665
+ // A held (or unrecognizable/"suspicious", per F3) identity always pins
666
+ // read-only, whether or not its supplied content still derives broader
667
+ // (see #3897 CAUSE C note above the docstring for this function's
668
+ // caller-facing counterpart): if it no longer derives broader, the pin is
669
+ // a no-op and there is nothing to fail about here — the staleness
670
+ // invariant lives at the roster level instead
671
+ // ({@link validateCodexSandboxHolds}).
672
+ return 'read-only';
673
+ }
674
+ return derivesBroader ? 'workspace-write' : 'read-only';
675
+ }
676
+ /**
677
+ * Exported form for external callers (`checkCodexSandboxPosture`,
678
+ * `generateCodexAgentToml`). Takes the already-resolved `tools:` frontmatter
679
+ * VALUE, not raw agent content or a frontmatter slice — this module does no
680
+ * frontmatter parsing at all (there is no third copy of that extraction
681
+ * here; see the module-header note above `CODEX_SANDBOX_HOLDS`). Both
682
+ * callers already have (or can trivially get) `tools:` in hand via this
683
+ * module's own {@link extractToolsValue} — the single shared extractor both
684
+ * `bin/install.js`'s `generateCodexAgentToml` and `agent-install-check.cts`'s
685
+ * `checkCodexSandboxPosture` route through (#3897 CAUSE A fix, and the
686
+ * #3897 list-form parse fix's Fix 3: `bin/install.js` used to read `tools:`
687
+ * via its own private `extractFrontmatterField`, a second single-line-only
688
+ * copy that disagreed with this module's reader on YAML block-list form —
689
+ * NOT `runtime-artifact-conversion.cts`'s general-purpose extractors either;
690
+ * that module's dependency chain does not resolve from an installed tree) —
691
+ * so this stays a pure predicate over data the caller already holds, never a
692
+ * document reader.
693
+ *
694
+ * `agentName` is one or more HOLD-LOOKUP IDENTITIES: pass the agent's
695
+ * canonical source filename stem (e.g. `gsd-doc-writer` for
696
+ * `agents/gsd-doc-writer.md`) as a plain string for the single-identity call
697
+ * sites this signature always supported, OR an array of every identity that
698
+ * can influence the emitted artifact (filename stem AND frontmatter `name:`)
699
+ * when the caller — like `bin/install.js`'s per-agent emit loop — has both
700
+ * (see the F1 security note on {@link _deriveCodexSandboxModeFromTools}
701
+ * above). TOTAL for any value here, including `undefined`, `null`, `[]`, and
702
+ * an array containing non-string members — none of those throw; they simply
703
+ * contribute nothing to the hold/suspicious check (see {@link isSandboxHeld}
704
+ * / {@link normalizeSandboxIdentity}).
705
+ */
706
+ function deriveCodexSandboxMode(agentName, toolsRaw) {
707
+ return _deriveCodexSandboxModeFromTools(agentName, toolsRaw || '');
708
+ }
709
+ /**
710
+ * (S5) Every {@link CODEX_SANDBOX_HOLDS} key must still name a real
711
+ * `<agentsSrcDir>/<role>.md`. A hold for a role that no longer exists is
712
+ * stale and must fail loudly, not be silently ignored — else the hold list
713
+ * only ever grows/rots instead of shrinking to zero.
714
+ *
715
+ * NOT called from the install runtime path ({@link deriveCodexSandboxMode} /
716
+ * `bin/install.js`'s `installCodexConfig`): the shrink-to-zero invariant it
717
+ * checks is a REPO invariant about the canonical roster in `agents/`, not a
718
+ * property of whatever directory an install happens to read from — a
719
+ * partial/synthetic install source (a test fixture, a `--config-dir`
720
+ * subset) legitimately contains only a few agents, and a hold whose role is
721
+ * simply absent from THAT source dir must be inert, not fatal. The invariant
722
+ * is enforced instead as a test over the real `agents/` roster
723
+ * (`tests/codex-config.test.cjs` T24/T25). This function is kept, exported,
724
+ * for any caller that specifically wants to validate the CANONICAL roster
725
+ * (pass `agents/` itself, never an arbitrary install source).
726
+ */
727
+ function validateCodexSandboxHolds(agentsSrcDir) {
728
+ for (const role of Object.keys(exports.CODEX_SANDBOX_HOLDS)) {
729
+ const agentFile = node_path_1.default.join(agentsSrcDir, `${role}.md`);
730
+ if (!node_fs_1.default.existsSync(agentFile)) {
731
+ throw new Error(`CODEX_SANDBOX_HOLDS: stale hold for "${role}" — no ${agentFile} exists. The hold list is ` +
732
+ 'closed and shrink-only (ADR-3473 §8.3 / HALT.md); remove this entry.');
733
+ }
734
+ }
735
+ }
@@ -2,41 +2,171 @@
2
2
  /**
3
3
  * Command Argument Projection Module (ADR-457 build-at-publish: the
4
4
  * hand-written bin/lib/command-arg-projection.cjs collapsed to a TypeScript
5
- * source of truth). Behaviour is preserved byte-for-behaviour from the prior
6
- * hand-written .cjs; only types are added.
5
+ * source of truth).
6
+ *
7
+ * ADR-3473 §8.4 ("failure is a value"): `parseNamedArgs` is now strict and
8
+ * returns a `Result` instead of silently accepting unrecognized or stray
9
+ * positional tokens. See .gsd/phase/feat-3884-failure-is-a-value/40-design.md
10
+ * for the full behavior table (A1-A18) and negative-space section (N1-N8).
7
11
  *
8
12
  * Shared helpers for command-family adapters to project argv tokens into
9
13
  * typed named values and multi-word segments.
10
14
  */
11
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.isFlagToken = isFlagToken;
12
17
  exports.parseNamedArgs = parseNamedArgs;
18
+ exports.parseNamedArgsOrExit = parseNamedArgsOrExit;
13
19
  exports.parseMultiwordArg = parseMultiwordArg;
20
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
21
+ const io = require("./io.cjs");
22
+ const { ERROR_REASON, formatDiagnosticToken } = io;
23
+ // ─── Internal helpers ─────────────────────────────────────────────────────────
24
+ function isPlainObject(v) {
25
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
26
+ }
14
27
  /**
15
- * Extract named --flag <value> pairs from an args array.
16
- * Returns an object mapping flag names to their values (null if absent).
17
- * Flags listed in `booleanFlags` are treated as booleans.
28
+ * ADR-3473 Decision 2: both ends of this seam are gsd-core's own source, so a
29
+ * malformed spec (a stale call site still using the legacy
30
+ * `parseNamedArgs(args, valueFlags, booleanFlags)` shape, or a missing spec
31
+ * entirely) is an internal invariant violation, not user input — it throws
32
+ * loudly instead of destructuring `undefined` off a Result.
33
+ */
34
+ function assertValidSpec(spec) {
35
+ if (!isPlainObject(spec)) {
36
+ throw new TypeError('parseNamedArgs: spec must be an object of shape ' +
37
+ '{ valueFlags?: string[], booleanFlags?: string[], positionals: number | "rest" }. ' +
38
+ 'Received a missing, array, or non-object value — this is the legacy ' +
39
+ 'parseNamedArgs(args, valueFlags, booleanFlags) call shape, retired by ADR-3473 §8.4.');
40
+ }
41
+ const positionals = spec.positionals;
42
+ const positionalsValid = positionals === 'rest' ||
43
+ (typeof positionals === 'number' && Number.isInteger(positionals) && positionals >= 0);
44
+ if (!positionalsValid) {
45
+ throw new TypeError('parseNamedArgs: spec.positionals must be a non-negative integer or the literal "rest" ' +
46
+ `— received ${JSON.stringify(positionals)}.`);
47
+ }
48
+ }
49
+ // Single predicate reused for both extraction (a value beginning with a
50
+ // single `-` is a value, not a flag) and validation (negative space N5).
51
+ // Exported for init-command-router's #3865 --phase alias normalization,
52
+ // which needs the same flag-shape test before deciding whether args[2] is a
53
+ // phase positional or a flag token.
54
+ function isFlagToken(tok) {
55
+ return tok.startsWith('--');
56
+ }
57
+ // ─── parseNamedArgs ───────────────────────────────────────────────────────────
58
+ /**
59
+ * Project argv tokens into typed named values, then strictly validate every
60
+ * token past the caller's declared positional boundary.
61
+ *
62
+ * Extraction (unchanged semantics, #312): first occurrence wins; a value
63
+ * flag whose next token is absent or starts with `--` yields `null` (this is
64
+ * NOT a validation error — see the value-flag branch below); boolean flags
65
+ * and optional-value flags (`optionalValueFlags`, #2932's `--wave` shape) are
66
+ * presence tests. Kept as a single first-index Map so the flag loops don't
67
+ * each re-scan argv — O(argv + flags) instead of O(flags * argv).
68
+ *
69
+ * Validation (skipped entirely when `positionals === 'rest'`): a single
70
+ * left-to-right cursor walk from `spec.positionals`, per the design doc's
71
+ * Kernighan's Law note — debuggable over clever, never a set-difference.
18
72
  */
19
- function parseNamedArgs(args, valueFlags = [], booleanFlags = []) {
20
- // Index each token's first position once (firstIndex.get(t) ?? -1 === args.indexOf(t),
21
- // firstIndex.has(t) === args.includes(t)) so the flag loops below don't each re-scan
22
- // argv — O(argv + flags) instead of O(flags * argv). Semantics are unchanged. (#312)
73
+ function parseNamedArgs(args, spec) {
74
+ assertValidSpec(spec);
75
+ const valueFlags = spec.valueFlags ?? [];
76
+ const booleanFlags = spec.booleanFlags ?? [];
77
+ const optionalValueFlags = spec.optionalValueFlags ?? [];
23
78
  const firstIndex = new Map();
24
79
  for (let i = 0; i < args.length; i++) {
25
80
  if (!firstIndex.has(args[i]))
26
81
  firstIndex.set(args[i], i);
27
82
  }
28
- const result = {};
83
+ const data = {};
29
84
  for (const flag of valueFlags) {
30
85
  const idx = firstIndex.has(`--${flag}`) ? firstIndex.get(`--${flag}`) : -1;
31
- result[flag] =
32
- idx !== -1 && args[idx + 1] !== undefined && !args[idx + 1].startsWith('--')
86
+ data[flag] =
87
+ idx !== -1 && args[idx + 1] !== undefined && !isFlagToken(args[idx + 1])
33
88
  ? args[idx + 1]
34
89
  : null;
35
90
  }
36
91
  for (const flag of booleanFlags) {
37
- result[flag] = firstIndex.has(`--${flag}`);
92
+ data[flag] = firstIndex.has(`--${flag}`);
93
+ }
94
+ // Optional-value flags (#2932: `--wave`-shaped) — presence-only, exactly
95
+ // like booleanFlags. The value (if any) is deliberately not surfaced here;
96
+ // see the NamedArgSpec.optionalValueFlags JSDoc.
97
+ for (const flag of optionalValueFlags) {
98
+ data[flag] = firstIndex.has(`--${flag}`);
99
+ }
100
+ if (spec.positionals === 'rest') {
101
+ return { ok: true, data };
102
+ }
103
+ const valueFlagSet = new Set(valueFlags);
104
+ const booleanFlagSet = new Set(booleanFlags);
105
+ const optionalValueFlagSet = new Set(optionalValueFlags);
106
+ const flagList = [
107
+ ...valueFlags.map((f) => `--${f} <value>`),
108
+ ...booleanFlags.map((f) => `--${f}`),
109
+ ...optionalValueFlags.map((f) => `--${f} [value]`),
110
+ ];
111
+ let i = spec.positionals;
112
+ while (i < args.length) {
113
+ const tok = args[i];
114
+ if (isFlagToken(tok)) {
115
+ const name = tok.slice(2);
116
+ if (valueFlagSet.has(name)) {
117
+ // A value flag whose next token is absent or flag-shaped resolves to
118
+ // `null` in `data` (see the extraction loop above) — this is NOT an
119
+ // error (#3180/behavior-lock: emptyPrdValueIsFalsyAndTreatedAsAbsent,
120
+ // section-manifest-init-facts.test.cjs "flag-shaped value"). Advance
121
+ // by 1 so the following flag token is validated on its own merits on
122
+ // the next iteration.
123
+ const next = args[i + 1];
124
+ i += next !== undefined && !isFlagToken(next) ? 2 : 1;
125
+ continue;
126
+ }
127
+ if (booleanFlagSet.has(name)) {
128
+ i += 1;
129
+ continue;
130
+ }
131
+ if (optionalValueFlagSet.has(name)) {
132
+ const next = args[i + 1];
133
+ i += next !== undefined && !isFlagToken(next) ? 2 : 1;
134
+ continue;
135
+ }
136
+ const reason = flagList.length > 0
137
+ ? `unknown flag ${formatDiagnosticToken(tok)}; accepted: ${flagList.join(', ')}`
138
+ : `unknown flag ${formatDiagnosticToken(tok)}; this command accepts no flags`;
139
+ return { ok: false, kind: 'InvalidArgs', arg: tok, reason, exitReason: ERROR_REASON.USAGE };
140
+ }
141
+ return {
142
+ ok: false,
143
+ kind: 'InvalidArgs',
144
+ arg: tok,
145
+ reason: `unexpected positional argument ${formatDiagnosticToken(tok)}`,
146
+ exitReason: ERROR_REASON.USAGE,
147
+ };
148
+ }
149
+ return { ok: true, data };
150
+ }
151
+ /**
152
+ * Thin projection over `parseNamedArgs`: on `ok:false` it calls
153
+ * `fail(result.reason, result.exitReason)` and then throws.
154
+ *
155
+ * The trailing throw exists for two reasons: (1) TypeScript's control-flow
156
+ * analysis needs a `never`-returning path so callers can destructure the
157
+ * return value without a null check; (2) it is a fail-closed backstop — the
158
+ * `fail` callbacks in this repo are `never`-returning at runtime (`io.error`
159
+ * calls `process.exit(1)`) but are typed `void`, so if a caller ever passes a
160
+ * `fail` that actually returns, this still halts instead of falling through
161
+ * with a half-built `ParsedNamedArgs`.
162
+ */
163
+ function parseNamedArgsOrExit(args, spec, fail) {
164
+ const result = parseNamedArgs(args, spec);
165
+ if (!result.ok) {
166
+ fail(result.reason, result.exitReason);
167
+ throw new Error(`parseNamedArgsOrExit: fail() returned instead of exiting (arg: ${result.arg})`);
38
168
  }
39
- return result;
169
+ return result.data;
40
170
  }
41
171
  /**
42
172
  * Collect all tokens after --flag until the next --flag or end of args.