@opengsd/gsd-core 1.10.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 (544) 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 +12 -1
  5. package/agents/gsd-debugger.md +1 -1
  6. package/agents/gsd-doc-synthesizer.md +2 -4
  7. package/agents/gsd-dom-verifier.md +169 -0
  8. package/agents/gsd-eval-auditor.md +1 -1
  9. package/agents/gsd-executor.md +22 -14
  10. package/agents/gsd-framework-selector.md +1 -3
  11. package/agents/gsd-intel-updater.md +1 -1
  12. package/agents/gsd-mempalace-curator.md +5 -3
  13. package/agents/gsd-pattern-mapper.md +11 -0
  14. package/agents/gsd-phase-researcher.md +23 -2
  15. package/agents/gsd-plan-checker.md +50 -53
  16. package/agents/gsd-planner.md +50 -50
  17. package/agents/gsd-project-researcher.md +1 -1
  18. package/agents/gsd-research-synthesizer.md +2 -2
  19. package/agents/gsd-roadmapper.md +15 -11
  20. package/agents/gsd-ui-checker.md +63 -4
  21. package/agents/gsd-ui-researcher.md +41 -3
  22. package/agents/gsd-user-profiler.md +3 -0
  23. package/agents/gsd-verifier.md +13 -4
  24. package/bin/install.js +1448 -1103
  25. package/commands/gsd/code-review.md +1 -1
  26. package/commands/gsd/discuss-phase.md +1 -1
  27. package/commands/gsd/execute-phase.md +1 -1
  28. package/commands/gsd/import.md +1 -1
  29. package/commands/gsd/map-codebase.md +1 -1
  30. package/commands/gsd/mempalace-capture.md +1 -1
  31. package/commands/gsd/mempalace-recall.md +1 -1
  32. package/commands/gsd/new-milestone.md +1 -1
  33. package/commands/gsd/quick.md +9 -5
  34. package/commands/gsd/review-backlog.md +2 -1
  35. package/commands/gsd/verify-work.md +1 -1
  36. package/gsd-core/bin/gsd-tools.cjs +1035 -138
  37. package/gsd-core/bin/lib/active-workstream-store.cjs +146 -22
  38. package/gsd-core/bin/lib/adr-parser.cjs +13 -7
  39. package/gsd-core/bin/lib/agent-install-check.cjs +392 -32
  40. package/gsd-core/bin/lib/api-coverage.cjs +33 -14
  41. package/gsd-core/bin/lib/artifacts.cjs +5 -0
  42. package/gsd-core/bin/lib/assumption-delta.cjs +32 -15
  43. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  44. package/gsd-core/bin/lib/audit.cjs +1026 -268
  45. package/gsd-core/bin/lib/broken-windows.cjs +306 -28
  46. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  47. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  48. package/gsd-core/bin/lib/capability-lock.cjs +10 -4
  49. package/gsd-core/bin/lib/capability-registry.cjs +845 -130
  50. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  51. package/gsd-core/bin/lib/capability-state.cjs +18 -3
  52. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  53. package/gsd-core/bin/lib/capability-validator.cjs +700 -40
  54. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  55. package/gsd-core/bin/lib/check-command-router.cjs +216 -42
  56. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  57. package/gsd-core/bin/lib/cli-exit.cjs +496 -10
  58. package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
  59. package/gsd-core/bin/lib/codex-agent-toml.cjs +735 -0
  60. package/gsd-core/bin/lib/command-aliases.cjs +22 -0
  61. package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
  62. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  63. package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
  64. package/gsd-core/bin/lib/commands.cjs +1172 -108
  65. package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
  66. package/gsd-core/bin/lib/complexity-trigger.cjs +1192 -0
  67. package/gsd-core/bin/lib/config-loader.cjs +187 -23
  68. package/gsd-core/bin/lib/config.cjs +102 -3
  69. package/gsd-core/bin/lib/configuration.cjs +129 -37
  70. package/gsd-core/bin/lib/core-utils.cjs +208 -33
  71. package/gsd-core/bin/lib/decisions.cjs +23 -0
  72. package/gsd-core/bin/lib/edge-probe.cjs +9 -1
  73. package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
  74. package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
  75. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  76. package/gsd-core/bin/lib/frontmatter.cjs +899 -229
  77. package/gsd-core/bin/lib/gap-checker.cjs +95 -10
  78. package/gsd-core/bin/lib/git-base-branch.cjs +276 -39
  79. package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
  80. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  81. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  82. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +149 -0
  83. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  84. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  85. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  86. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +268 -0
  87. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  88. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  89. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +187 -0
  90. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  91. package/gsd-core/bin/lib/health-diagnostic.cjs +451 -0
  92. package/gsd-core/bin/lib/host-integration.cjs +39 -6
  93. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  94. package/gsd-core/bin/lib/init-command-router.cjs +118 -21
  95. package/gsd-core/bin/lib/init.cjs +439 -168
  96. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  97. package/gsd-core/bin/lib/install-engine.cjs +811 -259
  98. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  99. package/gsd-core/bin/lib/install-model-override-resolver.cjs +235 -0
  100. package/gsd-core/bin/lib/install-profiles.cjs +212 -61
  101. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  102. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  103. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  104. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  105. package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
  106. package/gsd-core/bin/lib/installer-migrations.cjs +148 -38
  107. package/gsd-core/bin/lib/intel.cjs +101 -26
  108. package/gsd-core/bin/lib/io.cjs +170 -15
  109. package/gsd-core/bin/lib/learnings.cjs +85 -14
  110. package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
  111. package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
  112. package/gsd-core/bin/lib/markdown-table.cjs +183 -22
  113. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  114. package/gsd-core/bin/lib/milestone.cjs +842 -73
  115. package/gsd-core/bin/lib/model-catalog.cjs +232 -16
  116. package/gsd-core/bin/lib/model-resolver.cjs +193 -68
  117. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  118. package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
  119. package/gsd-core/bin/lib/pattern.cjs +122 -0
  120. package/gsd-core/bin/lib/phase-estimation.cjs +18 -9
  121. package/gsd-core/bin/lib/phase-id.cjs +514 -40
  122. package/gsd-core/bin/lib/phase-lifecycle.cjs +52 -19
  123. package/gsd-core/bin/lib/phase-locator.cjs +262 -34
  124. package/gsd-core/bin/lib/phase.cjs +1038 -214
  125. package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
  126. package/gsd-core/bin/lib/plan-document.cjs +263 -0
  127. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  128. package/gsd-core/bin/lib/plan-scan.cjs +98 -3
  129. package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
  130. package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
  131. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  132. package/gsd-core/bin/lib/planning-snapshot.cjs +894 -0
  133. package/gsd-core/bin/lib/planning-workspace.cjs +112 -6
  134. package/gsd-core/bin/lib/probe-core.cjs +5 -2
  135. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  136. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
  137. package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
  138. package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
  139. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +766 -0
  140. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
  141. package/gsd-core/bin/lib/review-lane-descriptor.cjs +22 -13
  142. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  143. package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
  144. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  145. package/gsd-core/bin/lib/roadmap-command-router.cjs +59 -11
  146. package/gsd-core/bin/lib/roadmap-parser.cjs +1006 -184
  147. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  148. package/gsd-core/bin/lib/roadmap.cjs +442 -96
  149. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +702 -52
  150. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  151. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +459 -55
  152. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  153. package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
  154. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +402 -58
  155. package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
  156. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  157. package/gsd-core/bin/lib/runtime-slash.cjs +96 -8
  158. package/gsd-core/bin/lib/security.cjs +104 -5
  159. package/gsd-core/bin/lib/shell-command-projection.cjs +342 -7
  160. package/gsd-core/bin/lib/smart-entry.cjs +133 -23
  161. package/gsd-core/bin/lib/spec-section.cjs +12 -7
  162. package/gsd-core/bin/lib/state-command-router.cjs +52 -19
  163. package/gsd-core/bin/lib/state-contract.cjs +359 -0
  164. package/gsd-core/bin/lib/state-document.cjs +338 -8
  165. package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
  166. package/gsd-core/bin/lib/state-transition.cjs +846 -176
  167. package/gsd-core/bin/lib/state.cjs +2589 -369
  168. package/gsd-core/bin/lib/surface.cjs +33 -11
  169. package/gsd-core/bin/lib/task-command-router.cjs +111 -1
  170. package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
  171. package/gsd-core/bin/lib/teams-status.cjs +4 -1
  172. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  173. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  174. package/gsd-core/bin/lib/uat-predicate.cjs +67 -23
  175. package/gsd-core/bin/lib/uat.cjs +1761 -167
  176. package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
  177. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  178. package/gsd-core/bin/lib/ui-safety-gate.cjs +51 -12
  179. package/gsd-core/bin/lib/unusable-input.cjs +37 -0
  180. package/gsd-core/bin/lib/update-context.cjs +8 -2
  181. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  182. package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
  183. package/gsd-core/bin/lib/validate.cjs +20 -6
  184. package/gsd-core/bin/lib/vendor/README.md +75 -0
  185. package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
  186. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  187. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  188. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  189. package/gsd-core/bin/lib/verification.cjs +272 -9
  190. package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
  191. package/gsd-core/bin/lib/verify.cjs +453 -918
  192. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
  193. package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
  194. package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
  195. package/gsd-core/bin/lib/workstream.cjs +2 -2
  196. package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
  197. package/gsd-core/bin/lib/worktree-safety.cjs +341 -18
  198. package/gsd-core/bin/shared/config-defaults.manifest.json +8 -1
  199. package/gsd-core/bin/shared/config-schema.manifest.json +12 -1
  200. package/gsd-core/bin/shared/exit-codes.json +8 -0
  201. package/gsd-core/bin/shared/exit-codes.sh +20 -0
  202. package/gsd-core/bin/shared/model-catalog.json +8 -1
  203. package/gsd-core/references/agent-contracts.md +44 -26
  204. package/gsd-core/references/api-coverage.md +24 -2
  205. package/gsd-core/references/autonomous-smart-discuss.md +3 -3
  206. package/gsd-core/references/checkpoints.md +39 -21
  207. package/gsd-core/references/context-budget.md +1 -1
  208. package/gsd-core/references/decimal-phase-calculation.md +5 -5
  209. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  210. package/gsd-core/references/doc-conflict-engine.md +1 -1
  211. package/gsd-core/references/edge-probe.md +8 -0
  212. package/gsd-core/references/execute-mvp-tdd.md +4 -6
  213. package/gsd-core/references/execute-phase-between-wave-reset.md +15 -14
  214. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  215. package/gsd-core/references/execute-phase-response-language.md +1 -1
  216. package/gsd-core/references/execute-phase-wave-guard.md +17 -11
  217. package/gsd-core/references/failing-direction.md +78 -0
  218. package/gsd-core/references/gate-prompts.md +1 -1
  219. package/gsd-core/references/git-integration.md +5 -5
  220. package/gsd-core/references/git-planning-commit.md +5 -4
  221. package/gsd-core/references/gsd-run-resolver.md +1 -1
  222. package/gsd-core/references/loop-hook-dispatch.md +61 -2
  223. package/gsd-core/references/model-profiles.md +12 -4
  224. package/gsd-core/references/mvp-concepts.md +9 -9
  225. package/gsd-core/references/nyquist-compliance.md +74 -0
  226. package/gsd-core/references/offer-next.md +3 -5
  227. package/gsd-core/references/phase-argument-parsing.md +3 -3
  228. package/gsd-core/references/planner-failing-direction.md +53 -0
  229. package/gsd-core/references/planner-guidance.md +3 -9
  230. package/gsd-core/references/planner-human-verify-mode.md +15 -1
  231. package/gsd-core/references/planner-preconditions.md +1 -1
  232. package/gsd-core/references/planner-reviews.md +1 -1
  233. package/gsd-core/references/planner-revision.md +1 -1
  234. package/gsd-core/references/planner-verify-command-grounding.md +17 -0
  235. package/gsd-core/references/planning-config.md +44 -13
  236. package/gsd-core/references/reviewer-instances.md +31 -0
  237. package/gsd-core/references/revision-loop.md +1 -1
  238. package/gsd-core/references/runtime-aware-dispatch.md +1 -1
  239. package/gsd-core/references/specless-probe-fallback.md +1 -1
  240. package/gsd-core/references/tdd.md +1 -3
  241. package/gsd-core/references/ui-brand.md +65 -21
  242. package/gsd-core/references/ui-consideration-probe.md +1 -1
  243. package/gsd-core/references/universal-anti-patterns.md +5 -5
  244. package/gsd-core/references/verifier-phase-gates.md +192 -0
  245. package/gsd-core/references/verify-command-path-resolvability.md +42 -0
  246. package/gsd-core/references/verify-mvp-mode.md +2 -2
  247. package/gsd-core/references/workstream-flag.md +33 -17
  248. package/gsd-core/templates/README.md +1 -1
  249. package/gsd-core/templates/SECURITY.md +3 -3
  250. package/gsd-core/templates/UI-SPEC.md +25 -3
  251. package/gsd-core/templates/VALIDATION.md +3 -3
  252. package/gsd-core/templates/discussion-log.md +1 -1
  253. package/gsd-core/templates/phase-prompt.md +5 -4
  254. package/gsd-core/templates/state.md +11 -4
  255. package/gsd-core/templates/verification-report.md +9 -1
  256. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  257. package/gsd-core/workflows/add-backlog.md +1 -1
  258. package/gsd-core/workflows/add-phase.md +3 -3
  259. package/gsd-core/workflows/add-tests.md +3 -8
  260. package/gsd-core/workflows/add-todo.md +1 -1
  261. package/gsd-core/workflows/ai-integration-phase.md +13 -20
  262. package/gsd-core/workflows/audit-fix.md +12 -3
  263. package/gsd-core/workflows/audit-milestone.md +9 -9
  264. package/gsd-core/workflows/audit-uat.md +17 -2
  265. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
  266. package/gsd-core/workflows/autonomous.md +11 -27
  267. package/gsd-core/workflows/check-todos.md +1 -1
  268. package/gsd-core/workflows/cleanup.md +64 -5
  269. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +14 -4
  270. package/gsd-core/workflows/code-review-fix.md +38 -11
  271. package/gsd-core/workflows/code-review.md +159 -52
  272. package/gsd-core/workflows/complete-milestone.md +151 -23
  273. package/gsd-core/workflows/debug.md +12 -8
  274. package/gsd-core/workflows/diagnose-issues.md +47 -15
  275. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  276. package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -8
  277. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  278. package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
  279. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
  280. package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
  281. package/gsd-core/workflows/discuss-phase.md +1 -1
  282. package/gsd-core/workflows/do.md +3 -6
  283. package/gsd-core/workflows/docs-update.md +5 -4
  284. package/gsd-core/workflows/edit-phase.md +27 -2
  285. package/gsd-core/workflows/eval-review.md +7 -14
  286. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  287. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +142 -15
  288. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  289. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  290. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  291. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +24 -4
  292. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
  293. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
  294. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -2
  295. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
  296. package/gsd-core/workflows/execute-phase.md +72 -100
  297. package/gsd-core/workflows/execute-plan.md +52 -15
  298. package/gsd-core/workflows/explore.md +131 -4
  299. package/gsd-core/workflows/extract-learnings.md +1 -1
  300. package/gsd-core/workflows/fast.md +10 -2
  301. package/gsd-core/workflows/forensics.md +1 -1
  302. package/gsd-core/workflows/graduation.md +5 -5
  303. package/gsd-core/workflows/health.md +76 -10
  304. package/gsd-core/workflows/import.md +18 -15
  305. package/gsd-core/workflows/inbox.md +4 -5
  306. package/gsd-core/workflows/ingest-docs.md +49 -16
  307. package/gsd-core/workflows/insert-phase.md +5 -5
  308. package/gsd-core/workflows/list-seeds.md +5 -3
  309. package/gsd-core/workflows/list-workspaces.md +1 -1
  310. package/gsd-core/workflows/manager.md +12 -23
  311. package/gsd-core/workflows/map-codebase.md +1 -1
  312. package/gsd-core/workflows/milestone-summary.md +1 -1
  313. package/gsd-core/workflows/mvp-phase.md +8 -5
  314. package/gsd-core/workflows/new-milestone.md +22 -29
  315. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  316. package/gsd-core/workflows/new-project.md +26 -40
  317. package/gsd-core/workflows/new-workspace.md +1 -1
  318. package/gsd-core/workflows/next.md +14 -2
  319. package/gsd-core/workflows/pause-work.md +1 -1
  320. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
  321. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
  322. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
  323. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
  324. package/gsd-core/workflows/plan-phase.md +162 -59
  325. package/gsd-core/workflows/plan-review-convergence.md +96 -11
  326. package/gsd-core/workflows/plant-seed.md +2 -2
  327. package/gsd-core/workflows/pr-branch.md +187 -51
  328. package/gsd-core/workflows/profile-user.md +16 -14
  329. package/gsd-core/workflows/progress.md +61 -18
  330. package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
  331. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +5 -7
  332. package/gsd-core/workflows/quick/steps/quick-verification.md +28 -9
  333. package/gsd-core/workflows/quick/steps/research-phase.md +4 -6
  334. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
  335. package/gsd-core/workflows/quick.md +55 -44
  336. package/gsd-core/workflows/remove-phase.md +4 -4
  337. package/gsd-core/workflows/remove-workspace.md +2 -2
  338. package/gsd-core/workflows/resume-project.md +8 -12
  339. package/gsd-core/workflows/review.md +219 -20
  340. package/gsd-core/workflows/scan.md +1 -1
  341. package/gsd-core/workflows/secure-phase.md +3 -3
  342. package/gsd-core/workflows/session-report.md +2 -1
  343. package/gsd-core/workflows/settings-advanced.md +7 -9
  344. package/gsd-core/workflows/settings-integrations.md +64 -31
  345. package/gsd-core/workflows/settings.md +69 -7
  346. package/gsd-core/workflows/ship.md +116 -50
  347. package/gsd-core/workflows/sketch-wrap-up.md +11 -17
  348. package/gsd-core/workflows/sketch.md +12 -18
  349. package/gsd-core/workflows/smart-entry.md +3 -5
  350. package/gsd-core/workflows/spec-phase.md +53 -13
  351. package/gsd-core/workflows/spike-wrap-up.md +7 -11
  352. package/gsd-core/workflows/spike.md +20 -31
  353. package/gsd-core/workflows/stats.md +2 -2
  354. package/gsd-core/workflows/sync-skills.md +64 -9
  355. package/gsd-core/workflows/thread.md +11 -7
  356. package/gsd-core/workflows/transition.md +49 -14
  357. package/gsd-core/workflows/ui-phase.md +15 -21
  358. package/gsd-core/workflows/ui-review.md +8 -12
  359. package/gsd-core/workflows/ultraplan-phase.md +5 -13
  360. package/gsd-core/workflows/undo.md +8 -16
  361. package/gsd-core/workflows/update.md +7 -11
  362. package/gsd-core/workflows/validate-phase.md +3 -3
  363. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
  364. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  365. package/gsd-core/workflows/verify-work.md +66 -25
  366. package/hooks/dist/gsd-agent-isolation-guard.js +158 -30
  367. package/hooks/dist/gsd-check-update-worker.js +56 -13
  368. package/hooks/dist/gsd-check-update.js +19 -1
  369. package/hooks/dist/gsd-config-reload.js +18 -12
  370. package/hooks/dist/gsd-context-monitor.js +19 -10
  371. package/hooks/dist/gsd-cursor-post-tool.js +3 -1
  372. package/hooks/dist/gsd-cursor-pre-tool.js +2 -3
  373. package/hooks/dist/gsd-cursor-session-start.js +2 -1
  374. package/hooks/dist/gsd-cursor-stop.js +2 -1
  375. package/hooks/dist/gsd-cursor-subagent-start.js +83 -3
  376. package/hooks/dist/gsd-cursor-subagent-stop.js +6 -3
  377. package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
  378. package/hooks/dist/gsd-graphify-update.sh +22 -18
  379. package/hooks/dist/gsd-node-runner.sh +76 -0
  380. package/hooks/dist/gsd-phase-boundary.sh +1 -0
  381. package/hooks/dist/gsd-prompt-guard.js +37 -27
  382. package/hooks/dist/gsd-read-guard.js +16 -7
  383. package/hooks/dist/gsd-read-injection-scanner.js +55 -32
  384. package/hooks/dist/gsd-session-state.sh +1 -0
  385. package/hooks/dist/gsd-statusline.js +231 -24
  386. package/hooks/dist/gsd-update-banner.js +22 -1
  387. package/hooks/dist/gsd-validate-commit.sh +80 -6
  388. package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
  389. package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
  390. package/hooks/dist/gsd-workflow-guard.js +162 -46
  391. package/hooks/dist/gsd-worktree-path-guard.js +36 -21
  392. package/hooks/dist/gsd-write-guard.js +35 -25
  393. package/hooks/dist/lib/cli-exit.js +560 -0
  394. package/hooks/dist/lib/exit-code-registry.js +98 -0
  395. package/hooks/dist/lib/git-cmd.js +92 -59
  396. package/hooks/dist/lib/git-probe.js +84 -0
  397. package/hooks/dist/lib/hook-exit.js +81 -0
  398. package/hooks/dist/lib/injection-patterns.js +45 -0
  399. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  400. package/hooks/dist/lib/isolation-sentinel.js +9 -0
  401. package/hooks/dist/managed-hooks-registry.cjs +3 -0
  402. package/hooks/gsd-agent-isolation-guard.js +158 -30
  403. package/hooks/gsd-check-update-worker.js +56 -13
  404. package/hooks/gsd-check-update.js +19 -1
  405. package/hooks/gsd-config-reload.js +18 -12
  406. package/hooks/gsd-context-monitor.js +19 -10
  407. package/hooks/gsd-cursor-post-tool.js +3 -1
  408. package/hooks/gsd-cursor-pre-tool.js +2 -3
  409. package/hooks/gsd-cursor-session-start.js +2 -1
  410. package/hooks/gsd-cursor-stop.js +2 -1
  411. package/hooks/gsd-cursor-subagent-start.js +83 -3
  412. package/hooks/gsd-cursor-subagent-stop.js +6 -3
  413. package/hooks/gsd-ensure-canonical-path.js +2 -1
  414. package/hooks/gsd-graphify-update.sh +22 -18
  415. package/hooks/gsd-node-runner.sh +76 -0
  416. package/hooks/gsd-phase-boundary.sh +1 -0
  417. package/hooks/gsd-prompt-guard.js +37 -27
  418. package/hooks/gsd-read-guard.js +16 -7
  419. package/hooks/gsd-read-injection-scanner.js +55 -32
  420. package/hooks/gsd-session-state.sh +1 -0
  421. package/hooks/gsd-statusline.js +231 -24
  422. package/hooks/gsd-update-banner.js +22 -1
  423. package/hooks/gsd-validate-commit.sh +80 -6
  424. package/hooks/gsd-windsurf-pre-command.js +16 -11
  425. package/hooks/gsd-windsurf-pre-write.js +22 -13
  426. package/hooks/gsd-workflow-guard.js +162 -46
  427. package/hooks/gsd-worktree-path-guard.js +36 -21
  428. package/hooks/gsd-write-guard.js +35 -25
  429. package/hooks/lib/cli-exit.js +560 -0
  430. package/hooks/lib/exit-code-registry.js +98 -0
  431. package/hooks/lib/git-cmd.js +92 -59
  432. package/hooks/lib/git-probe.js +84 -0
  433. package/hooks/lib/hook-exit.js +81 -0
  434. package/hooks/lib/injection-patterns.js +45 -0
  435. package/hooks/lib/isolation-deny-reason.js +39 -0
  436. package/hooks/lib/isolation-sentinel.js +9 -0
  437. package/hooks/managed-hooks-registry.cjs +3 -0
  438. package/package.json +28 -11
  439. package/pi/gsd.cjs +19 -5
  440. package/scripts/base64-scan.sh +74 -12
  441. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  442. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  443. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  444. package/scripts/build-hooks.js +5 -0
  445. package/scripts/changeset/lint.cjs +60 -5
  446. package/scripts/check-alias-drift.cjs +7 -43
  447. package/scripts/check-contract-drift.cjs +297 -0
  448. package/scripts/check-glossary-refs.cjs +77 -15
  449. package/scripts/check-mutation-score-ratchet.cjs +156 -0
  450. package/scripts/ci-check-job-near-cap.cjs +49 -0
  451. package/scripts/ci-pr-mergeability.cjs +262 -0
  452. package/scripts/ci-test-scope.cjs +64 -14
  453. package/scripts/ci-timeout-report.cjs +230 -0
  454. package/scripts/command-contract-helpers.cjs +903 -1
  455. package/scripts/docs-guard-registry.cjs +396 -0
  456. package/scripts/gen-adr-index.cjs +728 -38
  457. package/scripts/gen-capability-registry.cjs +11 -21
  458. package/scripts/gen-context-index.cjs +2 -11
  459. package/scripts/gen-exit-code-docs.cjs +318 -0
  460. package/scripts/gen-exit-code-registry.cjs +891 -0
  461. package/scripts/gen-features.cjs +836 -0
  462. package/scripts/gen-health-docs.cjs +390 -0
  463. package/scripts/gen-hooks-cli-exit.cjs +239 -0
  464. package/scripts/gen-install-tree-fixtures.cjs +2 -2
  465. package/scripts/gen-inventory-manifest.cjs +50 -4
  466. package/scripts/gen-loop-host-contract.cjs +138 -25
  467. package/scripts/gen-registry.cjs +3 -14
  468. package/scripts/gen-scripts-cli-exit.cjs +185 -0
  469. package/scripts/gen-state-md-docs.cjs +727 -0
  470. package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
  471. package/scripts/lib/alias-drift-families.cjs +46 -0
  472. package/scripts/lib/ci-job-timing.cjs +72 -0
  473. package/scripts/lib/cli-exit.cjs +546 -44
  474. package/scripts/lib/drift-scan.cjs +308 -0
  475. package/scripts/lib/exit-code-registry.cjs +98 -0
  476. package/scripts/lib/ndjson-reporter.cjs +119 -0
  477. package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
  478. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  479. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  480. package/scripts/lint-canary-version-leak.cjs +73 -0
  481. package/scripts/lint-command-contract.cjs +96 -13
  482. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  483. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  484. package/scripts/lint-default-flip-documentation.cjs +193 -0
  485. package/scripts/lint-docs-guard-registration.cjs +495 -0
  486. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +193 -0
  487. package/scripts/lint-eslint-glob-coverage.allowlist.json +38 -0
  488. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  489. package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
  490. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  491. package/scripts/lint-health-diagnostic-rule-table.cjs +461 -0
  492. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  493. package/scripts/lint-milestone-window-drift.cjs +468 -0
  494. package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
  495. package/scripts/lint-phase-enumeration-drift.cjs +492 -0
  496. package/scripts/lint-plan-count-drift.cjs +318 -0
  497. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  498. package/scripts/lint-planning-prompt-drift.cjs +471 -0
  499. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  500. package/scripts/lint-regression-test-names.cjs +15 -13
  501. package/scripts/lint-removed-but-needed.cjs +488 -0
  502. package/scripts/lint-seam-enforcement.cjs +182 -0
  503. package/scripts/lint-slug-derivation-drift.cjs +921 -0
  504. package/scripts/lint-source-test-name-collision.cjs +241 -0
  505. package/scripts/lint-state-field-drift.cjs +805 -0
  506. package/scripts/lint-state-write-path-drift.cjs +950 -0
  507. package/scripts/lint-test-file-count.allowlist.json +137 -8
  508. package/scripts/lint-test-file-count.cjs +25 -3
  509. package/scripts/lint-unreachable-guard-drift.cjs +830 -0
  510. package/scripts/lint-vendored-deps.cjs +297 -0
  511. package/scripts/mutation-matrix.cjs +599 -50
  512. package/scripts/pr-changed-files.cjs +63 -0
  513. package/scripts/pr-template-policy.cjs +14 -4
  514. package/scripts/prompt-injection-scan.sh +100 -14
  515. package/scripts/require-issue-link-policy.cjs +192 -0
  516. package/scripts/secret-scan.sh +75 -13
  517. package/scripts/select-docs-guards.cjs +56 -0
  518. package/scripts/sync-runtime-launcher.cjs +24 -7
  519. package/skills/gsd-autonomous/SKILL.md +0 -1
  520. package/skills/gsd-code-review/SKILL.md +1 -1
  521. package/skills/gsd-discuss-phase/SKILL.md +1 -1
  522. package/skills/gsd-execute-phase/SKILL.md +1 -2
  523. package/skills/gsd-import/SKILL.md +1 -1
  524. package/skills/gsd-map-codebase/SKILL.md +1 -1
  525. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  526. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  527. package/skills/gsd-new-milestone/SKILL.md +1 -1
  528. package/skills/gsd-next/SKILL.md +0 -1
  529. package/skills/gsd-plan-phase/SKILL.md +0 -1
  530. package/skills/gsd-progress/SKILL.md +0 -1
  531. package/skills/gsd-quick/SKILL.md +9 -5
  532. package/skills/gsd-review-backlog/SKILL.md +2 -1
  533. package/skills/gsd-stats/SKILL.md +0 -1
  534. package/skills/gsd-verify-work/SKILL.md +1 -1
  535. package/vscode/package.json +1 -1
  536. package/bin/lib/ui-safety-gate.cjs +0 -107
  537. package/gsd-core/workflows/discovery-phase.md +0 -298
  538. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  539. package/gsd-core/workflows/verify-phase.md +0 -574
  540. package/scripts/affected-tests-lib.cjs +0 -554
  541. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  542. package/scripts/lint-emitted-drift-ack.cjs +0 -344
  543. package/scripts/run-affected-tests.cjs +0 -7
  544. package/scripts/run-tests.cjs +0 -1051
