@opengsd/gsd-core 1.10.0 → 1.11.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 (328) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-debug-session-manager.md +11 -0
  4. package/agents/gsd-doc-synthesizer.md +2 -4
  5. package/agents/gsd-executor.md +5 -5
  6. package/agents/gsd-mempalace-curator.md +5 -2
  7. package/agents/gsd-phase-researcher.md +20 -1
  8. package/agents/gsd-plan-checker.md +37 -0
  9. package/agents/gsd-planner.md +44 -46
  10. package/agents/gsd-user-profiler.md +3 -0
  11. package/agents/gsd-verifier.md +12 -3
  12. package/bin/install.js +841 -971
  13. package/bin/lib/ui-safety-gate.cjs +2 -0
  14. package/commands/gsd/code-review.md +1 -1
  15. package/commands/gsd/execute-phase.md +1 -1
  16. package/commands/gsd/map-codebase.md +1 -1
  17. package/commands/gsd/mempalace-capture.md +1 -1
  18. package/commands/gsd/mempalace-recall.md +1 -1
  19. package/commands/gsd/new-milestone.md +1 -1
  20. package/commands/gsd/quick.md +1 -1
  21. package/commands/gsd/review-backlog.md +2 -1
  22. package/commands/gsd/verify-work.md +1 -1
  23. package/gsd-core/bin/gsd-tools.cjs +469 -88
  24. package/gsd-core/bin/lib/active-workstream-store.cjs +138 -22
  25. package/gsd-core/bin/lib/agent-install-check.cjs +230 -32
  26. package/gsd-core/bin/lib/api-coverage.cjs +3 -5
  27. package/gsd-core/bin/lib/artifacts.cjs +3 -0
  28. package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
  29. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  30. package/gsd-core/bin/lib/audit.cjs +876 -240
  31. package/gsd-core/bin/lib/broken-windows.cjs +1 -1
  32. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  33. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  34. package/gsd-core/bin/lib/capability-registry.cjs +575 -101
  35. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  36. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  37. package/gsd-core/bin/lib/capability-validator.cjs +495 -22
  38. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  39. package/gsd-core/bin/lib/check-command-router.cjs +71 -37
  40. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  41. package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
  42. package/gsd-core/bin/lib/command-aliases.cjs +22 -0
  43. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  44. package/gsd-core/bin/lib/commands.cjs +651 -86
  45. package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
  46. package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
  47. package/gsd-core/bin/lib/config-loader.cjs +75 -0
  48. package/gsd-core/bin/lib/config.cjs +10 -1
  49. package/gsd-core/bin/lib/core-utils.cjs +127 -29
  50. package/gsd-core/bin/lib/decisions.cjs +23 -0
  51. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  52. package/gsd-core/bin/lib/frontmatter.cjs +155 -20
  53. package/gsd-core/bin/lib/gap-checker.cjs +68 -7
  54. package/gsd-core/bin/lib/git-base-branch.cjs +102 -0
  55. package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
  56. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  57. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  58. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
  59. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  60. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  61. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  62. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
  63. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  64. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  65. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
  66. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  67. package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
  68. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  69. package/gsd-core/bin/lib/init.cjs +321 -129
  70. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  71. package/gsd-core/bin/lib/install-engine.cjs +745 -258
  72. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  73. package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
  74. package/gsd-core/bin/lib/install-profiles.cjs +134 -57
  75. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  76. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  77. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  78. package/gsd-core/bin/lib/installer-migrations.cjs +138 -31
  79. package/gsd-core/bin/lib/io.cjs +10 -0
  80. package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
  81. package/gsd-core/bin/lib/markdown-table.cjs +133 -20
  82. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  83. package/gsd-core/bin/lib/milestone.cjs +754 -70
  84. package/gsd-core/bin/lib/model-catalog.cjs +59 -1
  85. package/gsd-core/bin/lib/model-resolver.cjs +183 -40
  86. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  87. package/gsd-core/bin/lib/pattern.cjs +122 -0
  88. package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
  89. package/gsd-core/bin/lib/phase-id.cjs +444 -36
  90. package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
  91. package/gsd-core/bin/lib/phase-locator.cjs +125 -18
  92. package/gsd-core/bin/lib/phase.cjs +646 -143
  93. package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
  94. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  95. package/gsd-core/bin/lib/plan-scan.cjs +86 -2
  96. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  97. package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
  98. package/gsd-core/bin/lib/planning-workspace.cjs +56 -6
  99. package/gsd-core/bin/lib/probe-core.cjs +1 -1
  100. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  101. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
  102. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
  103. package/gsd-core/bin/lib/review-lane-descriptor.cjs +13 -4
  104. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  105. package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
  106. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  107. package/gsd-core/bin/lib/roadmap-command-router.cjs +34 -0
  108. package/gsd-core/bin/lib/roadmap-parser.cjs +943 -184
  109. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  110. package/gsd-core/bin/lib/roadmap.cjs +385 -94
  111. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +608 -46
  112. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  113. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +426 -55
  114. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  115. package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
  116. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +115 -3
  117. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  118. package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
  119. package/gsd-core/bin/lib/security.cjs +104 -5
  120. package/gsd-core/bin/lib/shell-command-projection.cjs +275 -3
  121. package/gsd-core/bin/lib/smart-entry.cjs +142 -22
  122. package/gsd-core/bin/lib/state-command-router.cjs +5 -1
  123. package/gsd-core/bin/lib/state-document.cjs +152 -8
  124. package/gsd-core/bin/lib/state-transition.cjs +371 -117
  125. package/gsd-core/bin/lib/state.cjs +1794 -357
  126. package/gsd-core/bin/lib/surface.cjs +23 -9
  127. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  128. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  129. package/gsd-core/bin/lib/uat-predicate.cjs +9 -3
  130. package/gsd-core/bin/lib/uat.cjs +399 -56
  131. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  132. package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
  133. package/gsd-core/bin/lib/unusable-input.cjs +24 -0
  134. package/gsd-core/bin/lib/update-context.cjs +8 -2
  135. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  136. package/gsd-core/bin/lib/validate.cjs +20 -6
  137. package/gsd-core/bin/lib/vendor/README.md +37 -0
  138. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  139. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  140. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  141. package/gsd-core/bin/lib/verification.cjs +258 -8
  142. package/gsd-core/bin/lib/verify.cjs +368 -888
  143. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
  144. package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
  145. package/gsd-core/bin/lib/workstream.cjs +2 -2
  146. package/gsd-core/bin/lib/worktree-safety.cjs +176 -9
  147. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  148. package/gsd-core/bin/shared/config-schema.manifest.json +7 -1
  149. package/gsd-core/references/agent-contracts.md +43 -26
  150. package/gsd-core/references/checkpoints.md +2 -2
  151. package/gsd-core/references/context-budget.md +1 -1
  152. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  153. package/gsd-core/references/doc-conflict-engine.md +1 -1
  154. package/gsd-core/references/execute-mvp-tdd.md +3 -3
  155. package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
  156. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  157. package/gsd-core/references/execute-phase-response-language.md +1 -1
  158. package/gsd-core/references/execute-phase-wave-guard.md +6 -2
  159. package/gsd-core/references/gate-prompts.md +1 -1
  160. package/gsd-core/references/git-planning-commit.md +2 -1
  161. package/gsd-core/references/loop-hook-dispatch.md +39 -2
  162. package/gsd-core/references/model-profiles.md +12 -4
  163. package/gsd-core/references/mvp-concepts.md +9 -9
  164. package/gsd-core/references/planner-guidance.md +3 -9
  165. package/gsd-core/references/planner-preconditions.md +1 -1
  166. package/gsd-core/references/planner-reviews.md +1 -1
  167. package/gsd-core/references/planning-config.md +8 -6
  168. package/gsd-core/references/revision-loop.md +1 -1
  169. package/gsd-core/references/specless-probe-fallback.md +1 -1
  170. package/gsd-core/references/universal-anti-patterns.md +3 -3
  171. package/gsd-core/references/verifier-phase-gates.md +192 -0
  172. package/gsd-core/references/verify-mvp-mode.md +1 -1
  173. package/gsd-core/references/workstream-flag.md +22 -6
  174. package/gsd-core/templates/discussion-log.md +1 -1
  175. package/gsd-core/templates/phase-prompt.md +2 -4
  176. package/gsd-core/templates/state.md +4 -4
  177. package/gsd-core/templates/verification-report.md +9 -1
  178. package/gsd-core/workflows/ai-integration-phase.md +9 -11
  179. package/gsd-core/workflows/autonomous.md +1 -1
  180. package/gsd-core/workflows/cleanup.md +62 -3
  181. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +13 -3
  182. package/gsd-core/workflows/code-review-fix.md +37 -10
  183. package/gsd-core/workflows/code-review.md +38 -12
  184. package/gsd-core/workflows/complete-milestone.md +141 -18
  185. package/gsd-core/workflows/debug.md +7 -5
  186. package/gsd-core/workflows/diagnose-issues.md +35 -9
  187. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
  188. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  189. package/gsd-core/workflows/discuss-phase-assumptions.md +2 -1
  190. package/gsd-core/workflows/edit-phase.md +26 -1
  191. package/gsd-core/workflows/eval-review.md +3 -5
  192. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +31 -6
  193. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  194. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +2 -0
  195. package/gsd-core/workflows/execute-phase.md +38 -50
  196. package/gsd-core/workflows/execute-plan.md +36 -4
  197. package/gsd-core/workflows/explore.md +131 -4
  198. package/gsd-core/workflows/fast.md +10 -2
  199. package/gsd-core/workflows/health.md +73 -4
  200. package/gsd-core/workflows/import.md +4 -4
  201. package/gsd-core/workflows/ingest-docs.md +5 -5
  202. package/gsd-core/workflows/mvp-phase.md +6 -3
  203. package/gsd-core/workflows/new-milestone.md +14 -9
  204. package/gsd-core/workflows/new-project.md +14 -14
  205. package/gsd-core/workflows/next.md +12 -0
  206. package/gsd-core/workflows/plan-phase.md +41 -17
  207. package/gsd-core/workflows/plan-review-convergence.md +50 -2
  208. package/gsd-core/workflows/progress.md +34 -6
  209. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -4
  210. package/gsd-core/workflows/quick/steps/quick-verification.md +27 -6
  211. package/gsd-core/workflows/quick/steps/research-phase.md +2 -2
  212. package/gsd-core/workflows/quick.md +35 -15
  213. package/gsd-core/workflows/review.md +26 -5
  214. package/gsd-core/workflows/secure-phase.md +1 -1
  215. package/gsd-core/workflows/session-report.md +2 -1
  216. package/gsd-core/workflows/settings.md +66 -2
  217. package/gsd-core/workflows/ship.md +104 -44
  218. package/gsd-core/workflows/spec-phase.md +30 -12
  219. package/gsd-core/workflows/sync-skills.md +63 -8
  220. package/gsd-core/workflows/transition.md +46 -11
  221. package/gsd-core/workflows/ui-phase.md +5 -5
  222. package/gsd-core/workflows/ui-review.md +2 -2
  223. package/gsd-core/workflows/update.md +1 -1
  224. package/gsd-core/workflows/validate-phase.md +1 -1
  225. package/gsd-core/workflows/verify-work.md +9 -7
  226. package/hooks/dist/gsd-agent-isolation-guard.js +103 -14
  227. package/hooks/dist/gsd-check-update-worker.js +56 -13
  228. package/hooks/dist/gsd-check-update.js +19 -1
  229. package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
  230. package/hooks/dist/gsd-cursor-subagent-start.js +77 -2
  231. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
  232. package/hooks/dist/gsd-prompt-guard.js +21 -20
  233. package/hooks/dist/gsd-read-injection-scanner.js +38 -24
  234. package/hooks/dist/gsd-statusline.js +18 -0
  235. package/hooks/dist/gsd-update-banner.js +22 -1
  236. package/hooks/dist/gsd-workflow-guard.js +134 -36
  237. package/hooks/dist/lib/git-cmd.js +92 -59
  238. package/hooks/dist/lib/injection-patterns.js +45 -0
  239. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  240. package/hooks/dist/lib/isolation-sentinel.js +9 -0
  241. package/hooks/gsd-agent-isolation-guard.js +103 -14
  242. package/hooks/gsd-check-update-worker.js +56 -13
  243. package/hooks/gsd-check-update.js +19 -1
  244. package/hooks/gsd-cursor-pre-tool.js +0 -3
  245. package/hooks/gsd-cursor-subagent-start.js +77 -2
  246. package/hooks/gsd-cursor-subagent-stop.js +3 -2
  247. package/hooks/gsd-prompt-guard.js +21 -20
  248. package/hooks/gsd-read-injection-scanner.js +38 -24
  249. package/hooks/gsd-statusline.js +18 -0
  250. package/hooks/gsd-update-banner.js +22 -1
  251. package/hooks/gsd-workflow-guard.js +134 -36
  252. package/hooks/lib/git-cmd.js +92 -59
  253. package/hooks/lib/injection-patterns.js +45 -0
  254. package/hooks/lib/isolation-deny-reason.js +39 -0
  255. package/hooks/lib/isolation-sentinel.js +9 -0
  256. package/package.json +21 -9
  257. package/pi/gsd.cjs +19 -5
  258. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  259. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  260. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  261. package/scripts/changeset/lint.cjs +60 -5
  262. package/scripts/check-alias-drift.cjs +7 -43
  263. package/scripts/check-contract-drift.cjs +297 -0
  264. package/scripts/ci-test-scope.cjs +19 -2
  265. package/scripts/command-contract-helpers.cjs +903 -1
  266. package/scripts/gen-adr-index.cjs +728 -38
  267. package/scripts/gen-capability-registry.cjs +3 -15
  268. package/scripts/gen-context-index.cjs +2 -11
  269. package/scripts/gen-health-docs.cjs +390 -0
  270. package/scripts/gen-inventory-manifest.cjs +50 -4
  271. package/scripts/gen-loop-host-contract.cjs +4 -24
  272. package/scripts/gen-registry.cjs +3 -14
  273. package/scripts/lib/alias-drift-families.cjs +46 -0
  274. package/scripts/lib/drift-scan.cjs +278 -0
  275. package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
  276. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  277. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  278. package/scripts/lint-canary-version-leak.cjs +73 -0
  279. package/scripts/lint-command-contract.cjs +96 -13
  280. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  281. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  282. package/scripts/lint-default-flip-documentation.cjs +193 -0
  283. package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
  284. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  285. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  286. package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
  287. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  288. package/scripts/lint-milestone-window-drift.cjs +468 -0
  289. package/scripts/lint-phase-enumeration-drift.cjs +479 -0
  290. package/scripts/lint-plan-count-drift.cjs +318 -0
  291. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  292. package/scripts/lint-planning-prompt-drift.cjs +434 -0
  293. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  294. package/scripts/lint-regression-test-names.cjs +15 -13
  295. package/scripts/lint-removed-but-needed.cjs +320 -0
  296. package/scripts/lint-state-field-drift.cjs +805 -0
  297. package/scripts/lint-state-write-path-drift.cjs +1045 -0
  298. package/scripts/lint-test-file-count.allowlist.json +21 -10
  299. package/scripts/lint-unreachable-guard-drift.cjs +843 -0
  300. package/scripts/lint-vendored-deps.cjs +124 -0
  301. package/scripts/pr-changed-files.cjs +63 -0
  302. package/scripts/pr-template-policy.cjs +14 -4
  303. package/scripts/prompt-injection-scan.sh +25 -0
  304. package/scripts/require-issue-link-policy.cjs +192 -0
  305. package/scripts/state-write-path-drift-baseline.json +19 -0
  306. package/scripts/sync-runtime-launcher.cjs +2 -4
  307. package/skills/gsd-autonomous/SKILL.md +0 -1
  308. package/skills/gsd-code-review/SKILL.md +1 -1
  309. package/skills/gsd-execute-phase/SKILL.md +1 -2
  310. package/skills/gsd-map-codebase/SKILL.md +1 -1
  311. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  312. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  313. package/skills/gsd-new-milestone/SKILL.md +1 -1
  314. package/skills/gsd-next/SKILL.md +0 -1
  315. package/skills/gsd-plan-phase/SKILL.md +0 -1
  316. package/skills/gsd-progress/SKILL.md +0 -1
  317. package/skills/gsd-quick/SKILL.md +1 -1
  318. package/skills/gsd-review-backlog/SKILL.md +2 -1
  319. package/skills/gsd-stats/SKILL.md +0 -1
  320. package/skills/gsd-verify-work/SKILL.md +1 -1
  321. package/vscode/package.json +1 -1
  322. package/gsd-core/workflows/discovery-phase.md +0 -298
  323. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  324. package/gsd-core/workflows/verify-phase.md +0 -574
  325. package/scripts/affected-tests-lib.cjs +0 -554
  326. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  327. package/scripts/run-affected-tests.cjs +0 -7
  328. package/scripts/run-tests.cjs +0 -1051
@@ -28,6 +28,7 @@ const { extractFrontmatter } = frontmatterMod;
28
28
  const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
29
29
  const security_cjs_1 = require("./security.cjs");
30
30
  const ui_safety_gate_cjs_1 = require("./ui-safety-gate.cjs");
31
+ const ui_frontend_evidence_cjs_1 = require("./ui-frontend-evidence.cjs");
31
32
  // eslint-disable-next-line @typescript-eslint/no-require-imports
32
33
  const verifyModule = require("./verify.cjs");
33
34
  const { cmdVerifySchemaDrift, cmdVerifyCodebaseDrift } = verifyModule;
@@ -45,6 +46,12 @@ const { evaluatePredicate } = gatePredicateEval;
45
46
  const apiCoverageMod = require("./api-coverage.cjs");
46
47
  const { detectApiIntegration, validateCoverageMatrix } = apiCoverageMod;
47
48
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
49
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
50
+ const planScanMod = require("./plan-scan.cjs");
51
+ const { scanPhasePlans } = planScanMod;
52
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
53
+ const planningScopeMod = require("./planning-scope.cjs");
54
+ const { SCOPE } = planningScopeMod;
48
55
  // ─── Helpers ──────────────────────────────────────────────────────────────────
49
56
  function normalizePhrase(text) {
50
57
  // eslint-disable-next-line @typescript-eslint/no-base-to-string
@@ -128,14 +135,13 @@ function gateEnabled(projectDir) {
128
135
  function loadPlanContents(phaseDir) {
129
136
  if (!node_fs_1.default.existsSync(phaseDir))
130
137
  return [];
131
- try {
132
- return node_fs_1.default.readdirSync(phaseDir)
133
- .filter((entry) => /-PLAN\.md$/.test(entry))
134
- .map((entry) => readIfExists(node_path_1.default.join(phaseDir, entry)));
135
- }
136
- catch {
137
- return [];
138
- }
138
+ // #3183 (lint-plan-count-drift): source live plan files from the single
139
+ // owner (scanPhasePlans) instead of a local `-PLAN.md` readdirSync filter
140
+ // — picks up bare PLAN.md and nested plans/, and excludes plans marked
141
+ // `status: superseded`, which the prior root-only exact-suffix filter did
142
+ // neither for.
143
+ return scanPhasePlans(phaseDir).planFiles
144
+ .map((entry) => readIfExists(node_path_1.default.join(phaseDir, entry)));
139
145
  }
140
146
  const DESIGNATED_HEADINGS_RE = /^#{1,6}\s+(?:must[_ ]haves?|truths?|tasks?|objective)\b/i;
141
147
  // #2372: scanned-tag set must match the planner-canonical surfaces where a D-NN citation
@@ -337,6 +343,7 @@ function recentCommitMessages(projectDir) {
337
343
  encoding: 'utf-8',
338
344
  maxBuffer: 4 * 1024 * 1024,
339
345
  windowsHide: true,
346
+ timeout: 15_000,
340
347
  });
341
348
  }