@@ -0,0 +1,735 @@
1
+ "use strict";
2
+ /**
3
+ * Codex Agent TOML — typed IR for `~/.codex/agents/<agent>.toml` (#3243, ADR-2313).
4
+ *
5
+ * A genuine leaf: node builtins only. This is a **document model**, not a policy —
6
+ * it knows how to parse/render/strip two known keys (`model`,
7
+ * `model_reasoning_effort`) from a Codex agent `.toml`. It does NOT know which
8
+ * `model` values are illegal for Codex (that predicate — Anthropic-flavored
9
+ * detection — stays in `model-catalog.cts`; callers decide what to strip).
10
+ *
11
+ * Moved here (not copied) from `agent-install-check.cts` (#3242, Phase 2), which
12
+ * wrote the hard half: block-range detection, BOM stripping, TOML value
13
+ * unquoting, and the lenient header scan. That module's behavior is UNCHANGED —
14
+ * it imports `stripBOM`/`scanTomlLines` from here and its regression suite
15
+ * (`tests/agent-install-check.test.cjs`) is the proof.
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
+ *
28
+ * ── The reconciliation (40-design.md) ──────────────────────────────────────
29
+ *
30
+ * Phase 2's reader and this phase's writer disagree on how to handle an
31
+ * unterminated `developer_instructions` block, deliberately:
32
+ *
33
+ * - The READER (`scanTomlLines`, used directly by `checkCodexModelPosture`)
34
+ * stays LENIENT: an unterminated block still excludes "the rest of the
35
+ * file" from the header scan (findDeveloperInstructionsBlockRange's
36
+ * existing fallback), because misreading prompt prose as a pin is only a
37
+ * false positive — it wastes a user's time, nothing more.
38
+ * - The WRITER (`parseCodexAgentToml`, used by the Codex sync) is STRICT: an
39
+ * unterminated block makes the whole document `{ok:false}`, because a
40
+ * writer that proceeds on a malformed document risks rewriting it.
41
+ *
42
+ * One block-range detector, two call sites, two policies — never two detectors
43
+ * that could silently drift from each other.
44
+ */
45
+ var __importDefault = (this && this.__importDefault) || function (mod) {
46
+ return (mod && mod.__esModule) ? mod : { "default": mod };
47
+ };
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.CODEX_SANDBOX_HOLDS = exports.PARSE_REASON = void 0;
50
+ exports.stripBOM = stripBOM;
51
+ exports.unquoteTomlValue = unquoteTomlValue;
52
+ exports.findDeveloperInstructionsBlockRange = findDeveloperInstructionsBlockRange;
53
+ exports.scanTomlLines = scanTomlLines;
54
+ exports.parseCodexAgentToml = parseCodexAgentToml;
55
+ exports.renderCodexAgentToml = renderCodexAgentToml;
56
+ exports.stripModel = stripModel;
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"));
65
+ /** Frozen reason enum for a failed {@link parseCodexAgentToml}. */
66
+ exports.PARSE_REASON = Object.freeze({
67
+ UNTERMINATED_BLOCK: 'unterminated_block',
68
+ });
69
+ // The UTF-8 BOM codepoint, spelled as an escape rather than the literal
70
+ // character so the source file never carries an invisible codepoint.
71
+ const BOM_CHAR = String.fromCharCode(0xfeff);
72
+ // Strips a leading UTF-8 BOM (U+FEFF), which fs.readFileSync(..., 'utf8') does not
73
+ // strip on its own, and unwraps a TOML basic/literal string value's surrounding
74
+ // quotes so `model = "sonnet"` yields `sonnet`, not `"sonnet"`.
75
+ function stripBOM(content) {
76
+ return content.charCodeAt(0) === 0xfeff ? content.slice(1) : content;
77
+ }
78
+ function unquoteTomlValue(rawValue) {
79
+ const trimmed = rawValue.trim();
80
+ const quoted = trimmed.match(/^"([^"]*)"/) ?? trimmed.match(/^'([^']*)'/);
81
+ return quoted ? quoted[1] : trimmed;
82
+ }
83
+ // The `developer_instructions` block is a TOML multi-line literal string
84
+ // (`developer_instructions = '''...'''`) that `generateCodexAgentToml` always
85
+ // emits after the header fields. Prompt prose inside that block discusses models
86
+ // constantly, so a `model = ...`-shaped line inside it must never be read as a
87
+ // live pin — but the block can legally appear anywhere in the file (a
88
+ // hand-reordered agent can move `model` after it), and another key's *value* can
89
+ // legally contain the literal text `developer_instructions = '''` (e.g. a
90
+ // `description` field quoting it) without that being the real block opener. So
91
+ // instead of truncating the file at the first textual occurrence of the marker
92
+ // anywhere in the content, this locates the block by its anchored line-start
93
+ // opener (`^\s*developer_instructions\s*=\s*'''`, never a mid-line/mid-value
94
+ // match) and its closing `'''` line, and excludes only the lines between them —
95
+ // every other line in the file, before AND after the block, is scanned.
96
+ //
97
+ // If no opener is found, nothing is excluded (the whole file is scanned) and
98
+ // `terminated` is trivially true. If the block IS opened but never closed before
99
+ // EOF (malformed file), `terminated` is false: the lenient reader (scanTomlLines)
100
+ // still treats the rest of the file as inside the block (the safe direction for
101
+ // a reader — see module header comment); the strict writer (parseCodexAgentToml)
102
+ // reads `terminated` and refuses instead. The emitter always uses `'''` (a TOML
103
+ // literal string), never a `"""` basic multi-line string, so only `'''` is
104
+ // treated as the block delimiter here.
105
+ function findDeveloperInstructionsBlockRange(lines) {
106
+ const openIndex = lines.findIndex((line) => /^\s*developer_instructions\s*=\s*'''/.test(line));
107
+ if (openIndex === -1) {
108
+ return { start: -1, end: -1, terminated: true };
109
+ }
110
+ const afterOpenMarker = lines[openIndex].replace(/^\s*developer_instructions\s*=\s*'''/, '');
111
+ if (afterOpenMarker.includes("'''")) {
112
+ // Same-line block: developer_instructions = '''one line'''
113
+ return { start: openIndex, end: openIndex, terminated: true };
114
+ }
115
+ for (let i = openIndex + 1; i < lines.length; i++) {
116
+ if (lines[i].includes("'''")) {
117
+ return { start: openIndex, end: i, terminated: true };
118
+ }
119
+ }
120
+ return { start: openIndex, end: lines.length - 1, terminated: false };
121
+ }
122
+ // Line-oriented scan of every line OUTSIDE the `developer_instructions` block
123
+ // (see findDeveloperInstructionsBlockRange). Full-key-name anchoring —
124
+ // `^([A-Za-z_][\w]*)\s*=` for a bare key, or `^"([^"]*)"\s*=` / `^'([^']*)'\s*=`
125
+ // for TOML's legal quoted-key forms, normalized to the same key name — means
126
+ // `model_verbosity` / `model_reasoning_effort` never satisfy a `model` probe,
127
+ // and vice versa; `#`-prefixed lines (after trimming leading whitespace) are
128
+ // treated as comments, never live pins. Shared by both scanTomlLines (the
129
+ // lenient reader, boolean-only for reasoning effort) and parseCodexAgentToml
130
+ // (the strict writer, which also needs the effort's value and both keys' line
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.
135
+ function scanHeaderLines(lines, blockStart, blockEnd) {
136
+ let model = null;
137
+ let modelLineIndex = null;
138
+ let reasoningEffort = null;
139
+ let reasoningEffortLineIndex = null;
140
+ let sandboxMode = null;
141
+ for (let i = 0; i < lines.length; i++) {
142
+ if (blockStart !== -1 && i >= blockStart && i <= blockEnd)
143
+ continue;
144
+ const trimmed = lines[i].trim();
145
+ if (trimmed === '' || trimmed.startsWith('#'))
146
+ continue;
147
+ const match = trimmed.match(/^(?:"([^"]*)"|'([^']*)'|([A-Za-z_][\w]*))\s*=\s*(.*)$/);
148
+ if (!match)
149
+ continue;
150
+ const key = match[1] ?? match[2] ?? match[3];
151
+ const rawValue = match[4];
152
+ if (key === 'model') {
153
+ model = unquoteTomlValue(rawValue);
154
+ modelLineIndex = i;
155
+ }
156
+ else if (key === 'model_reasoning_effort') {
157
+ reasoningEffort = unquoteTomlValue(rawValue);
158
+ reasoningEffortLineIndex = i;
159
+ }
160
+ else if (key === 'sandbox_mode') {
161
+ sandboxMode = unquoteTomlValue(rawValue);
162
+ }
163
+ }
164
+ return { model, modelLineIndex, reasoningEffort, reasoningEffortLineIndex, sandboxMode };
165
+ }
166
+ /**
167
+ * The LENIENT reader entry point (Phase 2, moved verbatim in behavior). Never
168
+ * fails: an unterminated block falls back to "rest of file is inside the
169
+ * block" via {@link findDeveloperInstructionsBlockRange}'s own fallback.
170
+ * `content` is expected already BOM-stripped (callers pass `stripBOM(raw)`).
171
+ */
172
+ function scanTomlLines(content) {
173
+ const lines = content.split(/\r?\n/);
174
+ const { start, end } = findDeveloperInstructionsBlockRange(lines);
175
+ const { model, reasoningEffort, sandboxMode } = scanHeaderLines(lines, start, end);
176
+ return { model, hasReasoningEffort: reasoningEffort !== null, sandboxMode };
177
+ }
178
+ // Splits `content` into `{lines, terminators}` where `terminators[i]` is the
179
+ // terminator that FOLLOWS `lines[i]` (`'\r\n'`, `'\r'`, `'\n'`, or `''` for a
180
+ // line with none — only possible as the file's last line). The two arrays are
181
+ // always the same length and there is NEVER a phantom trailing entry: a
182
+ // source ending in a terminator (the common case) yields exactly as many
183
+ // lines as it has content lines, not one more. `render` is then a plain
184
+ // `lines[i] + terminators[i]` concatenation with no special-casing of "the
185
+ // last line" — see `renderCodexAgentToml`.
186
+ //
187
+ // `String#split` with a capturing group interleaves the delimiters into the
188
+ // result array — `"a\r\nb\nc".split(/(\r\n|\r|\n)/)` yields
189
+ // `["a","\r\n","b","\n","c"]` — so even indices are line content and odd
190
+ // indices are that line's terminator. When `content` ends WITH a terminator,
191
+ // `split` appends one extra empty-string element after the last real
192
+ // terminator (e.g. `"a\n".split(...)` → `["a","\n",""]`); that trailing `""`
193
+ // is not a real line, it is `split`'s "nothing after the last delimiter"
194
+ // marker, so the loop below stops before consuming it instead of recording it
195
+ // as a phantom empty final line (the defect this replaced — see A29: a doc
196
+ // with a phantom last element made every removal rule reason about the wrong
197
+ // element for any trailing-newline-terminated file, the common case). `\r\n`
198
+ // is tried before the bare `\r` alternative so a CRLF is never misread as a
199
+ // lone-CR line followed by an empty LF-terminated line.
200
+ function splitPreservingTerminators(content) {
201
+ if (content === '')
202
+ return { lines: [], terminators: [] };
203
+ const parts = content.split(/(\r\n|\r|\n)/);
204
+ const lastIndex = parts.length - 1;
205
+ const lines = [];
206
+ const terminators = [];
207
+ for (let i = 0; i < parts.length; i += 2) {
208
+ if (i === lastIndex && parts[i] === '')
209
+ break; // split's post-terminator marker, not a real line
210
+ lines.push(parts[i]);
211
+ terminators.push(parts[i + 1] ?? '');
212
+ }
213
+ return { lines, terminators };
214
+ }
215
+ /**
216
+ * The STRICT parse entry point (Phase 3, the writer's half of the
217
+ * reconciliation). Returns `{ok:false, reason:UNTERMINATED_BLOCK}` rather than
218
+ * guessing when the `developer_instructions` block is opened but never closed.
219
+ * On success, `doc` carries enough (the original `lines`/`terminators`, BOM/
220
+ * trailing-newline flags, and the two resolved values with their line indices)
221
+ * for {@link renderCodexAgentToml} to reproduce the source byte-identically —
222
+ * including a source with mixed line-ending styles — and for
223
+ * {@link stripModel}/{@link stripReasoningEffort} to remove exactly one line
224
+ * and its own terminator.
225
+ */
226
+ function parseCodexAgentToml(content) {
227
+ const hadBOM = content.charCodeAt(0) === 0xfeff;
228
+ const stripped = stripBOM(content);
229
+ // Informational only — see CodexAgentDoc.eol's docstring. Never used by
230
+ // renderCodexAgentToml.
231
+ const eol = stripped.includes('\r\n') ? '\r\n' : '\n';
232
+ const trailingNewline = /(\r\n|\r|\n)$/.test(stripped);
233
+ const { lines, terminators } = splitPreservingTerminators(stripped);
234
+ const { start, end, terminated } = findDeveloperInstructionsBlockRange(lines);
235
+ if (start !== -1 && !terminated) {
236
+ return { ok: false, reason: exports.PARSE_REASON.UNTERMINATED_BLOCK };
237
+ }
238
+ const { model, modelLineIndex, reasoningEffort, reasoningEffortLineIndex } = scanHeaderLines(lines, start, end);
239
+ const doc = {
240
+ lines,
241
+ terminators,
242
+ eol,
243
+ hadBOM,
244
+ trailingNewline,
245
+ blockRange: { start, end },
246
+ model,
247
+ modelLineIndex,
248
+ reasoningEffort,
249
+ reasoningEffortLineIndex,
250
+ };
251
+ return { ok: true, doc };
252
+ }
253
+ /**
254
+ * Renders `doc` back to a string. For an unmodified doc this is
255
+ * byte-identical to the original `parseCodexAgentToml` input (matrix row
256
+ * A14) — it never re-derives line content, only rejoins each line with its
257
+ * OWN recorded terminator (`terminators[i]`, never the whole-file `eol`) and
258
+ * re-prepends a BOM if one was present. This is a plain concatenation of the
259
+ * surviving `[line, terminator]` pieces, so a source with mixed `\r\n`/`\n`/
260
+ * lone-`\r` line endings round-trips exactly, and a strip
261
+ * ({@link stripModel}/{@link stripReasoningEffort}) removes only the target
262
+ * line and its own terminator — every other line's ending is untouched.
263
+ */
264
+ function renderCodexAgentToml(doc) {
265
+ let body = '';
266
+ for (let i = 0; i < doc.lines.length; i++) {
267
+ body += doc.lines[i] + (doc.terminators[i] ?? '');
268
+ }
269
+ return doc.hadBOM ? BOM_CHAR + body : body;
270
+ }
271
+ // Removes exactly one line (by index) — AND its own terminator — from
272
+ // `doc.lines`/`doc.terminators`, re-indexing the block range and the OTHER
273
+ // key's line index so a subsequent strip/render still sees a consistent doc.
274
+ // Never touches any other line's content or terminator.
275
+ //
276
+ // The one exception is when `index` names the file's LAST line: a plain
277
+ // slice-out would drop the removed line's terminator but leave the
278
+ // *previous* line's terminator standing in its place, which silently
279
+ // invents (or drops) a trailing newline the source never had — a middle-line
280
+ // removal never has this problem because the terminator that survives (the
281
+ // one that WAS between the previous line and the removed one) is exactly the
282
+ // terminator the new neighbors should have between them. For a last-line
283
+ // removal, the file's trailing-newline-or-not status lives in whether the
284
+ // REMOVED line's own terminator was empty (that is what `trailingNewline`
285
+ // was computed from) — so the new last line inherits the removed line's
286
+ // EMPTINESS only: if the removed terminator was `''`, the new last line's
287
+ // terminator is cleared to `''` too. If the removed terminator was
288
+ // non-empty, the source already ended with a newline and the new last line
289
+ // already has the right one (its OWN, unchanged) — overwriting it with the
290
+ // removed line's terminator would silently change the new last line's own
291
+ // ending style on a mixed-EOL source (see A26). Removing the only remaining
292
+ // line is the degenerate case: there is no new last line, so the result is
293
+ // the empty document.
294
+ function removeLine(doc, index, which) {
295
+ const isLastLine = index === doc.lines.length - 1;
296
+ let lines;
297
+ let terminators;
298
+ if (doc.lines.length === 1) {
299
+ lines = [];
300
+ terminators = [];
301
+ }
302
+ else if (isLastLine) {
303
+ lines = doc.lines.slice(0, index);
304
+ terminators = doc.terminators.slice(0, index);
305
+ // Inherit the removed line's EMPTINESS, never its STYLE: if the removed
306
+ // line had no terminator (the source had no trailing newline), the new
307
+ // last line's terminator becomes '' too. Otherwise the source DID end
308
+ // with a newline, and the new last line already has the right one — its
309
+ // OWN terminator (already carried over by the slice above), which may
310
+ // differ in style from the removed line's (a mixed-EOL source) — so it is
311
+ // left unchanged rather than overwritten.
312
+ if (doc.terminators[index] === '') {
313
+ terminators[terminators.length - 1] = '';
314
+ }
315
+ }
316
+ else {
317
+ lines = doc.lines.slice(0, index).concat(doc.lines.slice(index + 1));
318
+ terminators = doc.terminators.slice(0, index).concat(doc.terminators.slice(index + 1));
319
+ }
320
+ const reindex = (i) => (i === null ? null : i > index ? i - 1 : i);
321
+ const blockRange = { ...doc.blockRange };
322
+ if (blockRange.start !== -1) {
323
+ if (blockRange.start > index)
324
+ blockRange.start -= 1;
325
+ if (blockRange.end > index)
326
+ blockRange.end -= 1;
327
+ }
328
+ return {
329
+ ...doc,
330
+ lines,
331
+ terminators,
332
+ blockRange,
333
+ model: which === 'model' ? null : doc.model,
334
+ modelLineIndex: which === 'model' ? null : reindex(doc.modelLineIndex),
335
+ reasoningEffort: which === 'reasoningEffort' ? null : doc.reasoningEffort,
336
+ reasoningEffortLineIndex: which === 'reasoningEffort' ? null : reindex(doc.reasoningEffortLineIndex),
337
+ };
338
+ }
339
+ /**
340
+ * Returns a new doc with the `model` line removed (a no-op copy if there was
341
+ * no `model` line). Every other byte — comments, other keys, the
342
+ * `developer_instructions` block, line endings, BOM — is untouched.
343
+ */
344
+ function stripModel(doc) {
345
+ if (doc.modelLineIndex === null)
346
+ return doc;
347
+ return removeLine(doc, doc.modelLineIndex, 'model');
348
+ }
349
+ /**
350
+ * Returns a new doc with the `model_reasoning_effort` line removed (a no-op
351
+ * copy if there was none). Every other byte is untouched.
352
+ */
353
+ function stripReasoningEffort(doc) {
354
+ if (doc.reasoningEffortLineIndex === null)
355
+ return doc;
356
+ return removeLine(doc, doc.reasoningEffortLineIndex, 'reasoningEffort');
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
+ }