342
349
  catch {
@@ -411,8 +418,11 @@ function cmdDecisionCoverageVerify(projectDir, args, raw) {
411
418
  return;
412
419
  }
413
420
  const planContents = loadPlanContents(phaseDir);
421
+ // #3183 (lint-plan-count-drift): same single-owner sourcing as
422
+ // loadPlanContents above — scanPhasePlans's summaryFiles instead of a
423
+ // local `-SUMMARY.md` readdirSync filter.
414
424
  const summaryParts = node_fs_1.default.existsSync(phaseDir)
415
- ? node_fs_1.default.readdirSync(phaseDir).filter((entry) => /-SUMMARY\.md$/.test(entry)).map((entry) => readIfExists(node_path_1.default.join(phaseDir, entry)))
425
+ ? scanPhasePlans(phaseDir).summaryFiles.map((entry) => readIfExists(node_path_1.default.join(phaseDir, entry)))
416
426
  : [];
417
427
  const haystack = [
418
428
  planContents.join('\n\n'),
@@ -442,8 +452,9 @@ function cmdDecisionCoverageVerify(projectDir, args, raw) {
442
452
  * ui-plan-gate: given a phase number, checks whether the phase has frontend
443
453
  * indicators and whether a *-UI-SPEC.md already exists in the phase directory.
444
454
  *
445
- * Returns JSON: { frontend: boolean, hasUiSpec: boolean, block: boolean }
446
- * block = frontend && !hasUiSpec (gate fires when UI work is detected but no spec exists)
455
+ * Returns JSON: { frontend, hasFrontendEvidence, hasUiSpec, block, uiSpecPath, matchedToken, matchedLine }
456
+ * block = frontend && hasFrontendEvidence && !hasUiSpec (#3312: gate fires when
457
+ * UI work is detected AND the repo has static frontend evidence but no spec exists)
447
458
  *
448
459
  * Invocable as: gsd_run check ui-plan-gate <phase>
449
460
  *
@@ -477,10 +488,21 @@ function findUiSpecInDir(phaseDir) {
477
488
  * (b) Runs checkUiPresence (frontend detection) — no reimplementation.
478
489
  * (c) Resolves the phase directory via findPhaseInternal (phase-locator.cjs); checks for *-UI-SPEC.md.
479
490
  *
480
- * Returns: { frontend, hasUiSpec, block, uiSpecPath, phaseLookupFailed }
481
- * block = frontend && !hasUiSpec
491
+ * Returns: { frontend, hasFrontendEvidence, hasUiSpec, block, uiSpecPath, matchedToken, matchedLine, phaseLookupFailed }
492
+ * block = frontend && hasFrontendEvidence && !hasUiSpec (#3312)
482
493
  * phaseLookupFailed = ROADMAP.md present but phase header not found (surfaced for
483
494
  * onError:halt gates so a missing phase doesn't silently bypass)
495
+ *
496
+ * #3312 — structural corroboration: `frontend` is a vocabulary signal only. A
497
+ * hyphen is a word boundary, so a phase naming the repo `dashboard-financeiro`
498
+ * matches the token `dashboard` exactly like the real compound `micro-frontend`
499
+ * (the boundary rule of #3718 is intentional and untouched). The gate therefore
500
+ * blocks only when the token match is corroborated by static frontend evidence
501
+ * in the repo tree (hasStaticFrontendEvidence: package.json UI-framework dep or
502
+ * a component-framework file). This mirrors the sibling post-wave gate
503
+ * computeUiSafetyGate, which requires `hasUiFiles` (git diff) before blocking.
504
+ * matchedToken/matchedLine surface what tripped the sniffer so an operator can
505
+ * judge the flag in one second instead of reaching for --skip-ui.
484
506
  */
485
507
  function computeUiPlanGate(projectDir, phase) {
486
508
  // (a) Read the phase section text using the same two-pass lookup as roadmap.get-phase.
@@ -509,6 +531,9 @@ function computeUiPlanGate(projectDir, phase) {
509
531
  // (b) Run checkUiPresence (frontend detection) — reuse existing helper; no reimplementation
510
532
  const presenceResult = (0, ui_safety_gate_cjs_1.checkUiPresence)(phaseSection);
511
533
  const frontend = presenceResult.hasUI;
534
+ // (b') #3312 — static structural corroboration. Only probed when the sniffer
535
+ // matched (evidence is irrelevant otherwise); failures degrade to false.
536
+ const hasFrontendEvidence = frontend ? (0, ui_frontend_evidence_cjs_1.hasStaticFrontendEvidence)(projectDir) : false;
512
537
  // (c) Resolve phase directory via findPhaseInternal and check for *-UI-SPEC.md
513
538
  let phaseDir = '';
514
539
  try {
@@ -528,10 +553,13 @@ function computeUiPlanGate(projectDir, phase) {
528
553
  catch { /* phase dir lookup failure → hasUiSpec=false */ }
529
554
  const uiSpecPath = findUiSpecInDir(phaseDir);
530
555
  const hasUiSpec = uiSpecPath !== '';
531
- // block = frontend phase with no UI-SPEC
532
- const block = frontend && !hasUiSpec;
556
+ // block = frontend phase with structural frontend evidence and no UI-SPEC (#3312)
557
+ const block = frontend && hasFrontendEvidence && !hasUiSpec;
533
558
  const result = {
534
- frontend, hasUiSpec, block, uiSpecPath: hasUiSpec ? uiSpecPath : null,
559
+ frontend, hasFrontendEvidence, hasUiSpec, block,
560
+ uiSpecPath: hasUiSpec ? uiSpecPath : null,
561
+ matchedToken: presenceResult.matchedToken,
562
+ matchedLine: presenceResult.matchedLine,
535
563
  };
536
564
  if (phaseLookupFailed)
537
565
  result.phaseLookupFailed = true;
@@ -613,6 +641,7 @@ function computeUiSafetyGate(projectDir, phase) {
613
641
  encoding: 'utf-8',
614
642
  maxBuffer: 2 * 1024 * 1024,
615
643
  windowsHide: true,
644
+ timeout: 10_000,
616
645
  });
617
646
  hasUiFiles = changed.split('\n').some((f) => f.trim() && (UI_FILE_EXTENSIONS_RE.test(f) || UI_PATH_PATTERNS_RE.test(f)));
618
647
  }
@@ -679,7 +708,9 @@ function cmdTddReviewCheckpoint(projectDir, args, raw) {
679
708
  const tddPlanFiles = [];
680
709
  if (phaseDir) {
681
710
  try {
682
- const files = node_fs_1.default.readdirSync(phaseDir).filter(f => f.endsWith('-PLAN.md'));
711
+ // #3183: canonical plan set (root+nested, superseded-excluded) from the
712
+ // single owner, rather than a root-only hand-rolled readdirSync filter.
713
+ const files = scanPhasePlans(phaseDir).planFiles;
683
714
  for (const file of files) {
684
715
  const planPath = node_path_1.default.join(phaseDir, file);
685
716
  const content = readIfExists(planPath);
@@ -728,17 +759,17 @@ function cmdTddReviewCheckpoint(projectDir, args, raw) {
728
759
  let green = false;
729
760
  let refactor = false;
730
761
  try {
731
- const redCommit = (0, node_child_process_1.execFileSync)('git', ['log', '--oneline', `--grep=^test(${planId}):`, '--', '.'], { cwd: projectDir, encoding: 'utf-8', maxBuffer: 1024 * 1024, windowsHide: true });
762
+ const redCommit = (0, node_child_process_1.execFileSync)('git', ['log', '--oneline', `--grep=^test(${planId}):`, '--', '.'], { cwd: projectDir, encoding: 'utf-8', maxBuffer: 1024 * 1024, windowsHide: true, timeout: 10_000 });
732
763
  red = redCommit.trim().length > 0;
733
764
  }
734
765
  catch { /* git unavailable or no match */ }
735
766
  try {
736
- const greenCommit = (0, node_child_process_1.execFileSync)('git', ['log', '--oneline', `--grep=^feat(${planId}):`, '--', '.'], { cwd: projectDir, encoding: 'utf-8', maxBuffer: 1024 * 1024, windowsHide: true });
767
+ const greenCommit = (0, node_child_process_1.execFileSync)('git', ['log', '--oneline', `--grep=^feat(${planId}):`, '--', '.'], { cwd: projectDir, encoding: 'utf-8', maxBuffer: 1024 * 1024, windowsHide: true, timeout: 10_000 });
737
768
  green = greenCommit.trim().length > 0;
738
769
  }
739
770
  catch { /* git unavailable or no match */ }
740
771
  try {
741
- const refactorCommit = (0, node_child_process_1.execFileSync)('git', ['log', '--oneline', `--grep=^refactor(${planId}):`, '--', '.'], { cwd: projectDir, encoding: 'utf-8', maxBuffer: 1024 * 1024, windowsHide: true });
772
+ const refactorCommit = (0, node_child_process_1.execFileSync)('git', ['log', '--oneline', `--grep=^refactor(${planId}):`, '--', '.'], { cwd: projectDir, encoding: 'utf-8', maxBuffer: 1024 * 1024, windowsHide: true, timeout: 10_000 });
742
773
  refactor = refactorCommit.trim().length > 0;
743
774
  }
744
775
  catch { /* git unavailable or no match */ }
@@ -1207,12 +1238,26 @@ function isRealReadFailure(err) {
1207
1238
  function readPhaseScope(projectDir, phaseDir, phaseNumber) {
1208
1239
  const chunks = [];
1209
1240
  let readError = null;
1210
- try {
1211
- const entries = node_fs_1.default.readdirSync(phaseDir, { withFileTypes: true });
1212
- const plans = entries
1213
- .filter((e) => e.isFile() && /-PLAN\.md$/i.test(e.name))
1214
- .map((e) => e.name)
1215
- .sort();
1241
+ // A MISSING phase directory is fine (no plans yet → fall through to the
1242
+ // roadmap). Checked up front (rather than via a readdirSync catch) because
1243
+ // #3183 (lint-plan-count-drift) now sources the plan-file list from the
1244
+ // single owner (scanPhasePlans) instead of a local `-PLAN\.md$` readdirSync
1245
+ // filter — picks up bare PLAN.md and nested plans/, and excludes
1246
+ // superseded plans, none of which the prior root-only exact-suffix filter
1247
+ // did.
1248
+ if (node_fs_1.default.existsSync(phaseDir)) {
1249
+ const scan = scanPhasePlans(phaseDir);
1250
+ if (scan.scope === SCOPE.UNREADABLE) {
1251
+ // Directory exists but scanPhasePlans's own readdirSync(phaseDir) call
1252
+ // failed (EACCES/EIO race) — a real read failure the gate must not
1253
+ // silently pass (#2365 review), mirroring the prior isRealReadFailure
1254
+ // branch below for the readdirSync-throws case.
1255
+ return {
1256
+ text: '',
1257
+ readError: 'could not read the phase directory: scanPhasePlans reported scope UNREADABLE',
1258
+ };
1259
+ }
1260
+ const plans = [...scan.planFiles].sort();
1216
1261
  for (const p of plans) {
1217
1262
  try {
1218
1263
  chunks.push(node_fs_1.default.readFileSync(node_path_1.default.join(phaseDir, p), 'utf8'));
@@ -1226,17 +1271,6 @@ function readPhaseScope(projectDir, phaseDir, phaseNumber) {
1226
1271
  }
1227
1272
  }
1228
1273
  }
1229
- catch (err) {
1230
- // A MISSING phase directory is fine (no plans yet → fall through to the
1231
- // roadmap). A directory that exists but cannot be enumerated (EACCES/EIO)
1232
- // is a real read failure the gate must not silently pass (#2365 review).
1233
- if (isRealReadFailure(err)) {
1234
- return {
1235
- text: '',
1236
- readError: `could not read the phase directory: ${err instanceof Error ? err.message : String(err)}`,
1237
- };
1238
- }
1239
- }
1240
1274
  if (readError)
1241
1275
  return { text: chunks.join('\n\n'), readError };
1242
1276
  if (chunks.join('').trim().length > 0)
@@ -470,13 +470,43 @@ function emitWorkflowScript(input) {
470
470
  lines.push('// read-only in a Workflow script — set it via the caller\'s token directive.');
471
471
  }
472
472
  lines.push('');
473
+ // #3302: the generated script must hand the per-agent executor results back
474
+ // to the orchestrator so it can feed the wave merge chain. Emitted right
475
+ // after the header comments (meta stays the first statement): a helper that
476
+ // extracts the executor's <worktree_metadata> JSON (agents/gsd-executor.md
477
+ // <worktree_metadata_capture>) from one agent() result, plus the outcomes
478
+ // accumulator the stage barriers below push into. `metadata` is null when
479
+ // the result carried no parseable block — the LOUD-failure input the
480
+ // orchestrator halts on for expects_worktree plans (never a silent skip).
481
+ lines.push('// #3302: extract the executor-returned <worktree_metadata> JSON so the');
482
+ lines.push('// orchestrator can record it into WAVE_WORKTREE_MANIFEST after the run');
483
+ lines.push('// (worktree.record-agent -> worktree.cleanup-wave, the same manifest-scoped');
484
+ lines.push('// merge chain inline dispatch feeds). null = absent/unparseable/interrupted.');
485
+ lines.push('function gsdWorktreeMetadata(agentResult) {');
486
+ lines.push(' if (typeof agentResult !== \'string\') return null;');
487
+ lines.push(' const m = agentResult.match(/<worktree_metadata>([\\s\\S]*?)<\\/worktree_metadata>/);');
488
+ lines.push(' if (m === null) return null;');
489
+ lines.push(' try {');
490
+ lines.push(' const parsed = JSON.parse(m[1]);');
491
+ lines.push(' return (parsed !== null && typeof parsed === \'object\') ? parsed : null;');
492
+ lines.push(' } catch (e) {');
493
+ lines.push(' return null;');
494
+ lines.push(' }');
495
+ lines.push('}');
496
+ lines.push('const gsdAgentOutcomes = [];');
497
+ lines.push('');
473
498
  const stagesByWave = [];
474
499
  let totalPlans = 0;
500
+ let worktreePlans = 0;
475
501
  for (let wi = 0; wi < waves.length; wi++) {
476
502
  const wave = waves[wi];
477
503
  const stages = partitionStages(wave.plans);
478
504
  stagesByWave.push(stages);
479
505
  totalPlans += wave.plans.length;
506
+ for (const p of wave.plans) {
507
+ if (p.use_worktree !== false)
508
+ worktreePlans += 1;
509
+ }
480
510
  lines.push('// Wave ' + wave.id);
481
511
  // Title must match this wave's meta.phases entry EXACTLY.
482
512
  lines.push('phase(' + quoteString('Wave ' + wave.id) + ')');
@@ -491,17 +521,37 @@ function emitWorkflowScript(input) {
491
521
  // threw "parallel() expects an array of functions" (#2590). Passing
492
522
  // agent() results directly would also start every agent eagerly, before
493
523
  // parallel() could bound concurrency.
494
- lines.push('await parallel([');
524
+ // #3302: capture the barrier's resolved results (one per thunk, in thunk
525
+ // order — the documented parallel() contract) so each plan's outcome can
526
+ // be tagged and returned below. Discarding them stranded every
527
+ // worktree-wf_* branch: the merge chain had no input (#3302).
528
+ lines.push('const gsdStage_' + wi + '_' + si + ' = await parallel([');
495
529
  for (const p of stagePlans) {
496
530
  lines.push(' () => agent(' + quoteString(p.brief) + ', ' + agentOptions(p, executorModel) + '),');
497
531
  }
498
532
  lines.push('])');
533
+ // Positional tagging is decided at EMIT time from the validated manifest,
534
+ // so attribution survives out-of-order completion and needs no runtime
535
+ // introspection. expects_worktree mirrors agentOptions' own per-plan
536
+ // decision (use_worktree !== false).
537
+ lines.push('gsdAgentOutcomes.push(');
538
+ for (let pi = 0; pi < stagePlans.length; pi++) {
539
+ const p = stagePlans[pi];
540
+ const tail = pi < stagePlans.length - 1 ? ',' : '';
541
+ lines.push(' { plan: ' + quoteString(p.id) + ', expects_worktree: ' + (p.use_worktree !== false) + ', metadata: gsdWorktreeMetadata(gsdStage_' + wi + '_' + si + '[' + pi + ']) }' + tail);
542
+ }
543
+ lines.push(')');
499
544
  }
500
545
  if (wi < waves.length - 1)
501
546
  lines.push('');
502
547
  }
503
- lines.push('// Each agent writes SUMMARY.md on its worktree branch; commits land there');
504
- lines.push('// and are merged by the orchestrator exactly as in inline wave dispatch.');
548
+ // #3302: the script's top-level return value is what the Workflow tool hands
549
+ // back to the orchestrator. One { plan, expects_worktree, metadata } entry
550
+ // per dispatched plan; the orchestrator records every worktree entry via
551
+ // `gsd_run query worktree.record-agent` and HALTS on a null metadata entry
552
+ // for an expects_worktree plan (see the execute:wave:pre fragment) — a
553
+ // silently-empty manifest is the exact #3302 failure mode.
554
+ lines.push('return gsdAgentOutcomes');
505
555
  const script = lines.join('\n');
506
556
  return {
507
557
  ok: true,
@@ -509,6 +559,9 @@ function emitWorkflowScript(input) {
509
559
  summary: {
510
560
  waves: waves.length,
511
561
  plans: totalPlans,
562
+ // #3302: the number of record-agent entries the orchestrator must end up
563
+ // with in WAVE_WORKTREE_MANIFEST after the run — the loud count check.
564
+ worktreePlans,
512
565
  stagesByWave,
513
566
  resumeRunId: runId,
514
567
  budgetTokens,
@@ -0,0 +1,329 @@
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
+ * ── The reconciliation (40-design.md) ──────────────────────────────────────
18
+ *
19
+ * Phase 2's reader and this phase's writer disagree on how to handle an
20
+ * unterminated `developer_instructions` block, deliberately:
21
+ *
22
+ * - The READER (`scanTomlLines`, used directly by `checkCodexModelPosture`)
23
+ * stays LENIENT: an unterminated block still excludes "the rest of the
24
+ * file" from the header scan (findDeveloperInstructionsBlockRange's
25
+ * existing fallback), because misreading prompt prose as a pin is only a
26
+ * false positive — it wastes a user's time, nothing more.
27
+ * - The WRITER (`parseCodexAgentToml`, used by the Codex sync) is STRICT: an
28
+ * unterminated block makes the whole document `{ok:false}`, because a
29
+ * writer that proceeds on a malformed document risks rewriting it.
30
+ *
31
+ * One block-range detector, two call sites, two policies — never two detectors
32
+ * that could silently drift from each other.
33
+ */
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.PARSE_REASON = void 0;
36
+ exports.stripBOM = stripBOM;
37
+ exports.unquoteTomlValue = unquoteTomlValue;
38
+ exports.findDeveloperInstructionsBlockRange = findDeveloperInstructionsBlockRange;
39
+ exports.scanTomlLines = scanTomlLines;
40
+ exports.parseCodexAgentToml = parseCodexAgentToml;
41
+ exports.renderCodexAgentToml = renderCodexAgentToml;
42
+ exports.stripModel = stripModel;
43
+ exports.stripReasoningEffort = stripReasoningEffort;
44
+ /** Frozen reason enum for a failed {@link parseCodexAgentToml}. */
45
+ exports.PARSE_REASON = Object.freeze({
46
+ UNTERMINATED_BLOCK: 'unterminated_block',
47
+ });
48
+ // The UTF-8 BOM codepoint, spelled as an escape rather than the literal
49
+ // character so the source file never carries an invisible codepoint.
50
+ const BOM_CHAR = String.fromCharCode(0xfeff);
51
+ // Strips a leading UTF-8 BOM (U+FEFF), which fs.readFileSync(..., 'utf8') does not
52
+ // strip on its own, and unwraps a TOML basic/literal string value's surrounding
53
+ // quotes so `model = "sonnet"` yields `sonnet`, not `"sonnet"`.
54
+ function stripBOM(content) {
55
+ return content.charCodeAt(0) === 0xfeff ? content.slice(1) : content;
56
+ }
57
+ function unquoteTomlValue(rawValue) {
58
+ const trimmed = rawValue.trim();
59
+ const quoted = trimmed.match(/^"([^"]*)"/) ?? trimmed.match(/^'([^']*)'/);
60
+ return quoted ? quoted[1] : trimmed;
61
+ }
62
+ // The `developer_instructions` block is a TOML multi-line literal string
63
+ // (`developer_instructions = '''...'''`) that `generateCodexAgentToml` always
64
+ // emits after the header fields. Prompt prose inside that block discusses models
65
+ // constantly, so a `model = ...`-shaped line inside it must never be read as a
66
+ // live pin — but the block can legally appear anywhere in the file (a
67
+ // hand-reordered agent can move `model` after it), and another key's *value* can
68
+ // legally contain the literal text `developer_instructions = '''` (e.g. a
69
+ // `description` field quoting it) without that being the real block opener. So
70
+ // instead of truncating the file at the first textual occurrence of the marker
71
+ // anywhere in the content, this locates the block by its anchored line-start
72
+ // opener (`^\s*developer_instructions\s*=\s*'''`, never a mid-line/mid-value
73
+ // match) and its closing `'''` line, and excludes only the lines between them —
74
+ // every other line in the file, before AND after the block, is scanned.
75
+ //
76
+ // If no opener is found, nothing is excluded (the whole file is scanned) and
77
+ // `terminated` is trivially true. If the block IS opened but never closed before
78
+ // EOF (malformed file), `terminated` is false: the lenient reader (scanTomlLines)
79
+ // still treats the rest of the file as inside the block (the safe direction for
80
+ // a reader — see module header comment); the strict writer (parseCodexAgentToml)
81
+ // reads `terminated` and refuses instead. The emitter always uses `'''` (a TOML
82
+ // literal string), never a `"""` basic multi-line string, so only `'''` is
83
+ // treated as the block delimiter here.
84
+ function findDeveloperInstructionsBlockRange(lines) {
85
+ const openIndex = lines.findIndex((line) => /^\s*developer_instructions\s*=\s*'''/.test(line));
86
+ if (openIndex === -1) {
87
+ return { start: -1, end: -1, terminated: true };
88
+ }
89
+ const afterOpenMarker = lines[openIndex].replace(/^\s*developer_instructions\s*=\s*'''/, '');
90
+ if (afterOpenMarker.includes("'''")) {
91
+ // Same-line block: developer_instructions = '''one line'''
92
+ return { start: openIndex, end: openIndex, terminated: true };
93
+ }
94
+ for (let i = openIndex + 1; i < lines.length; i++) {
95
+ if (lines[i].includes("'''")) {
96
+ return { start: openIndex, end: i, terminated: true };
97
+ }
98
+ }
99
+ return { start: openIndex, end: lines.length - 1, terminated: false };
100
+ }
101
+ // Line-oriented scan of every line OUTSIDE the `developer_instructions` block
102
+ // (see findDeveloperInstructionsBlockRange). Full-key-name anchoring —
103
+ // `^([A-Za-z_][\w]*)\s*=` for a bare key, or `^"([^"]*)"\s*=` / `^'([^']*)'\s*=`
104
+ // for TOML's legal quoted-key forms, normalized to the same key name — means
105
+ // `model_verbosity` / `model_reasoning_effort` never satisfy a `model` probe,
106
+ // and vice versa; `#`-prefixed lines (after trimming leading whitespace) are
107
+ // treated as comments, never live pins. Shared by both scanTomlLines (the
108
+ // lenient reader, boolean-only for reasoning effort) and parseCodexAgentToml
109
+ // (the strict writer, which also needs the effort's value and both keys' line
110
+ // indices so stripModel/stripReasoningEffort can remove exactly one line).
111
+ function scanHeaderLines(lines, blockStart, blockEnd) {
112
+ let model = null;
113
+ let modelLineIndex = null;
114
+ let reasoningEffort = null;
115
+ let reasoningEffortLineIndex = null;
116
+ for (let i = 0; i < lines.length; i++) {
117
+ if (blockStart !== -1 && i >= blockStart && i <= blockEnd)
118
+ continue;
119
+ const trimmed = lines[i].trim();
120
+ if (trimmed === '' || trimmed.startsWith('#'))
121
+ continue;
122
+ const match = trimmed.match(/^(?:"([^"]*)"|'([^']*)'|([A-Za-z_][\w]*))\s*=\s*(.*)$/);
123
+ if (!match)
124
+ continue;
125
+ const key = match[1] ?? match[2] ?? match[3];
126
+ const rawValue = match[4];
127
+ if (key === 'model') {
128
+ model = unquoteTomlValue(rawValue);
129
+ modelLineIndex = i;
130
+ }
131
+ else if (key === 'model_reasoning_effort') {
132
+ reasoningEffort = unquoteTomlValue(rawValue);
133
+ reasoningEffortLineIndex = i;
134
+ }
135
+ }
136
+ return { model, modelLineIndex, reasoningEffort, reasoningEffortLineIndex };
137
+ }
138
+ /**
139
+ * The LENIENT reader entry point (Phase 2, moved verbatim in behavior). Never
140
+ * fails: an unterminated block falls back to "rest of file is inside the
141
+ * block" via {@link findDeveloperInstructionsBlockRange}'s own fallback.
142
+ * `content` is expected already BOM-stripped (callers pass `stripBOM(raw)`).
143
+ */
144
+ function scanTomlLines(content) {
145
+ const lines = content.split(/\r?\n/);
146
+ const { start, end } = findDeveloperInstructionsBlockRange(lines);
147
+ const { model, reasoningEffort } = scanHeaderLines(lines, start, end);
148
+ return { model, hasReasoningEffort: reasoningEffort !== null };
149
+ }
150
+ // Splits `content` into `{lines, terminators}` where `terminators[i]` is the
151
+ // terminator that FOLLOWS `lines[i]` (`'\r\n'`, `'\r'`, `'\n'`, or `''` for a
152
+ // line with none — only possible as the file's last line). The two arrays are
153
+ // always the same length and there is NEVER a phantom trailing entry: a
154
+ // source ending in a terminator (the common case) yields exactly as many
155
+ // lines as it has content lines, not one more. `render` is then a plain
156
+ // `lines[i] + terminators[i]` concatenation with no special-casing of "the
157
+ // last line" — see `renderCodexAgentToml`.
158
+ //
159
+ // `String#split` with a capturing group interleaves the delimiters into the
160
+ // result array — `"a\r\nb\nc".split(/(\r\n|\r|\n)/)` yields
161
+ // `["a","\r\n","b","\n","c"]` — so even indices are line content and odd
162
+ // indices are that line's terminator. When `content` ends WITH a terminator,
163
+ // `split` appends one extra empty-string element after the last real
164
+ // terminator (e.g. `"a\n".split(...)` → `["a","\n",""]`); that trailing `""`
165
+ // is not a real line, it is `split`'s "nothing after the last delimiter"
166
+ // marker, so the loop below stops before consuming it instead of recording it
167
+ // as a phantom empty final line (the defect this replaced — see A29: a doc
168
+ // with a phantom last element made every removal rule reason about the wrong
169
+ // element for any trailing-newline-terminated file, the common case). `\r\n`
170
+ // is tried before the bare `\r` alternative so a CRLF is never misread as a
171
+ // lone-CR line followed by an empty LF-terminated line.
172
+ function splitPreservingTerminators(content) {
173
+ if (content === '')
174
+ return { lines: [], terminators: [] };
175
+ const parts = content.split(/(\r\n|\r|\n)/);
176
+ const lastIndex = parts.length - 1;
177
+ const lines = [];
178
+ const terminators = [];
179
+ for (let i = 0; i < parts.length; i += 2) {
180
+ if (i === lastIndex && parts[i] === '')
181
+ break; // split's post-terminator marker, not a real line
182
+ lines.push(parts[i]);
183
+ terminators.push(parts[i + 1] ?? '');
184
+ }
185
+ return { lines, terminators };
186
+ }
187
+ /**
188
+ * The STRICT parse entry point (Phase 3, the writer's half of the
189
+ * reconciliation). Returns `{ok:false, reason:UNTERMINATED_BLOCK}` rather than
190
+ * guessing when the `developer_instructions` block is opened but never closed.
191
+ * On success, `doc` carries enough (the original `lines`/`terminators`, BOM/
192
+ * trailing-newline flags, and the two resolved values with their line indices)
193
+ * for {@link renderCodexAgentToml} to reproduce the source byte-identically —
194
+ * including a source with mixed line-ending styles — and for
195
+ * {@link stripModel}/{@link stripReasoningEffort} to remove exactly one line
196
+ * and its own terminator.
197
+ */
198
+ function parseCodexAgentToml(content) {
199
+ const hadBOM = content.charCodeAt(0) === 0xfeff;
200
+ const stripped = stripBOM(content);
201
+ // Informational only — see CodexAgentDoc.eol's docstring. Never used by
202
+ // renderCodexAgentToml.
203
+ const eol = stripped.includes('\r\n') ? '\r\n' : '\n';
204
+ const trailingNewline = /(\r\n|\r|\n)$/.test(stripped);
205
+ const { lines, terminators } = splitPreservingTerminators(stripped);
206
+ const { start, end, terminated } = findDeveloperInstructionsBlockRange(lines);
207
+ if (start !== -1 && !terminated) {
208
+ return { ok: false, reason: exports.PARSE_REASON.UNTERMINATED_BLOCK };
209
+ }
210
+ const { model, modelLineIndex, reasoningEffort, reasoningEffortLineIndex } = scanHeaderLines(lines, start, end);
211
+ const doc = {
212
+ lines,
213
+ terminators,
214
+ eol,
215
+ hadBOM,
216
+ trailingNewline,
217
+ blockRange: { start, end },
218
+ model,
219
+ modelLineIndex,
220
+ reasoningEffort,
221
+ reasoningEffortLineIndex,
222
+ };
223
+ return { ok: true, doc };
224
+ }
225
+ /**
226
+ * Renders `doc` back to a string. For an unmodified doc this is
227
+ * byte-identical to the original `parseCodexAgentToml` input (matrix row
228
+ * A14) — it never re-derives line content, only rejoins each line with its
229
+ * OWN recorded terminator (`terminators[i]`, never the whole-file `eol`) and
230
+ * re-prepends a BOM if one was present. This is a plain concatenation of the
231
+ * surviving `[line, terminator]` pieces, so a source with mixed `\r\n`/`\n`/
232
+ * lone-`\r` line endings round-trips exactly, and a strip
233
+ * ({@link stripModel}/{@link stripReasoningEffort}) removes only the target
234
+ * line and its own terminator — every other line's ending is untouched.
235
+ */
236
+ function renderCodexAgentToml(doc) {
237
+ let body = '';
238
+ for (let i = 0; i < doc.lines.length; i++) {
239
+ body += doc.lines[i] + (doc.terminators[i] ?? '');
240
+ }
241
+ return doc.hadBOM ? BOM_CHAR + body : body;
242
+ }
243
+ // Removes exactly one line (by index) — AND its own terminator — from
244
+ // `doc.lines`/`doc.terminators`, re-indexing the block range and the OTHER
245
+ // key's line index so a subsequent strip/render still sees a consistent doc.
246
+ // Never touches any other line's content or terminator.
247
+ //
248
+ // The one exception is when `index` names the file's LAST line: a plain
249
+ // slice-out would drop the removed line's terminator but leave the
250
+ // *previous* line's terminator standing in its place, which silently
251
+ // invents (or drops) a trailing newline the source never had — a middle-line
252
+ // removal never has this problem because the terminator that survives (the
253
+ // one that WAS between the previous line and the removed one) is exactly the
254
+ // terminator the new neighbors should have between them. For a last-line
255
+ // removal, the file's trailing-newline-or-not status lives in whether the
256
+ // REMOVED line's own terminator was empty (that is what `trailingNewline`
257
+ // was computed from) — so the new last line inherits the removed line's
258
+ // EMPTINESS only: if the removed terminator was `''`, the new last line's
259
+ // terminator is cleared to `''` too. If the removed terminator was
260
+ // non-empty, the source already ended with a newline and the new last line
261
+ // already has the right one (its OWN, unchanged) — overwriting it with the
262
+ // removed line's terminator would silently change the new last line's own
263
+ // ending style on a mixed-EOL source (see A26). Removing the only remaining
264
+ // line is the degenerate case: there is no new last line, so the result is
265
+ // the empty document.
266
+ function removeLine(doc, index, which) {
267
+ const isLastLine = index === doc.lines.length - 1;
268
+ let lines;
269
+ let terminators;
270
+ if (doc.lines.length === 1) {
271
+ lines = [];
272
+ terminators = [];
273
+ }
274
+ else if (isLastLine) {
275
+ lines = doc.lines.slice(0, index);
276
+ terminators = doc.terminators.slice(0, index);
277
+ // Inherit the removed line's EMPTINESS, never its STYLE: if the removed
278
+ // line had no terminator (the source had no trailing newline), the new
279
+ // last line's terminator becomes '' too. Otherwise the source DID end
280
+ // with a newline, and the new last line already has the right one — its
281
+ // OWN terminator (already carried over by the slice above), which may
282
+ // differ in style from the removed line's (a mixed-EOL source) — so it is
283
+ // left unchanged rather than overwritten.
284
+ if (doc.terminators[index] === '') {
285
+ terminators[terminators.length - 1] = '';
286
+ }
287
+ }
288
+ else {
289
+ lines = doc.lines.slice(0, index).concat(doc.lines.slice(index + 1));
290
+ terminators = doc.terminators.slice(0, index).concat(doc.terminators.slice(index + 1));
291
+ }
292
+ const reindex = (i) => (i === null ? null : i > index ? i - 1 : i);
293
+ const blockRange = { ...doc.blockRange };
294
+ if (blockRange.start !== -1) {
295
+ if (blockRange.start > index)
296
+ blockRange.start -= 1;
297
+ if (blockRange.end > index)
298
+ blockRange.end -= 1;
299
+ }
300
+ return {
301
+ ...doc,
302
+ lines,
303
+ terminators,
304
+ blockRange,
305
+ model: which === 'model' ? null : doc.model,
306
+ modelLineIndex: which === 'model' ? null : reindex(doc.modelLineIndex),
307
+ reasoningEffort: which === 'reasoningEffort' ? null : doc.reasoningEffort,
308
+ reasoningEffortLineIndex: which === 'reasoningEffort' ? null : reindex(doc.reasoningEffortLineIndex),
309
+ };
310
+ }
311
+ /**
312
+ * Returns a new doc with the `model` line removed (a no-op copy if there was
313
+ * no `model` line). Every other byte — comments, other keys, the
314
+ * `developer_instructions` block, line endings, BOM — is untouched.
315
+ */
316
+ function stripModel(doc) {
317
+ if (doc.modelLineIndex === null)
318
+ return doc;
319
+ return removeLine(doc, doc.modelLineIndex, 'model');
320
+ }
321
+ /**
322
+ * Returns a new doc with the `model_reasoning_effort` line removed (a no-op
323
+ * copy if there was none). Every other byte is untouched.
324
+ */
325
+ function stripReasoningEffort(doc) {
326
+ if (doc.reasoningEffortLineIndex === null)
327
+ return doc;
328
+ return removeLine(doc, doc.reasoningEffortLineIndex, 'reasoningEffort');
329
+ }