@gajae-code/coding-agent 0.11.11 → 0.12.1

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 (344) hide show
  1. package/CHANGELOG.md +131 -4
  2. package/dist/types/async/job-manager.d.ts +17 -3
  3. package/dist/types/cli/read-cli.d.ts +1 -0
  4. package/dist/types/commands/deep-interview.d.ts +9 -0
  5. package/dist/types/commands/harness.d.ts +22 -0
  6. package/dist/types/commands/read.d.ts +6 -0
  7. package/dist/types/config/model-registry.d.ts +4 -0
  8. package/dist/types/config/provider-auth-health.d.ts +14 -0
  9. package/dist/types/config/provider-ranking.d.ts +59 -0
  10. package/dist/types/config/settings-schema.d.ts +83 -1
  11. package/dist/types/config/settings.d.ts +2 -0
  12. package/dist/types/exa/mcp-client.d.ts +10 -1
  13. package/dist/types/exec/bash-executor.d.ts +45 -12
  14. package/dist/types/extensibility/extensions/types.d.ts +38 -4
  15. package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
  16. package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
  17. package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
  18. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
  19. package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
  20. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
  21. package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
  22. package/dist/types/extensibility/shared-events.d.ts +6 -0
  23. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  24. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  25. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  26. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  27. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  28. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  29. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  30. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +70 -1
  31. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  32. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  33. package/dist/types/gjc-runtime/team-runtime.d.ts +27 -1
  34. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  35. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
  36. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  37. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +17 -0
  38. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  39. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +31 -80
  40. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  41. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  42. package/dist/types/main.d.ts +1 -1
  43. package/dist/types/memories/index.d.ts +2 -0
  44. package/dist/types/modes/acp/acp-agent.d.ts +10 -19
  45. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  46. package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
  47. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  48. package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
  49. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  50. package/dist/types/modes/components/session-observer-overlay.d.ts +22 -0
  51. package/dist/types/modes/components/settings-selector.d.ts +11 -0
  52. package/dist/types/modes/components/status-line/gh.d.ts +5 -0
  53. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  54. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
  55. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  56. package/dist/types/modes/interactive-mode.d.ts +2 -0
  57. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  58. package/dist/types/modes/types.d.ts +2 -0
  59. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  60. package/dist/types/reminders/star-reminder.d.ts +3 -13
  61. package/dist/types/runtime-mcp/manager.d.ts +1 -0
  62. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  63. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  64. package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
  65. package/dist/types/sdk/broker/transport.d.ts +2 -0
  66. package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
  67. package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -1
  68. package/dist/types/sdk/bus/config.d.ts +6 -2
  69. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  70. package/dist/types/sdk/bus/index.d.ts +44 -1
  71. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +44 -0
  72. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  73. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  74. package/dist/types/sdk/bus/reconciliation-store.d.ts +69 -0
  75. package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
  76. package/dist/types/sdk/bus/rich-render.d.ts +2 -0
  77. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
  78. package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
  79. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -4
  80. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
  81. package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
  82. package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
  83. package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
  84. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  85. package/dist/types/sdk/host/host.d.ts +2 -0
  86. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  87. package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
  88. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  89. package/dist/types/sdk/prompt-status.d.ts +23 -3
  90. package/dist/types/sdk/session.d.ts +6 -0
  91. package/dist/types/session/agent-session.d.ts +82 -12
  92. package/dist/types/session/artifacts.d.ts +8 -0
  93. package/dist/types/session/blob-store.d.ts +57 -4
  94. package/dist/types/session/internal/managed-session-storage.d.ts +8 -1
  95. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  96. package/dist/types/session/session-manager.d.ts +48 -19
  97. package/dist/types/session/session-storage.d.ts +4 -0
  98. package/dist/types/session/streaming-output.d.ts +96 -12
  99. package/dist/types/setup/credential-import.d.ts +12 -0
  100. package/dist/types/skill-state/active-state.d.ts +12 -4
  101. package/dist/types/skill-state/workflow-hud.d.ts +9 -0
  102. package/dist/types/task/executor.d.ts +3 -2
  103. package/dist/types/task/index.d.ts +3 -1
  104. package/dist/types/task/provider-retry-status.d.ts +8 -0
  105. package/dist/types/task/receipt.d.ts +7 -1
  106. package/dist/types/task/types.d.ts +23 -0
  107. package/dist/types/tools/ask-answer-registry.d.ts +2 -2
  108. package/dist/types/tools/bash-interactive.d.ts +4 -1
  109. package/dist/types/tools/bash.d.ts +3 -1
  110. package/dist/types/tools/browser/launch.d.ts +7 -0
  111. package/dist/types/tools/fetch.d.ts +17 -1
  112. package/dist/types/tools/image-gen.d.ts +4 -0
  113. package/dist/types/tools/index.d.ts +4 -0
  114. package/dist/types/tools/output-meta.d.ts +44 -3
  115. package/dist/types/tools/path-utils.d.ts +1 -0
  116. package/dist/types/tools/read-internals.d.ts +12 -0
  117. package/dist/types/tools/read.d.ts +45 -7
  118. package/dist/types/tools/resource-gc.d.ts +4 -0
  119. package/dist/types/tools/review.d.ts +5 -7
  120. package/dist/types/tools/subagent.d.ts +2 -0
  121. package/dist/types/tools/tool-result.d.ts +7 -1
  122. package/dist/types/utils/gh.d.ts +13 -0
  123. package/dist/types/utils/image-resize.d.ts +1 -0
  124. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  125. package/dist/types/web/search/providers/searxng.d.ts +7 -0
  126. package/dist/types/web/search/providers/xai.d.ts +2 -0
  127. package/package.json +11 -13
  128. package/scripts/acp-conformance-agent.ts +218 -0
  129. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  130. package/scripts/capture-gjc-bundle-settings.ts +306 -0
  131. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  132. package/scripts/generate-sdk-operation-inventory.ts +5 -0
  133. package/scripts/resident-memory-bench.ts +1341 -0
  134. package/scripts/run-acp-conformance.ts +285 -0
  135. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  136. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  137. package/src/async/job-manager.ts +78 -20
  138. package/src/capability/index.ts +8 -0
  139. package/src/cli/fast-help.ts +1 -1
  140. package/src/cli/plugin-cli.ts +176 -18
  141. package/src/cli/read-cli.ts +2 -1
  142. package/src/cli.ts +1 -0
  143. package/src/commands/deep-interview.ts +29 -9
  144. package/src/commands/harness.ts +35 -12
  145. package/src/commands/read.ts +11 -2
  146. package/src/commands/team.ts +20 -2
  147. package/src/config/file-lock.ts +11 -2
  148. package/src/config/model-registry.ts +21 -2
  149. package/src/config/provider-auth-health.ts +42 -0
  150. package/src/config/provider-ranking.ts +119 -0
  151. package/src/config/settings-schema.ts +74 -1
  152. package/src/config/settings.ts +246 -108
  153. package/src/coordinator-mcp/question-gate-codec.ts +1 -1
  154. package/src/coordinator-mcp/server.ts +5 -12
  155. package/src/defaults/gjc/skills/deep-interview/SKILL.md +230 -82
  156. package/src/defaults/gjc/skills/ralplan/SKILL.md +77 -41
  157. package/src/defaults/gjc/skills/team/SKILL.md +3 -1
  158. package/src/defaults/gjc/skills/ultragoal/SKILL.md +62 -39
  159. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
  160. package/src/defaults/gjc-defaults.ts +4 -4
  161. package/src/eval/py/executor.ts +7 -2
  162. package/src/exa/mcp-client.ts +12 -3
  163. package/src/exec/bash-executor.ts +144 -27
  164. package/src/extensibility/extensions/runner.ts +15 -0
  165. package/src/extensibility/extensions/types.ts +46 -5
  166. package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
  167. package/src/extensibility/gjc-plugins/index.ts +19 -2
  168. package/src/extensibility/gjc-plugins/installer.ts +189 -50
  169. package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
  170. package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
  171. package/src/extensibility/gjc-plugins/observability.ts +3 -3
  172. package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
  173. package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
  174. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
  175. package/src/extensibility/gjc-plugins/schema.ts +84 -12
  176. package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
  177. package/src/extensibility/gjc-plugins/types.ts +141 -0
  178. package/src/extensibility/gjc-plugins/validation.ts +9 -0
  179. package/src/extensibility/shared-events.ts +5 -0
  180. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
  181. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  182. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  183. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  184. package/src/gjc-runtime/deep-interview-state.ts +12 -943
  185. package/src/gjc-runtime/gc-render.ts +1 -0
  186. package/src/gjc-runtime/gc-runtime.ts +11 -1
  187. package/src/gjc-runtime/launch-tmux.ts +25 -6
  188. package/src/gjc-runtime/managed-owner-admission.ts +1 -1
  189. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  190. package/src/gjc-runtime/ralplan-runtime.ts +1149 -98
  191. package/src/gjc-runtime/state-runtime.ts +2 -1
  192. package/src/gjc-runtime/state-writer.ts +70 -222
  193. package/src/gjc-runtime/team-launch.ts +42 -5
  194. package/src/gjc-runtime/team-runtime.ts +1085 -40
  195. package/src/gjc-runtime/team-store.ts +32 -0
  196. package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
  197. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  198. package/src/gjc-runtime/team-workers.ts +1 -0
  199. package/src/gjc-runtime/tmux-owner-isolation.ts +28 -0
  200. package/src/gjc-runtime/tmux-sessions.ts +58 -13
  201. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  202. package/src/gjc-runtime/ultragoal-runtime.ts +414 -618
  203. package/src/gjc-runtime/workflow-manifest.generated.json +105 -449
  204. package/src/gjc-runtime/workflow-manifest.ts +38 -247
  205. package/src/hooks/native-skill-hook.ts +17 -4
  206. package/src/hooks/skill-state.ts +1 -23
  207. package/src/internal-urls/docs-index.generated.ts +21 -22
  208. package/src/internal-urls/local-root-gc.ts +127 -0
  209. package/src/main.ts +9 -5
  210. package/src/memories/index.ts +10 -0
  211. package/src/modes/DESIGN.md +98 -0
  212. package/src/modes/acp/acp-agent.ts +339 -191
  213. package/src/modes/acp/acp-event-mapper.ts +14 -7
  214. package/src/modes/acp/acp-mode.ts +39 -3
  215. package/src/modes/components/assistant-message.ts +123 -22
  216. package/src/modes/components/gjc-bundle-settings.ts +508 -0
  217. package/src/modes/components/irc-sidebar.ts +176 -62
  218. package/src/modes/components/model-selector.ts +61 -4
  219. package/src/modes/components/notifications-settings-editor.ts +12 -1
  220. package/src/modes/components/oauth-selector.ts +96 -28
  221. package/src/modes/components/read-tool-group.ts +82 -14
  222. package/src/modes/components/session-observer-overlay.ts +457 -83
  223. package/src/modes/components/settings-selector.ts +116 -23
  224. package/src/modes/components/status-line/gh.ts +16 -0
  225. package/src/modes/components/tool-execution.ts +138 -27
  226. package/src/modes/components/tool-status-header.ts +3 -14
  227. package/src/modes/components/transcript-viewer-overlay.ts +99 -24
  228. package/src/modes/controllers/command-controller.ts +4 -0
  229. package/src/modes/controllers/event-controller.ts +109 -27
  230. package/src/modes/controllers/extension-ui-controller.ts +26 -2
  231. package/src/modes/controllers/input-controller.ts +18 -3
  232. package/src/modes/controllers/selector-controller.ts +127 -36
  233. package/src/modes/interactive-mode.ts +62 -3
  234. package/src/modes/irc-observation-ledger.ts +71 -15
  235. package/src/modes/runtime-init.ts +22 -2
  236. package/src/modes/types.ts +2 -0
  237. package/src/modes/utils/ui-helpers.ts +94 -1
  238. package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
  239. package/src/prompts/agents/architect.md +9 -1
  240. package/src/prompts/agents/critic.md +9 -0
  241. package/src/prompts/system/eager-todo.md +8 -4
  242. package/src/prompts/system/system-prompt.md +11 -5
  243. package/src/prompts/tools/bash.md +5 -5
  244. package/src/prompts/tools/read.md +4 -2
  245. package/src/prompts/tools/search-tool-bm25.md +5 -0
  246. package/src/prompts/tools/task.md +1 -1
  247. package/src/reminders/star-reminder.ts +4 -42
  248. package/src/runtime-mcp/manager.ts +19 -4
  249. package/src/runtime-mcp/smithery-auth.ts +17 -2
  250. package/src/runtime-mcp/smithery-connect.ts +11 -1
  251. package/src/sdk/acp/adapter.ts +22 -6
  252. package/src/sdk/broker/lifecycle.ts +25 -6
  253. package/src/sdk/broker/transport.ts +35 -3
  254. package/src/sdk/bus/attachment-registry.ts +5 -1
  255. package/src/sdk/bus/chat-daemon-cli.ts +4 -39
  256. package/src/sdk/bus/chat-daemon-control.ts +6 -3
  257. package/src/sdk/bus/config.ts +18 -2
  258. package/src/sdk/bus/control-drain-lease.ts +104 -0
  259. package/src/sdk/bus/index.ts +984 -146
  260. package/src/sdk/bus/kind-aware-reconciliation.ts +317 -0
  261. package/src/sdk/bus/lifecycle-commands.ts +4 -1
  262. package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
  263. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  264. package/src/sdk/bus/reconciliation-store.ts +303 -0
  265. package/src/sdk/bus/rich-draft.ts +1 -1
  266. package/src/sdk/bus/rich-render.ts +2 -2
  267. package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
  268. package/src/sdk/bus/telegram-cli.ts +33 -8
  269. package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
  270. package/src/sdk/bus/telegram-daemon-contract.ts +11 -4
  271. package/src/sdk/bus/telegram-daemon.ts +916 -23
  272. package/src/sdk/bus/telegram-reference.ts +40 -3
  273. package/src/sdk/bus/threaded-render.ts +48 -3
  274. package/src/sdk/bus/topic-registry.ts +20 -0
  275. package/src/sdk/host/control/dispatch.ts +5 -1
  276. package/src/sdk/host/control/operations.ts +1 -1
  277. package/src/sdk/host/host.ts +23 -1
  278. package/src/sdk/host/query/handlers.ts +55 -0
  279. package/src/sdk/host/query/revision-store.ts +36 -2
  280. package/src/sdk/host/reverse-leases.ts +44 -7
  281. package/src/sdk/mcp/server.ts +46 -18
  282. package/src/sdk/prompt-status.ts +16 -3
  283. package/src/sdk/protocol/operation-inventory.generated.json +51 -0
  284. package/src/sdk/protocol/operation-registry.ts +7 -2
  285. package/src/sdk/session.ts +101 -10
  286. package/src/session/agent-session.ts +1286 -220
  287. package/src/session/artifacts.ts +22 -0
  288. package/src/session/auth-broker-config.ts +10 -3
  289. package/src/session/blob-store.ts +915 -24
  290. package/src/session/internal/managed-session-scope.ts +44 -6
  291. package/src/session/internal/managed-session-storage.ts +48 -6
  292. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  293. package/src/session/session-manager.ts +2364 -438
  294. package/src/session/session-storage.ts +9 -1
  295. package/src/session/streaming-output.ts +500 -111
  296. package/src/setup/credential-import.ts +75 -13
  297. package/src/setup/provider-onboarding.ts +19 -4
  298. package/src/skill-state/active-state.ts +32 -10
  299. package/src/skill-state/workflow-hud.ts +42 -2
  300. package/src/skill-state/workflow-mutation-guard.ts +560 -8
  301. package/src/task/executor.ts +122 -39
  302. package/src/task/index.ts +23 -14
  303. package/src/task/provider-retry-status.ts +16 -0
  304. package/src/task/receipt.ts +23 -7
  305. package/src/task/render.ts +6 -2
  306. package/src/task/types.ts +108 -0
  307. package/src/tools/ask-answer-registry.ts +12 -6
  308. package/src/tools/ask.ts +169 -118
  309. package/src/tools/ast-edit.ts +1 -0
  310. package/src/tools/ast-grep.ts +1 -0
  311. package/src/tools/bash-interactive.ts +8 -3
  312. package/src/tools/bash.ts +503 -60
  313. package/src/tools/browser/launch.ts +41 -6
  314. package/src/tools/fetch.ts +114 -23
  315. package/src/tools/find.ts +1 -0
  316. package/src/tools/image-gen.ts +29 -5
  317. package/src/tools/index.ts +4 -0
  318. package/src/tools/output-meta.ts +281 -20
  319. package/src/tools/path-utils.ts +8 -3
  320. package/src/tools/read-internals.ts +44 -0
  321. package/src/tools/read.ts +1560 -310
  322. package/src/tools/resource-gc.ts +182 -18
  323. package/src/tools/review.ts +5 -7
  324. package/src/tools/search.ts +1 -0
  325. package/src/tools/subagent-render.ts +6 -2
  326. package/src/tools/subagent.ts +6 -0
  327. package/src/tools/telegram-send.ts +6 -3
  328. package/src/tools/tool-result.ts +14 -1
  329. package/src/utils/gh.ts +39 -0
  330. package/src/utils/image-resize.ts +3 -0
  331. package/src/web/search/providers/anthropic.ts +4 -4
  332. package/src/web/search/providers/codex.ts +1 -1
  333. package/src/web/search/providers/kimi.ts +15 -5
  334. package/src/web/search/providers/searxng.ts +47 -4
  335. package/src/web/search/providers/xai.ts +7 -2
  336. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  337. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  338. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  339. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
  340. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
  341. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  342. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  343. package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
  344. package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
@@ -1,6 +1,6 @@
1
1
  import * as crypto from "node:crypto";
2
- import * as os from "node:os";
3
2
  import * as path from "node:path";
3
+ import { getConfigRootDir } from "@gajae-code/utils";
4
4
  import type { WorkflowHudSummary } from "../skill-state/active-state";
5
5
  import { buildUltragoalHudSummary as buildWorkflowUltragoalHudSummary } from "../skill-state/workflow-hud";
6
6
  import { renderCliWriteReceipt } from "./cli-write-receipt";
@@ -19,6 +19,7 @@ import {
19
19
  computeCriticVerdictPlanGeneration,
20
20
  computeUltragoalPlanGeneration,
21
21
  countNonOkayTerminalCriticVerdicts,
22
+ finalAggregateReceiptMissingCriticOkay,
22
23
  findFreshBatchCloseReceipt,
23
24
  findLedgerReceiptEvent,
24
25
  isCleanPauseCriticVerdictShape,
@@ -85,47 +86,6 @@ export type UltragoalGoalStatus =
85
86
  | "review_blocked"
86
87
  | "superseded";
87
88
 
88
- export type UltragoalPipelineMetadataSource = "original_plan_graph" | "legacy_brief_only" | "steering";
89
- export type UltragoalPipelineOverlapState =
90
- | "none"
91
- | "open"
92
- | "joined_clean"
93
- | "blocked_disjoint_continue"
94
- | "quarantine_required"
95
- | "rebaseline_complete";
96
-
97
- export interface UltragoalPipelineTargets extends JsonObject {
98
- files: string[];
99
- surfaces: string[];
100
- }
101
-
102
- export interface UltragoalPipelineMetadata extends JsonObject {
103
- schemaVersion: 1;
104
- goalId: string;
105
- source: UltragoalPipelineMetadataSource;
106
- eligible: boolean;
107
- dependsOn: string[];
108
- independentOf: string[];
109
- targets: UltragoalPipelineTargets;
110
- metadataHash: string;
111
- overlap: UltragoalPipelineOverlapState;
112
- overlapId?: string;
113
- priorGoalId?: string;
114
- nextGoalId?: string;
115
- blockerFootprints?: UltragoalPipelineTargets[];
116
- invalidationReason?: string;
117
- invalidatedAt?: string;
118
- }
119
-
120
- export interface UltragoalGoalMetadataInput {
121
- schemaVersion: 1;
122
- goalId: string;
123
- source: UltragoalPipelineMetadataSource;
124
- dependsOn?: string[];
125
- independentOf?: string[];
126
- targets?: Partial<UltragoalPipelineTargets>;
127
- }
128
-
129
89
  export interface UltragoalValidationBatchMetadata extends JsonObject {
130
90
  schemaVersion: 1;
131
91
  batchId: string;
@@ -142,39 +102,6 @@ export interface UltragoalValidationBatchInput {
142
102
  finalGoalId: string;
143
103
  }
144
104
 
145
- export interface UltragoalPipelineOverlapHandles extends JsonObject {
146
- review: JsonObject;
147
- qa: JsonObject;
148
- implementation: JsonObject;
149
- }
150
-
151
- export interface UltragoalPipelineOverlapReceipt extends JsonObject {
152
- ok: true;
153
- event: string;
154
- overlap_id: string;
155
- prior_goal_id: string;
156
- next_goal_id?: string;
157
- goal_id?: string;
158
- status?: UltragoalPipelineOverlapState;
159
- next_goal_status?: UltragoalGoalStatus;
160
- goals_path: string;
161
- ledger_path: string;
162
- }
163
-
164
- export type UltragoalPipelineLedgerEventName =
165
- | "pipeline_overlap_started"
166
- | "pipeline_overlap_joined"
167
- | "pipeline_overlap_blocked"
168
- | "pipeline_overlap_quarantined"
169
- | "pipeline_overlap_rebaselined";
170
-
171
- export interface UltragoalPipelineLedgerEvent extends UltragoalLedgerEvent {
172
- event: UltragoalPipelineLedgerEventName;
173
- schemaVersion: 1;
174
- overlapId: string;
175
- priorGoalId: string;
176
- nextGoalId: string;
177
- }
178
105
  export interface UltragoalGoal {
179
106
  id: string;
180
107
  title: string;
@@ -187,7 +114,6 @@ export interface UltragoalGoal {
187
114
  evidence?: string;
188
115
  steering?: Record<string, unknown>;
189
116
  completionVerification?: UltragoalCompletionVerification;
190
- pipelineMetadata?: UltragoalPipelineMetadata;
191
117
  validationBatch?: UltragoalValidationBatchMetadata;
192
118
  }
193
119
 
@@ -322,7 +248,6 @@ export interface UltragoalStatusSummary {
322
248
  nudgeRemaining?: number;
323
249
  nudgeGoalId?: string;
324
250
  nudgeTargetKind?: UltragoalNudgeTargetKind;
325
- pipelineOverlap?: JsonObject;
326
251
  }
327
252
 
328
253
  export interface UltragoalCommandResult {
@@ -490,8 +415,7 @@ export async function resolveUltragoalNudgeBudget(cwd: string): Promise<{ budget
490
415
  const projectPath = path.join(gjcRoot(cwd), "settings.json");
491
416
  const project = await readSettingsNudgeBudget(projectPath);
492
417
  if (project !== null) return { budget: project, source: projectPath };
493
- const userDir = process.env.GJC_CONFIG_DIR?.trim() || path.join(os.homedir(), ".gjc");
494
- const userPath = path.join(userDir, "settings.json");
418
+ const userPath = path.join(getConfigRootDir(), "settings.json");
495
419
  const user = await readSettingsNudgeBudget(userPath);
496
420
  if (user !== null) return { budget: user, source: userPath };
497
421
  return { budget: DEFAULT_ULTRAGOAL_NUDGE_BUDGET, source: "default" };
@@ -742,61 +666,6 @@ export function stringArray(value: unknown): string[] | null {
742
666
  return Array.isArray(value) && value.every(item => typeof item === "string") ? value.map(item => item.trim()) : null;
743
667
  }
744
668
 
745
- function normalizePipelineStringArray(value: unknown, fieldName: string): string[] {
746
- const items = stringArray(value) ?? [];
747
- const filtered = items.filter(item => item.length > 0);
748
- if (items.length !== filtered.length) throw new Error(`${fieldName} must contain only non-empty strings`);
749
- return filtered;
750
- }
751
-
752
- function normalizePipelinePath(value: string, fieldName: string): string {
753
- const raw = value.trim();
754
- if (raw.split(/[\\/]+/).includes("..")) throw new Error(`${fieldName} contains unsafe path ${value}`);
755
- const normalized = normalizeRepoPath(raw);
756
- if (
757
- !normalized ||
758
- normalized.startsWith("../") ||
759
- normalized === ".." ||
760
- path.isAbsolute(normalized) ||
761
- normalized.includes("\0")
762
- ) {
763
- throw new Error(`${fieldName} contains unsafe path ${value}`);
764
- }
765
- return normalized;
766
- }
767
-
768
- function normalizePipelineTargets(value: unknown, fieldName: string): UltragoalPipelineTargets {
769
- const record = typeof value === "object" && value !== null && !Array.isArray(value) ? (value as JsonObject) : {};
770
- const files = normalizePipelineStringArray(record.files, `${fieldName}.files`).map(item =>
771
- normalizePipelinePath(item, `${fieldName}.files`),
772
- );
773
- const surfaces = normalizePipelineStringArray(record.surfaces, `${fieldName}.surfaces`).map(normalizeSurfaceToken);
774
- if (new Set(files).size !== files.length) throw new Error(`${fieldName}.files contains duplicate normalized paths`);
775
- if (new Set(surfaces).size !== surfaces.length)
776
- throw new Error(`${fieldName}.surfaces contains duplicate normalized surfaces`);
777
- return { files, surfaces };
778
- }
779
-
780
- function pipelineMetadataHashBasis(metadata: Omit<UltragoalPipelineMetadata, "metadataHash">): JsonObject {
781
- return {
782
- schemaVersion: metadata.schemaVersion,
783
- goalId: metadata.goalId,
784
- source: metadata.source,
785
- dependsOn: metadata.dependsOn,
786
- independentOf: metadata.independentOf,
787
- targets: metadata.targets,
788
- };
789
- }
790
-
791
- export function hashPipelineMetadata(metadata: Omit<UltragoalPipelineMetadata, "metadataHash">): string {
792
- return hashStructuredValue(pipelineMetadataHashBasis(metadata));
793
- }
794
-
795
- function withPipelineMetadataHash(
796
- metadata: Omit<UltragoalPipelineMetadata, "metadataHash">,
797
- ): UltragoalPipelineMetadata {
798
- return { ...metadata, metadataHash: hashPipelineMetadata(metadata) } as UltragoalPipelineMetadata;
799
- }
800
669
  function validationBatchHashBasis(metadata: Omit<UltragoalValidationBatchMetadata, "metadataHash">): JsonObject {
801
670
  return {
802
671
  schemaVersion: metadata.schemaVersion,
@@ -900,22 +769,12 @@ function normalizeSavedValidationBatch(record: unknown, id: string): UltragoalVa
900
769
  return normalized;
901
770
  }
902
771
 
903
- function pipelineMetadataConflictsWithValidationBatch(metadata: UltragoalPipelineMetadata | undefined): boolean {
904
- return metadata?.eligible === true || metadata?.source === "original_plan_graph" || metadata?.source === "steering";
905
- }
906
-
907
- export function validateValidationBatchPipelineExclusion(goal: UltragoalGoal): void {
908
- if (goal.validationBatch && pipelineMetadataConflictsWithValidationBatch(goal.pipelineMetadata)) {
909
- throw new Error(`Goal ${goal.id} cannot combine validationBatch with eligible pipeline metadata`);
910
- }
911
- }
912
772
  function requireFreshValidationBatchMetadata(goal: UltragoalGoal): UltragoalValidationBatchMetadata | undefined {
913
773
  const metadata = goal.validationBatch;
914
774
  if (!metadata) return undefined;
915
775
  const { metadataHash, ...basis } = metadata;
916
776
  if (metadataHash !== hashValidationBatch(basis))
917
777
  throw new Error(`Goal ${goal.id} has stale validation batch metadata hash`);
918
- validateValidationBatchPipelineExclusion(goal);
919
778
  return metadata;
920
779
  }
921
780
 
@@ -1048,245 +907,6 @@ function requireValidationBatchSteeringAllowed(
1048
907
  `steer ${kind} cannot invalidate validation batch ${metadata.batchId} while member ${blocker.id} has a fresh deferred receipt`,
1049
908
  );
1050
909
  }
1051
- function legacyPipelineMetadata(goalId: string): UltragoalPipelineMetadata {
1052
- const basis: Omit<UltragoalPipelineMetadata, "metadataHash"> = {
1053
- schemaVersion: 1,
1054
- goalId,
1055
- source: "legacy_brief_only",
1056
- eligible: false,
1057
- dependsOn: [],
1058
- independentOf: [],
1059
- targets: { files: [], surfaces: [] },
1060
- overlap: "none",
1061
- invalidationReason: "missing_pipeline_metadata",
1062
- };
1063
- return withPipelineMetadataHash(basis);
1064
- }
1065
-
1066
- function normalizePipelineMetadataRecord(value: unknown, goalIds: ReadonlySet<string>): UltragoalPipelineMetadata {
1067
- if (typeof value !== "object" || value === null || Array.isArray(value))
1068
- throw new Error("goal metadata rows must be objects");
1069
- const record = value as JsonObject;
1070
- if (record.schemaVersion !== 1) throw new Error("goal metadata schemaVersion must be 1");
1071
- const goalId = nonEmptyString(record.goalId);
1072
- if (!goalId || !goalIds.has(goalId)) throw new Error(`goal metadata references unknown goal id ${goalId ?? ""}`);
1073
- const source = record.source;
1074
- if (source !== "original_plan_graph" && source !== "legacy_brief_only" && source !== "steering") {
1075
- throw new Error("goal metadata source must be original_plan_graph, legacy_brief_only, or steering");
1076
- }
1077
- const dependsOn = normalizePipelineStringArray(record.dependsOn, `metadata ${goalId}.dependsOn`);
1078
- const independentOf = normalizePipelineStringArray(record.independentOf, `metadata ${goalId}.independentOf`);
1079
- if (dependsOn.includes(goalId) || independentOf.includes(goalId))
1080
- throw new Error(`goal metadata ${goalId} cannot reference itself`);
1081
- for (const id of [...dependsOn, ...independentOf]) {
1082
- if (!goalIds.has(id)) throw new Error(`goal metadata ${goalId} references unknown goal id ${id}`);
1083
- }
1084
- if (dependsOn.some(id => independentOf.includes(id)))
1085
- throw new Error(`goal metadata ${goalId} has dependency/independence conflict`);
1086
- const targets = requireNonEmptyPipelineTargets(record.targets, `metadata ${goalId}.targets`);
1087
- const basis: Omit<UltragoalPipelineMetadata, "metadataHash"> = {
1088
- schemaVersion: 1,
1089
- goalId,
1090
- source,
1091
- eligible: false,
1092
- dependsOn,
1093
- independentOf,
1094
- targets,
1095
- overlap: "none",
1096
- };
1097
- return withPipelineMetadataHash(basis);
1098
- }
1099
-
1100
- export function targetsAreDisjoint(left: UltragoalPipelineTargets, right: UltragoalPipelineTargets): boolean {
1101
- return (
1102
- left.files.every(file => !right.files.includes(file)) &&
1103
- left.surfaces.every(surface => !right.surfaces.includes(surface))
1104
- );
1105
- }
1106
-
1107
- export function targetsOverlap(left: UltragoalPipelineTargets, right: UltragoalPipelineTargets): boolean {
1108
- return (
1109
- left.files.some(file => right.files.includes(file)) ||
1110
- left.surfaces.some(surface => right.surfaces.includes(surface))
1111
- );
1112
- }
1113
-
1114
- export function requireNonEmptyPipelineTargets(value: unknown, fieldName: string): UltragoalPipelineTargets {
1115
- const targets = normalizePipelineTargets(value, fieldName);
1116
- if (targets.files.length === 0 && targets.surfaces.length === 0)
1117
- throw new Error(`${fieldName} requires files or surfaces`);
1118
- return targets;
1119
- }
1120
-
1121
- export function collectPipelineBlockerFootprints(result: JsonObject, fieldName: string): UltragoalPipelineTargets[] {
1122
- const raw = Array.isArray(result.blockers)
1123
- ? result.blockers
1124
- : Array.isArray(result.blockerFootprints)
1125
- ? result.blockerFootprints
1126
- : [];
1127
- return raw.map((item, index) => {
1128
- const record = requireJsonObjectValue(item, `${fieldName}.blockers[${index}]`);
1129
- const footprint = typeof record.footprint === "object" && record.footprint !== null ? record.footprint : record;
1130
- return requireNonEmptyPipelineTargets(footprint, `${fieldName}.blockers[${index}].footprint`);
1131
- });
1132
- }
1133
-
1134
- function pipelineTargetsCoverPath(targets: UltragoalPipelineTargets, filePath: string): boolean {
1135
- const normalized = normalizeRepoPath(filePath);
1136
- return targets.files.some(target => normalized === target || normalized.startsWith(`${target}/`));
1137
- }
1138
-
1139
- export function pipelinePeer(plan: UltragoalPlan, metadata: UltragoalPipelineMetadata): UltragoalGoal | undefined {
1140
- const peerId = metadata.goalId === metadata.priorGoalId ? metadata.nextGoalId : metadata.priorGoalId;
1141
- return peerId ? plan.goals.find(goal => goal.id === peerId) : undefined;
1142
- }
1143
-
1144
- export function handleIdsFromValue(value: JsonObject | JsonObject[], fieldName: string): string[] {
1145
- const records = Array.isArray(value) ? value : [value];
1146
- const ids = records.map(
1147
- (record, index) =>
1148
- nonEmptyString(record.id) ??
1149
- nonEmptyString(record.handleId) ??
1150
- nonEmptyString(record.name) ??
1151
- `${fieldName}-${index}`,
1152
- );
1153
- if (ids.some(id => id.length === 0)) throw new Error(`${fieldName} handles require ids`);
1154
- return ids;
1155
- }
1156
-
1157
- export function resultHandleIds(value: JsonObject, fieldName: string): string[] {
1158
- const ids = stringArray(value.handleIds) ?? stringArray(value.handles) ?? [];
1159
- if (ids.length === 0) throw new Error(`${fieldName} requires handleIds`);
1160
- return ids;
1161
- }
1162
-
1163
- export function requireCoveredHandles(expected: readonly string[], actual: readonly string[], fieldName: string): void {
1164
- const missing = expected.filter(id => !actual.includes(id));
1165
- if (missing.length > 0) throw new Error(`${fieldName} is missing handle coverage for ${missing.join(", ")}`);
1166
- }
1167
-
1168
- function validatePipelineEligibility(metadata: UltragoalPipelineMetadata[]): UltragoalPipelineMetadata[] {
1169
- const byId = new Map(metadata.map(item => [item.goalId, item]));
1170
- return metadata.map(item => {
1171
- const invalidationReasons: string[] = [];
1172
- if (item.source !== "original_plan_graph") invalidationReasons.push("not_original_plan_graph");
1173
- if (item.targets.files.length === 0 && item.targets.surfaces.length === 0)
1174
- invalidationReasons.push("empty_targets");
1175
- for (const otherId of item.independentOf) {
1176
- const other = byId.get(otherId);
1177
- if (!other?.independentOf.includes(item.goalId))
1178
- invalidationReasons.push(`missing_symmetric_independence:${otherId}`);
1179
- if (other && !targetsAreDisjoint(item.targets, other.targets))
1180
- invalidationReasons.push(`shared_targets:${otherId}`);
1181
- }
1182
- const eligible = invalidationReasons.length === 0;
1183
- return {
1184
- ...item,
1185
- eligible,
1186
- ...(eligible ? {} : { invalidationReason: invalidationReasons.join(",") || "ineligible" }),
1187
- };
1188
- });
1189
- }
1190
-
1191
- function parseGoalMetadataInput(value: unknown, goalIds: ReadonlySet<string>): UltragoalPipelineMetadata[] {
1192
- if (!Array.isArray(value)) throw new Error("goal metadata JSON must be an array");
1193
- const seen = new Set<string>();
1194
- const metadata = value.map(row => {
1195
- const item = normalizePipelineMetadataRecord(row, goalIds);
1196
- if (seen.has(item.goalId)) throw new Error(`duplicate goal metadata for ${item.goalId}`);
1197
- seen.add(item.goalId);
1198
- return item;
1199
- });
1200
- return validatePipelineEligibility(metadata);
1201
- }
1202
- function normalizeSavedPipelineMetadata(value: unknown, goalId: string): UltragoalPipelineMetadata | undefined {
1203
- if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
1204
- const record = value as JsonObject;
1205
- const source =
1206
- record.source === "original_plan_graph" || record.source === "legacy_brief_only" || record.source === "steering"
1207
- ? record.source
1208
- : "legacy_brief_only";
1209
- const overlap =
1210
- record.overlap === "open" ||
1211
- record.overlap === "joined_clean" ||
1212
- record.overlap === "blocked_disjoint_continue" ||
1213
- record.overlap === "quarantine_required" ||
1214
- record.overlap === "rebaseline_complete"
1215
- ? record.overlap
1216
- : "none";
1217
- const basis: Omit<UltragoalPipelineMetadata, "metadataHash"> = {
1218
- schemaVersion: 1,
1219
- goalId,
1220
- source,
1221
- eligible: record.eligible === true,
1222
- dependsOn: normalizePipelineStringArray(record.dependsOn, `metadata ${goalId}.dependsOn`),
1223
- independentOf: normalizePipelineStringArray(record.independentOf, `metadata ${goalId}.independentOf`),
1224
- targets: normalizePipelineTargets(record.targets, `metadata ${goalId}.targets`),
1225
- overlap,
1226
- ...(nonEmptyString(record.overlapId) ? { overlapId: nonEmptyString(record.overlapId)! } : {}),
1227
- ...(nonEmptyString(record.priorGoalId) ? { priorGoalId: nonEmptyString(record.priorGoalId)! } : {}),
1228
- ...(nonEmptyString(record.nextGoalId) ? { nextGoalId: nonEmptyString(record.nextGoalId)! } : {}),
1229
- ...(Array.isArray(record.blockerFootprints)
1230
- ? {
1231
- blockerFootprints: record.blockerFootprints.map((item, index) =>
1232
- normalizePipelineTargets(item, `metadata ${goalId}.blockerFootprints[${index}]`),
1233
- ),
1234
- }
1235
- : {}),
1236
- ...(nonEmptyString(record.invalidationReason)
1237
- ? { invalidationReason: nonEmptyString(record.invalidationReason)! }
1238
- : {}),
1239
- ...(nonEmptyString(record.invalidatedAt) ? { invalidatedAt: nonEmptyString(record.invalidatedAt)! } : {}),
1240
- };
1241
- return {
1242
- ...basis,
1243
- metadataHash: nonEmptyString(record.metadataHash) ?? hashPipelineMetadata(basis),
1244
- } as UltragoalPipelineMetadata;
1245
- }
1246
-
1247
- function currentPipelineHash(metadata: UltragoalPipelineMetadata): string {
1248
- return hashPipelineMetadata(metadata);
1249
- }
1250
-
1251
- export function requireFreshPipelineMetadata(goal: UltragoalGoal): UltragoalPipelineMetadata {
1252
- const metadata = goal.pipelineMetadata;
1253
- if (!metadata) throw new Error(`Goal ${goal.id} has no pipeline metadata`);
1254
- if (metadata.metadataHash !== currentPipelineHash(metadata))
1255
- throw new Error(`Goal ${goal.id} has stale pipeline metadata hash`);
1256
- return metadata;
1257
- }
1258
-
1259
- export function openPipelineOverlap(
1260
- plan: UltragoalPlan,
1261
- ): { prior: UltragoalGoal; next: UltragoalGoal; overlapId: string } | null {
1262
- const openGoals = plan.goals.filter(goal => goal.pipelineMetadata?.overlap === "open");
1263
- if (openGoals.length === 0) return null;
1264
- const overlapId = openGoals[0]?.pipelineMetadata?.overlapId;
1265
- if (!overlapId) return null;
1266
- const peers = openGoals.filter(goal => goal.pipelineMetadata?.overlapId === overlapId);
1267
- if (peers.length !== 2) return null;
1268
- const prior = peers[0];
1269
- const next = peers[1];
1270
- if (!prior || !next) return null;
1271
- return { prior, next, overlapId };
1272
- }
1273
-
1274
- function invalidatePipelineMetadata(goal: UltragoalGoal, reason: string, now: string): void {
1275
- const basis: Omit<UltragoalPipelineMetadata, "metadataHash"> = {
1276
- schemaVersion: 1,
1277
- goalId: goal.id,
1278
- source: goal.pipelineMetadata?.source ?? "steering",
1279
- eligible: false,
1280
- dependsOn: goal.pipelineMetadata?.dependsOn ?? [],
1281
- independentOf: goal.pipelineMetadata?.independentOf ?? [],
1282
- targets: goal.pipelineMetadata?.targets ?? { files: [], surfaces: [] },
1283
- overlap: "none",
1284
- invalidationReason: reason,
1285
- invalidatedAt: now,
1286
- };
1287
- goal.pipelineMetadata = withPipelineMetadataHash(basis);
1288
- }
1289
-
1290
910
  function normalizeGoalStatus(value: unknown): UltragoalGoalStatus {
1291
911
  switch (value) {
1292
912
  case "pending":
@@ -1327,7 +947,6 @@ function normalizePlan(raw: unknown): UltragoalPlan {
1327
947
  const title = nonEmptyString(goalRecord.title) ?? id;
1328
948
  const objective = nonEmptyString(goalRecord.objective) ?? title;
1329
949
  const goalCreatedAt = nonEmptyString(goalRecord.createdAt) ?? createdAt;
1330
- const pipelineMetadata = normalizeSavedPipelineMetadata(goalRecord.pipelineMetadata, id);
1331
950
  const validationBatch = normalizeSavedValidationBatch(goalRecord.validationBatch, id);
1332
951
  return {
1333
952
  ...goalRecord,
@@ -1348,7 +967,6 @@ function normalizePlan(raw: unknown): UltragoalPlan {
1348
967
  typeof goalRecord.completionVerification === "object" && goalRecord.completionVerification !== null
1349
968
  ? (goalRecord.completionVerification as UltragoalCompletionVerification)
1350
969
  : undefined,
1351
- pipelineMetadata,
1352
970
  validationBatch,
1353
971
  };
1354
972
  });
@@ -1411,7 +1029,6 @@ export async function getUltragoalStatus(cwd: string, sessionId?: string | null)
1411
1029
  if (!plan) return { exists: false, status: "missing", paths, counts, goals: [] };
1412
1030
  for (const goal of plan.goals) counts[goal.status] += 1;
1413
1031
  const currentGoal = plan.goals.find(goal => SCHEDULABLE_STATUSES.has(goal.status));
1414
- const overlap = openPipelineOverlap(plan);
1415
1032
  let status: UltragoalStatusSummary["status"] = "pending";
1416
1033
  if (plan.goals.length > 0 && plan.goals.every(goal => TERMINAL_OR_SKIPPED_STATUSES.has(goal.status)))
1417
1034
  status = "complete";
@@ -1441,16 +1058,6 @@ export async function getUltragoalStatus(cwd: string, sessionId?: string | null)
1441
1058
  counts,
1442
1059
  goals: plan.goals,
1443
1060
  ...nudgeFields,
1444
- ...(overlap
1445
- ? {
1446
- pipelineOverlap: {
1447
- overlapId: overlap.overlapId,
1448
- priorGoalId: overlap.prior.id,
1449
- nextGoalId: overlap.next.id,
1450
- status: overlap.next.pipelineMetadata?.overlap,
1451
- },
1452
- }
1453
- : {}),
1454
1061
  };
1455
1062
  }
1456
1063
  export function buildUltragoalHudSummary(
@@ -1525,8 +1132,6 @@ export async function createUltragoalPlan(input: {
1525
1132
  brief: string;
1526
1133
  gjcGoalMode?: UltragoalGjcGoalMode;
1527
1134
  sessionId?: string | null;
1528
- goalMetadata?: UltragoalGoalMetadataInput[];
1529
- goalMetadataJson?: string;
1530
1135
  validationBatches?: UltragoalValidationBatchInput[];
1531
1136
  validationBatchJson?: string;
1532
1137
  }): Promise<UltragoalPlan> {
@@ -1545,31 +1150,18 @@ export async function createUltragoalPlan(input: {
1545
1150
  updatedAt: now,
1546
1151
  }));
1547
1152
  const goalIds = new Set(goals.map(goal => goal.id));
1548
- const metadataInput = input.goalMetadataJson
1549
- ? await readStructuredValue(input.cwd, input.goalMetadataJson)
1550
- : input.goalMetadata;
1551
1153
  const validationBatchInput = input.validationBatchJson
1552
1154
  ? await readStructuredValue(input.cwd, input.validationBatchJson)
1553
1155
  : input.validationBatches;
1554
- if (metadataInput !== undefined && validationBatchInput !== undefined) {
1555
- const metadataRows = Array.isArray(metadataInput) ? metadataInput : [];
1556
- const batchRows = Array.isArray(validationBatchInput) ? validationBatchInput : [];
1557
- if (metadataRows.length > 0 && batchRows.length > 0)
1558
- throw new Error("validation-batch-json and goal-metadata-json are mutually exclusive");
1559
- }
1560
- const metadata = metadataInput === undefined ? [] : parseGoalMetadataInput(metadataInput, goalIds);
1561
1156
  const validationBatches =
1562
1157
  validationBatchInput === undefined
1563
1158
  ? []
1564
1159
  : parseValidationBatchInput(validationBatchInput, goalIds, input.gjcGoalMode ?? "aggregate");
1565
- const metadataByGoalId = new Map(metadata.map(item => [item.goalId, item]));
1566
1160
  const validationBatchByGoalId = new Map<string, UltragoalValidationBatchMetadata>();
1567
1161
  for (const batch of validationBatches)
1568
1162
  for (const memberId of batch.memberIds) validationBatchByGoalId.set(memberId, batch);
1569
1163
  for (const goal of goals) {
1570
- goal.pipelineMetadata = metadataByGoalId.get(goal.id) ?? legacyPipelineMetadata(goal.id);
1571
1164
  goal.validationBatch = validationBatchByGoalId.get(goal.id);
1572
- validateValidationBatchPipelineExclusion(goal);
1573
1165
  }
1574
1166
  const repositoryBinding = await captureRepositoryBinding(input.cwd, {
1575
1167
  displayPath: input.cwd,
@@ -1604,41 +1196,6 @@ export interface UltragoalRunCompletionState {
1604
1196
  hasBlockers: boolean;
1605
1197
  needsFinalAggregateReceipt: boolean;
1606
1198
  }
1607
- export function requireJsonObjectValue(value: unknown, fieldName: string): JsonObject {
1608
- if (typeof value !== "object" || value === null || Array.isArray(value))
1609
- throw new Error(`${fieldName} must be an object`);
1610
- if (Object.keys(value).length === 0) throw new Error(`${fieldName} must be non-empty`);
1611
- return value as JsonObject;
1612
- }
1613
-
1614
- export function requireJsonObjectOrArrayValue(value: unknown, fieldName: string): JsonObject | JsonObject[] {
1615
- if (Array.isArray(value)) {
1616
- if (value.length === 0) throw new Error(`${fieldName} must be non-empty`);
1617
- return value.map((item, index) => requireJsonObjectValue(item, `${fieldName}[${index}]`));
1618
- }
1619
- return requireJsonObjectValue(value, fieldName);
1620
- }
1621
-
1622
- async function readRequiredJsonObject(cwd: string, value: string, fieldName: string): Promise<JsonObject> {
1623
- return requireJsonObjectValue(await readStructuredValue(cwd, value), fieldName);
1624
- }
1625
-
1626
- async function readRequiredJsonObjectOrArray(
1627
- cwd: string,
1628
- value: string,
1629
- fieldName: string,
1630
- ): Promise<JsonObject | JsonObject[]> {
1631
- return requireJsonObjectOrArrayValue(await readStructuredValue(cwd, value), fieldName);
1632
- }
1633
-
1634
- import {
1635
- joinUltragoalPipelineOverlap,
1636
- rebaselineUltragoalPipelineOverlap,
1637
- startUltragoalPipelineOverlap,
1638
- } from "./ultragoal-pipeline";
1639
-
1640
- export { joinUltragoalPipelineOverlap, rebaselineUltragoalPipelineOverlap, startUltragoalPipelineOverlap };
1641
-
1642
1199
  export function getUltragoalRunCompletionState(
1643
1200
  plan: UltragoalPlan,
1644
1201
  options: { retryFailed?: boolean } = {},
@@ -1767,6 +1324,72 @@ export function nonEmptyStringArray(value: unknown): string[] | null {
1767
1324
  return strings.length === value.length && strings.length > 0 ? strings : null;
1768
1325
  }
1769
1326
 
1327
+ export interface UltragoalQualityGateDiagnostic {
1328
+ path: string;
1329
+ code: string;
1330
+ message: string;
1331
+ }
1332
+
1333
+ /**
1334
+ * Collects every quality-gate defect in one pass instead of throwing on the first.
1335
+ * Authoring a valid gate is otherwise an edit/retry loop at the most expensive phase
1336
+ * of a run (#3474). The aggregate error message keeps each individual message verbatim
1337
+ * so existing callers and assertions that match on a single message still work, and
1338
+ * `diagnostics` carries the machine-readable stable `path` + `code` pairs.
1339
+ */
1340
+ export class UltragoalQualityGateError extends Error {
1341
+ readonly diagnostics: readonly UltragoalQualityGateDiagnostic[];
1342
+ constructor(diagnostics: readonly UltragoalQualityGateDiagnostic[]) {
1343
+ super(diagnostics.map(diagnostic => diagnostic.message).join("\n"));
1344
+ this.name = "UltragoalQualityGateError";
1345
+ this.diagnostics = diagnostics;
1346
+ }
1347
+ }
1348
+
1349
+ class QualityGateDiagnostics {
1350
+ private readonly collected: UltragoalQualityGateDiagnostic[] = [];
1351
+
1352
+ /**
1353
+ * Runs one independent check. A thrown error is recorded and swallowed so later
1354
+ * checks still run; unrelated defects therefore surface together.
1355
+ */
1356
+ check(path: string, code: string, run: () => void): boolean {
1357
+ try {
1358
+ run();
1359
+ return true;
1360
+ } catch (error) {
1361
+ this.add(path, code, error instanceof Error ? error.message : String(error));
1362
+ return false;
1363
+ }
1364
+ }
1365
+
1366
+ async checkAsync(path: string, code: string, run: () => Promise<void>): Promise<boolean> {
1367
+ try {
1368
+ await run();
1369
+ return true;
1370
+ } catch (error) {
1371
+ this.add(path, code, error instanceof Error ? error.message : String(error));
1372
+ return false;
1373
+ }
1374
+ }
1375
+
1376
+ add(path: string, code: string, message: string): void {
1377
+ this.collected.push({ path, code, message });
1378
+ }
1379
+
1380
+ get empty(): boolean {
1381
+ return this.collected.length === 0;
1382
+ }
1383
+
1384
+ get diagnostics(): readonly UltragoalQualityGateDiagnostic[] {
1385
+ return this.collected;
1386
+ }
1387
+
1388
+ throwIfAny(): void {
1389
+ if (this.collected.length > 0) throw new UltragoalQualityGateError(this.collected);
1390
+ }
1391
+ }
1392
+
1770
1393
  function requireNonEmptyString(value: unknown, fieldName: string): void {
1771
1394
  if (typeof value !== "string" || value.trim().length === 0) {
1772
1395
  throw new Error(`qualityGate ${fieldName} must be a non-empty string`);
@@ -2561,10 +2184,56 @@ function requireValidationBatchTuple(
2561
2184
  }
2562
2185
  }
2563
2186
 
2187
+ const DEFERRABLE_REVIEW_LANES = new Set(["architectReview", "executorQa"]);
2188
+ const DECLARABLE_DEFERRED_LANES: Record<string, string> = {
2189
+ targetedVerification: "targetedVerification",
2190
+ aiSlopCleaner: "aiSlopCleaner",
2191
+ iteration: "iteration",
2192
+ };
2193
+
2194
+ /**
2195
+ * Declaration-vs-evidence check. Per-subgoal enforcement is relaxed: the agent
2196
+ * chooses which verification lanes to run and declares them in `ranLanes`. The
2197
+ * runtime does not dictate that set, but it fails closed when the declaration and
2198
+ * the submitted evidence disagree in either direction, so a declaration can never
2199
+ * be cheaper than the proof behind it.
2200
+ */
2201
+ function validateDeferredLaneDeclaration(deferred: JsonObject, fieldName: string): void {
2202
+ const declared = stringArray(deferred.ranLanes)?.filter(lane => lane.length > 0);
2203
+ // `ranLanes` is optional for compatibility with gates that only carry the
2204
+ // mandatory targeted-verification lane; an explicit empty array is a claim
2205
+ // that nothing ran and is rejected, because targetedVerification is required.
2206
+ if (declared === undefined) return;
2207
+ const declaredSet = new Set(declared);
2208
+ if (declaredSet.size !== declared.length) throw new Error(`${fieldName}.ranLanes must not repeat a lane`);
2209
+ for (const lane of declaredSet) {
2210
+ if (DEFERRABLE_REVIEW_LANES.has(lane)) {
2211
+ throw new Error(
2212
+ `${fieldName}.ranLanes cannot declare ${lane}: a deferred gate structurally cannot carry review-lane evidence, so it is deferred to the boundary`,
2213
+ );
2214
+ }
2215
+ const evidenceKey = DECLARABLE_DEFERRED_LANES[lane];
2216
+ if (!evidenceKey) throw new Error(`${fieldName}.ranLanes contains unknown lane ${lane}`);
2217
+ const laneRecord = qualityGateObject(deferred[evidenceKey]);
2218
+ if (!laneRecord || !nonEmptyString(laneRecord.evidence)) {
2219
+ throw new Error(`${fieldName}.ranLanes declares ${lane} but ${fieldName}.${evidenceKey}.evidence is missing`);
2220
+ }
2221
+ }
2222
+ if (!declaredSet.has("targetedVerification")) {
2223
+ throw new Error(`${fieldName}.ranLanes must declare targetedVerification`);
2224
+ }
2225
+ for (const [lane, evidenceKey] of Object.entries(DECLARABLE_DEFERRED_LANES)) {
2226
+ if (declaredSet.has(lane)) continue;
2227
+ if (qualityGateObject(deferred[evidenceKey])) {
2228
+ throw new Error(`${fieldName}.${evidenceKey} is present but ${lane} is not declared in ${fieldName}.ranLanes`);
2229
+ }
2230
+ }
2231
+ }
2232
+
2564
2233
  function validateDeferredCompletionQualityGate(
2565
2234
  gate: JsonObject,
2566
2235
  goal: UltragoalGoal,
2567
- metadata: UltragoalValidationBatchMetadata,
2236
+ metadata: UltragoalValidationBatchMetadata | undefined,
2568
2237
  changeSet?: UltragoalChangeSet,
2569
2238
  ): void {
2570
2239
  const allowedKeys = new Set(["deferredToBatch"]);
@@ -2575,9 +2244,11 @@ function validateDeferredCompletionQualityGate(
2575
2244
  if (!deferred) throw new Error("deferred qualityGate requires deferredToBatch object");
2576
2245
  if (deferred.kind !== "validation-batch-deferred")
2577
2246
  throw new Error("deferredToBatch.kind must be validation-batch-deferred");
2578
- requireValidationBatchTuple(metadata, deferred, "deferredToBatch");
2579
- if (goal.id === metadata.finalGoalId)
2580
- throw new Error("final validation batch goal cannot use deferredToBatch quality gate");
2247
+ if (metadata) {
2248
+ requireValidationBatchTuple(metadata, deferred, "deferredToBatch");
2249
+ if (goal.id === metadata.finalGoalId)
2250
+ throw new Error("final validation batch goal cannot use deferredToBatch quality gate");
2251
+ }
2581
2252
  const deferredLanes = stringArray(deferred.deferredLanes)?.filter(Boolean).sort();
2582
2253
  if (deferredLanes?.join(",") !== "architectReview,executorQa")
2583
2254
  throw new Error("deferredToBatch.deferredLanes must be architectReview and executorQa");
@@ -2585,16 +2256,23 @@ function validateDeferredCompletionQualityGate(
2585
2256
  if (!targeted || targeted.status !== PASSED_STATUS || !nonEmptyStringArray(targeted.commands))
2586
2257
  throw new Error("deferredToBatch.targetedVerification must pass with non-empty commands");
2587
2258
  requireNonEmptyString(targeted.evidence, "deferredToBatch.targetedVerification.evidence");
2259
+ // The ai-slop-cleaner pass and a full verification rerun are no longer
2260
+ // mandatory per subgoal; they are boundary duties. When either is supplied it
2261
+ // must still be internally consistent and blocker-free.
2588
2262
  const cleaner = qualityGateObject(deferred.aiSlopCleaner);
2589
- if (!cleaner || cleaner.status !== PASSED_STATUS) throw new Error("deferredToBatch.aiSlopCleaner must pass");
2590
- requireNonEmptyString(cleaner.evidence, "deferredToBatch.aiSlopCleaner.evidence");
2263
+ if (cleaner) {
2264
+ if (cleaner.status !== PASSED_STATUS) throw new Error("deferredToBatch.aiSlopCleaner must pass when present");
2265
+ requireNonEmptyString(cleaner.evidence, "deferredToBatch.aiSlopCleaner.evidence");
2266
+ }
2591
2267
  const iteration = qualityGateObject(deferred.iteration);
2592
- if (!iteration || iteration.status !== PASSED_STATUS || iteration.fullRerun !== true)
2593
- throw new Error("deferredToBatch.iteration must pass with fullRerun true");
2594
- if (!nonEmptyStringArray(iteration.rerunCommands))
2595
- throw new Error("deferredToBatch.iteration.rerunCommands must be non-empty");
2596
- requireNonEmptyString(iteration.evidence, "deferredToBatch.iteration.evidence");
2597
- requireEmptyBlockers(iteration.blockers, "deferredToBatch.iteration.blockers");
2268
+ if (iteration) {
2269
+ if (iteration.status !== PASSED_STATUS) throw new Error("deferredToBatch.iteration must pass when present");
2270
+ if (!nonEmptyStringArray(iteration.rerunCommands))
2271
+ throw new Error("deferredToBatch.iteration.rerunCommands must be non-empty");
2272
+ requireNonEmptyString(iteration.evidence, "deferredToBatch.iteration.evidence");
2273
+ requireEmptyBlockers(iteration.blockers, "deferredToBatch.iteration.blockers");
2274
+ }
2275
+ validateDeferredLaneDeclaration(deferred, "deferredToBatch");
2598
2276
  const declaredChangeSet = qualityGateObject(deferred.changeSet);
2599
2277
  if (!declaredChangeSet) throw new Error("deferredToBatch.changeSet is required");
2600
2278
  if (declaredChangeSet.memberGoalId !== goal.id)
@@ -2606,6 +2284,80 @@ function validateDeferredCompletionQualityGate(
2606
2284
  if (declaredChangeSet.changeSetHash !== changeSetHashForPaths(paths))
2607
2285
  throw new Error("deferredToBatch.changeSet.changeSetHash does not match declared paths");
2608
2286
  }
2287
+ const COHORT_LANE_KEYS = ["cleaner", "architect", "qa"] as const;
2288
+
2289
+ /**
2290
+ * Frozen-source-hash review cohort (#3473). One boundary generation runs at most one
2291
+ * cleaner, one architect, and one QA lane, every lane verdict is bound to the same
2292
+ * immutable source hash, and findings must be joined before any repair starts. Later
2293
+ * generations are delta-only. Cohort state rides the existing `iteration` gate key so
2294
+ * no new top-level quality-gate key is introduced.
2295
+ */
2296
+ function validateReviewCohort(gate: JsonObject, iteration: JsonObject): void {
2297
+ const cohort = qualityGateObject(iteration.reviewCohort);
2298
+ if (!cohort) throw new Error("qualityGate iteration.reviewCohort is required at the review boundary");
2299
+ const generation = cohort.reviewGeneration;
2300
+ if (typeof generation !== "number" || !Number.isInteger(generation) || generation < 1)
2301
+ throw new Error("iteration.reviewCohort.reviewGeneration must be an integer >= 1");
2302
+ const sourceHash = nonEmptyString(cohort.sourceHash);
2303
+ if (!sourceHash) throw new Error("iteration.reviewCohort.sourceHash is required");
2304
+ if (cohort.joined !== true)
2305
+ throw new Error("iteration.reviewCohort.joined must be true: all lane findings must join before checkpoint");
2306
+ const lanes = qualityGateObject(cohort.lanes);
2307
+ if (!lanes) throw new Error("iteration.reviewCohort.lanes is required");
2308
+ const unsupportedLanes = Object.keys(lanes).filter(key => !(COHORT_LANE_KEYS as readonly string[]).includes(key));
2309
+ if (unsupportedLanes.length > 0)
2310
+ throw new Error(`iteration.reviewCohort.lanes contains unsupported lanes: ${unsupportedLanes.join(", ")}`);
2311
+ for (const lane of COHORT_LANE_KEYS) {
2312
+ if (Array.isArray(lanes[lane]))
2313
+ throw new Error(`iteration.reviewCohort.lanes.${lane} must be one lane per generation, not a list`);
2314
+ const record = qualityGateObject(lanes[lane]);
2315
+ if (!record) throw new Error(`iteration.reviewCohort.lanes.${lane} is required`);
2316
+ const laneHash = nonEmptyString(record.sourceHash);
2317
+ if (!laneHash) throw new Error(`iteration.reviewCohort.lanes.${lane}.sourceHash is required`);
2318
+ if (laneHash !== sourceHash)
2319
+ throw new Error(
2320
+ `iteration.reviewCohort.lanes.${lane}.sourceHash does not match the frozen cohort sourceHash: every lane must inspect the same immutable source`,
2321
+ );
2322
+ if (record.status !== "CLEAR" && record.status !== PASSED_STATUS)
2323
+ throw new Error(`iteration.reviewCohort.lanes.${lane}.status must be CLEAR or passed`);
2324
+ requireNonEmptyString(record.evidence, `iteration.reviewCohort.lanes.${lane}.evidence`);
2325
+ requireEmptyBlockers(record.blockers, `iteration.reviewCohort.lanes.${lane}.blockers`);
2326
+ }
2327
+ // Generation 1 is the full cohort review; every later generation exists only
2328
+ // because one consolidated blocker batch produced it, so its scope is the delta.
2329
+ if (generation > 1) {
2330
+ if (cohort.deltaOnly !== true)
2331
+ throw new Error("iteration.reviewCohort.deltaOnly must be true for reviewGeneration > 1");
2332
+ requireNonEmptyString(cohort.priorGenerationSourceHash, "iteration.reviewCohort.priorGenerationSourceHash");
2333
+ if (nonEmptyString(cohort.priorGenerationSourceHash) === sourceHash)
2334
+ throw new Error(
2335
+ "iteration.reviewCohort.priorGenerationSourceHash must differ from sourceHash: a new generation requires a new frozen source",
2336
+ );
2337
+ const deltaPaths = stringArray(cohort.deltaPaths)?.filter(path => path.length > 0);
2338
+ if (!deltaPaths || deltaPaths.length === 0)
2339
+ throw new Error("iteration.reviewCohort.deltaPaths must be non-empty for reviewGeneration > 1");
2340
+ const expansion = qualityGateObject(cohort.scopeExpansion);
2341
+ if (expansion) {
2342
+ requireNonEmptyString(expansion.severity, "iteration.reviewCohort.scopeExpansion.severity");
2343
+ requireNonEmptyString(expansion.novelty, "iteration.reviewCohort.scopeExpansion.novelty");
2344
+ requireNonEmptyString(expansion.justification, "iteration.reviewCohort.scopeExpansion.justification");
2345
+ }
2346
+ } else if (cohort.deltaOnly === true) {
2347
+ throw new Error("iteration.reviewCohort.deltaOnly cannot be true for the first reviewGeneration");
2348
+ }
2349
+ // The terminal critic is one verdict on the final joined generation, never a
2350
+ // per-lane or per-generation vote.
2351
+ const critic = qualityGateObject(gate.criticReview);
2352
+ if (critic) {
2353
+ const criticHash = nonEmptyString(critic.sourceHash);
2354
+ if (criticHash && criticHash !== sourceHash)
2355
+ throw new Error(
2356
+ "criticReview.sourceHash must match the final joined cohort sourceHash: the terminal critic runs once on the terminal generation",
2357
+ );
2358
+ }
2359
+ }
2360
+
2609
2361
  async function validateCompletionQualityGate(
2610
2362
  cwd: string,
2611
2363
  gate: JsonObject,
@@ -2622,8 +2374,26 @@ async function validateCompletionQualityGate(
2622
2374
  ? chooseReceiptKind(options.plan, options.ledger, options.goal, "complete")
2623
2375
  : undefined;
2624
2376
  const isFinalAggregate = receiptKind === "final-aggregate";
2377
+ // Every independent defect is collected instead of thrown, so one validate run
2378
+ // reports the whole list rather than forcing an edit/retry loop per field (#3474).
2379
+ const found = new QualityGateDiagnostics();
2625
2380
  if (batchMode && options.goal && options.goal.id !== batchMode.finalGoalId) {
2626
- validateDeferredCompletionQualityGate(gate, options.goal, batchMode, options.changeSet);
2381
+ found.check("deferredToBatch", "deferred_gate_invalid", () => {
2382
+ validateDeferredCompletionQualityGate(gate, options.goal!, batchMode, options.changeSet);
2383
+ });
2384
+ found.throwIfAny();
2385
+ return;
2386
+ }
2387
+ // Boundary-by-default: in aggregate mode every checkpoint before the run's
2388
+ // final boundary may present the lightweight deferred gate, so heavyweight
2389
+ // architect/QA review runs once per boundary instead of once per story. The
2390
+ // boundary is derived from `chooseReceiptKind` rather than from synthesized
2391
+ // durable batch metadata, which would restale/deadlock on appended goals.
2392
+ if (!batchMode && options.goal && receiptKind === "per-goal" && qualityGateObject(gate.deferredToBatch)) {
2393
+ found.check("deferredToBatch", "deferred_gate_invalid", () => {
2394
+ validateDeferredCompletionQualityGate(gate, options.goal!, undefined, options.changeSet);
2395
+ });
2396
+ found.throwIfAny();
2627
2397
  return;
2628
2398
  }
2629
2399
  if (batchMode && options.goal && options.goal.id === batchMode.finalGoalId) {
@@ -2636,13 +2406,22 @@ async function validateCompletionQualityGate(
2636
2406
  ]);
2637
2407
  const unsupportedKeys = Object.keys(gate).filter(key => !allowedKeys.has(key));
2638
2408
  if (unsupportedKeys.length > 0)
2639
- throw new Error(`qualityGate contains unsupported keys: ${unsupportedKeys.join(", ")}`);
2409
+ found.add(
2410
+ "qualityGate",
2411
+ "unsupported_keys",
2412
+ `qualityGate contains unsupported keys: ${unsupportedKeys.join(", ")}`,
2413
+ );
2640
2414
  if (!qualityGateObject(gate.validationBatchClose))
2641
- throw new Error("final validation batch goal requires validationBatchClose");
2415
+ found.add(
2416
+ "validationBatchClose",
2417
+ "missing_validation_batch_close",
2418
+ "final validation batch goal requires validationBatchClose",
2419
+ );
2642
2420
  }
2643
- const codeReview = qualityGateObject(gate.codeReview);
2644
- if (codeReview) {
2645
- throw new Error(
2421
+ if (qualityGateObject(gate.codeReview)) {
2422
+ found.add(
2423
+ "codeReview",
2424
+ "legacy_code_review_gate",
2646
2425
  "checkpoint --status complete requires architect review approval through architectReview, executorQa, and iteration quality-gate evidence; legacy codeReview-only gates are not sufficient",
2647
2426
  );
2648
2427
  }
@@ -2653,13 +2432,23 @@ async function validateCompletionQualityGate(
2653
2432
  );
2654
2433
  const unsupportedKeys = Object.keys(gate).filter(key => !allowedKeys.has(key));
2655
2434
  if (unsupportedKeys.length > 0) {
2656
- throw new Error(`qualityGate contains unsupported keys: ${unsupportedKeys.join(", ")}`);
2435
+ found.add(
2436
+ "qualityGate",
2437
+ "unsupported_keys",
2438
+ `qualityGate contains unsupported keys: ${unsupportedKeys.join(", ")}`,
2439
+ );
2657
2440
  }
2658
2441
  const architectReview = qualityGateObject(gate.architectReview);
2659
2442
  const executorQa = qualityGateObject(gate.executorQa);
2660
2443
  const iteration = qualityGateObject(gate.iteration);
2661
2444
  if (!architectReview || !executorQa || !iteration) {
2662
- throw new Error("qualityGate requires architectReview, executorQa, and iteration objects");
2445
+ found.add(
2446
+ "qualityGate",
2447
+ "missing_required_sections",
2448
+ "qualityGate requires architectReview, executorQa, and iteration objects",
2449
+ );
2450
+ found.throwIfAny();
2451
+ return;
2663
2452
  }
2664
2453
  if (isFinalAggregate) {
2665
2454
  if (
@@ -2667,18 +2456,28 @@ async function validateCompletionQualityGate(
2667
2456
  terminalCriticCeilingReached(options.ledger) &&
2668
2457
  !terminalCriticGateOverridden(options.ledger)
2669
2458
  ) {
2670
- throw new Error(
2459
+ found.add(
2460
+ "criticReview",
2461
+ "terminal_critic_ceiling",
2671
2462
  "checkpoint --status complete blocked: terminal-critic ceiling reached; requires human/leader gjc ultragoal record-critic-gate-override before completion",
2672
2463
  );
2673
2464
  }
2674
2465
  const criticReview = qualityGateObject(gate.criticReview);
2675
2466
  if (criticReview?.verdict !== "OKAY") {
2676
- throw new Error(
2467
+ found.add(
2468
+ "criticReview.verdict",
2469
+ "critic_verdict_not_okay",
2677
2470
  "checkpoint --status complete (final aggregate) requires criticReview with verdict OKAY, non-empty evidence, and empty blockers",
2678
2471
  );
2679
2472
  }
2680
- requireNonEmptyString(criticReview.evidence, "criticReview.evidence");
2681
- requireEmptyBlockers(criticReview.blockers, "criticReview.blockers");
2473
+ if (criticReview) {
2474
+ found.check("criticReview.evidence", "missing_evidence", () =>
2475
+ requireNonEmptyString(criticReview.evidence, "criticReview.evidence"),
2476
+ );
2477
+ found.check("criticReview.blockers", "non_empty_blockers", () =>
2478
+ requireEmptyBlockers(criticReview.blockers, "criticReview.blockers"),
2479
+ );
2480
+ }
2682
2481
  }
2683
2482
  if (
2684
2483
  architectReview.architectureStatus !== CLEAN_ARCHITECT_STATUS ||
@@ -2686,39 +2485,75 @@ async function validateCompletionQualityGate(
2686
2485
  architectReview.codeStatus !== CLEAN_ARCHITECT_STATUS ||
2687
2486
  architectReview.recommendation !== APPROVE_RECOMMENDATION
2688
2487
  ) {
2689
- throw new Error(
2488
+ found.add(
2489
+ "architectReview",
2490
+ "architect_not_clear",
2690
2491
  "checkpoint --status complete requires architect review approval: architectReview architecture/product/code must be CLEAR and recommendation must be APPROVE",
2691
2492
  );
2692
2493
  }
2693
2494
  if (!nonEmptyStringArray(architectReview.commands)) {
2694
- throw new Error("qualityGate architectReview.commands must be a non-empty string array");
2495
+ found.add(
2496
+ "architectReview.commands",
2497
+ "missing_command_array",
2498
+ "qualityGate architectReview.commands must be a non-empty string array",
2499
+ );
2695
2500
  }
2696
- requireNonEmptyString(architectReview.evidence, "architectReview.evidence");
2697
- requireEmptyBlockers(architectReview.blockers, "architectReview.blockers");
2501
+ found.check("architectReview.evidence", "missing_evidence", () =>
2502
+ requireNonEmptyString(architectReview.evidence, "architectReview.evidence"),
2503
+ );
2504
+ found.check("architectReview.blockers", "non_empty_blockers", () =>
2505
+ requireEmptyBlockers(architectReview.blockers, "architectReview.blockers"),
2506
+ );
2698
2507
  if (
2699
2508
  executorQa.status !== PASSED_STATUS ||
2700
2509
  executorQa.e2eStatus !== PASSED_STATUS ||
2701
2510
  executorQa.redTeamStatus !== PASSED_STATUS
2702
2511
  ) {
2703
- throw new Error("qualityGate executorQa status, e2eStatus, and redTeamStatus must be passed");
2512
+ found.add(
2513
+ "executorQa",
2514
+ "executor_qa_not_passed",
2515
+ "qualityGate executorQa status, e2eStatus, and redTeamStatus must be passed",
2516
+ );
2704
2517
  }
2705
2518
  if (!nonEmptyStringArray(executorQa.e2eCommands) || !nonEmptyStringArray(executorQa.redTeamCommands)) {
2706
- throw new Error("qualityGate executorQa e2eCommands and redTeamCommands must be non-empty string arrays");
2519
+ found.add(
2520
+ "executorQa.e2eCommands",
2521
+ "missing_command_array",
2522
+ "qualityGate executorQa e2eCommands and redTeamCommands must be non-empty string arrays",
2523
+ );
2707
2524
  }
2708
- requireNonEmptyString(executorQa.evidence, "executorQa.evidence");
2709
- requireEmptyBlockers(executorQa.blockers, "executorQa.blockers");
2710
- await validateExecutorQaRedTeamEvidence(cwd, executorQa, { changeSet: options.changeSet });
2525
+ found.check("executorQa.evidence", "missing_evidence", () =>
2526
+ requireNonEmptyString(executorQa.evidence, "executorQa.evidence"),
2527
+ );
2528
+ found.check("executorQa.blockers", "non_empty_blockers", () =>
2529
+ requireEmptyBlockers(executorQa.blockers, "executorQa.blockers"),
2530
+ );
2531
+ await found.checkAsync("executorQa", "executor_qa_evidence_invalid", () =>
2532
+ validateExecutorQaRedTeamEvidence(cwd, executorQa, { changeSet: options.changeSet }),
2533
+ );
2711
2534
  if (iteration.status !== PASSED_STATUS || iteration.fullRerun !== true) {
2712
- throw new Error("qualityGate iteration must be passed with fullRerun true");
2535
+ found.add("iteration", "iteration_not_passed", "qualityGate iteration must be passed with fullRerun true");
2713
2536
  }
2714
2537
  if (!nonEmptyStringArray(iteration.rerunCommands)) {
2715
- throw new Error("qualityGate iteration.rerunCommands must be a non-empty string array");
2538
+ found.add(
2539
+ "iteration.rerunCommands",
2540
+ "missing_command_array",
2541
+ "qualityGate iteration.rerunCommands must be a non-empty string array",
2542
+ );
2716
2543
  }
2717
- requireNonEmptyString(iteration.evidence, "iteration.evidence");
2718
- requireEmptyBlockers(iteration.blockers, "iteration.blockers");
2544
+ found.check("iteration.evidence", "missing_evidence", () =>
2545
+ requireNonEmptyString(iteration.evidence, "iteration.evidence"),
2546
+ );
2547
+ found.check("iteration.blockers", "non_empty_blockers", () =>
2548
+ requireEmptyBlockers(iteration.blockers, "iteration.blockers"),
2549
+ );
2550
+ found.check("iteration.reviewCohort", "review_cohort_invalid", () => validateReviewCohort(gate, iteration));
2719
2551
  if (batchMode && options.goal && options.plan && options.ledger) {
2720
- validateBatchCloseQualityGate(gate, options.plan, batchMode, options.ledger, options.changeSet);
2552
+ found.check("validationBatchClose", "batch_close_invalid", () =>
2553
+ validateBatchCloseQualityGate(gate, options.plan!, batchMode, options.ledger!, options.changeSet),
2554
+ );
2721
2555
  }
2556
+ found.throwIfAny();
2722
2557
  }
2723
2558
 
2724
2559
  function validateBatchCloseQualityGate(
@@ -2940,6 +2775,56 @@ function hydrateReviewedBatchReplacementClose(input: {
2940
2775
  },
2941
2776
  };
2942
2777
  }
2778
+ /**
2779
+ * Read-only quality-gate validation (#3474). Applies exactly the same rules as
2780
+ * `checkpoint --status complete` — including deferred-vs-boundary gate selection and
2781
+ * artifact existence checks — but never touches `goals.json`, `ledger.jsonl`, or goal
2782
+ * state, and reports every diagnostic in one run instead of the first failure.
2783
+ */
2784
+ export async function validateUltragoalQualityGateReadOnly(input: {
2785
+ cwd: string;
2786
+ qualityGateJson: string;
2787
+ goalId?: string;
2788
+ sessionId?: string | null;
2789
+ }): Promise<{ valid: boolean; errors: readonly UltragoalQualityGateDiagnostic[] }> {
2790
+ const sessionId = input.sessionId?.trim() || currentUltragoalSessionId(input.cwd);
2791
+ const plan = await readUltragoalPlan(input.cwd, sessionId);
2792
+ const goal = input.goalId
2793
+ ? plan?.goals.find(item => item.id === input.goalId)
2794
+ : plan?.goals.find(item => SCHEDULABLE_STATUSES.has(item.status));
2795
+ if (input.goalId && !goal) {
2796
+ return {
2797
+ valid: false,
2798
+ errors: [{ path: "goalId", code: "unknown_goal", message: `Unknown ultragoal goal ${input.goalId}` }],
2799
+ };
2800
+ }
2801
+ const gate = qualityGateObject(await readStructuredValue(input.cwd, input.qualityGateJson));
2802
+ if (!gate) {
2803
+ return {
2804
+ valid: false,
2805
+ errors: [{ path: "qualityGate", code: "not_an_object", message: "qualityGate must be a JSON object" }],
2806
+ };
2807
+ }
2808
+ const ledger = plan ? await readUltragoalLedger(input.cwd, sessionId) : undefined;
2809
+ const changeSet = await computeCheckpointChangeSet(input.cwd);
2810
+ try {
2811
+ await validateCompletionQualityGate(input.cwd, gate, { changeSet, plan: plan ?? undefined, goal, ledger });
2812
+ return { valid: true, errors: [] };
2813
+ } catch (error) {
2814
+ if (error instanceof UltragoalQualityGateError) return { valid: false, errors: error.diagnostics };
2815
+ return {
2816
+ valid: false,
2817
+ errors: [
2818
+ {
2819
+ path: "qualityGate",
2820
+ code: "validation_failed",
2821
+ message: error instanceof Error ? error.message : String(error),
2822
+ },
2823
+ ],
2824
+ };
2825
+ }
2826
+ }
2827
+
2943
2828
  async function readRequiredCompletionQualityGate(
2944
2829
  cwd: string,
2945
2830
  value: string | undefined,
@@ -2979,46 +2864,6 @@ async function readRequiredCompletionQualityGate(
2979
2864
  return hydratedGate;
2980
2865
  }
2981
2866
 
2982
- function validatePipelineCheckpointSafety(
2983
- plan: UltragoalPlan,
2984
- goal: UltragoalGoal,
2985
- changeSet?: UltragoalChangeSet,
2986
- ): void {
2987
- const metadata = goal.pipelineMetadata;
2988
- if (!metadata) return;
2989
- validateValidationBatchPipelineExclusion(goal);
2990
- requireFreshPipelineMetadata(goal);
2991
- if (metadata.overlap === "open") {
2992
- throw new Error(
2993
- `Cannot complete ${goal.id} while pipeline overlap ${metadata.overlapId ?? ""} is open; join or quarantine first.`,
2994
- );
2995
- }
2996
- if (metadata.overlap === "quarantine_required") {
2997
- throw new Error(
2998
- `Cannot complete ${goal.id} while pipeline overlap ${metadata.overlapId ?? ""} requires rebaseline.`,
2999
- );
3000
- }
3001
- if (metadata.goalId === metadata.priorGoalId && metadata.overlap !== "none" && metadata.overlap !== "joined_clean") {
3002
- throw new Error(
3003
- `Cannot complete ${goal.id} without a clean join for pipeline overlap ${metadata.overlapId ?? ""}.`,
3004
- );
3005
- }
3006
- const peer = pipelinePeer(plan, metadata);
3007
- if (changeSet && metadata.overlap !== "none") {
3008
- const peerTargets = peer?.pipelineMetadata?.targets;
3009
- for (const row of changeSet.paths) {
3010
- const ownedByGoal = pipelineTargetsCoverPath(metadata.targets, row.path);
3011
- const ownedByPeer = peerTargets ? pipelineTargetsCoverPath(peerTargets, row.path) : false;
3012
- if (ownedByGoal && ownedByPeer)
3013
- throw new Error(`Cannot complete ${goal.id} with shared pipeline change-set path ${row.path}.`);
3014
- if (!ownedByGoal && !ownedByPeer)
3015
- throw new Error(`Cannot complete ${goal.id} with unattributable pipeline change-set path ${row.path}.`);
3016
- if (!ownedByGoal && ownedByPeer)
3017
- throw new Error(`Cannot complete ${goal.id} with next-goal pipeline change-set path ${row.path}.`);
3018
- }
3019
- }
3020
- }
3021
-
3022
2867
  function validateCompleteCheckpointTargetGoal(goal: UltragoalGoal): void {
3023
2868
  if (COMPLETE_CHECKPOINT_ALLOWED_PRE_STATUSES.has(goal.status)) return;
3024
2869
  if (goal.status === "pending") {
@@ -3091,8 +2936,13 @@ export async function checkpointUltragoalGoal(input: {
3091
2936
  // receipt, the replay is a genuine re-verification: it must run the full
3092
2937
  // quality gate and mint a fresh receipt, otherwise a completed goal with a
3093
2938
  // context-staled receipt can never be repaired (different evidence is
3094
- // rejected on complete goals by design). A mutated goal row keeps the
3095
- // fail-loud tamper handling in the idempotent branch below.
2939
+ // rejected on complete goals by design). A final-aggregate receipt whose
2940
+ // recorded checkpoint gate lacks a clean criticReview OKAY is likewise
2941
+ // repair-eligible: it is not "stale", but the completion guard rejects it
2942
+ // forever (active_missing_critic_verdict), so a no-op replay would leave
2943
+ // the run permanently unable to complete even after the terminal critic
2944
+ // records OKAY. A mutated goal row keeps the fail-loud tamper handling in
2945
+ // the idempotent branch below.
3096
2946
  const staleCompleteReceiptReplay =
3097
2947
  input.status === "complete" &&
3098
2948
  goal.status === "complete" &&
@@ -3100,13 +2950,14 @@ export async function checkpointUltragoalGoal(input: {
3100
2950
  Boolean(matchingIdempotentEvent) &&
3101
2951
  (!goal.completionVerification ||
3102
2952
  (goal.completionVerification.verifiedAt === goal.updatedAt &&
3103
- validateReceiptFreshBase({
2953
+ (validateReceiptFreshBase({
3104
2954
  plan,
3105
2955
  ledger: ledgerBefore,
3106
2956
  goal,
3107
2957
  receipt: goal.completionVerification,
3108
2958
  receiptKind: goal.completionVerification.receiptKind,
3109
- }) !== null));
2959
+ }) !== null ||
2960
+ finalAggregateReceiptMissingCriticOkay(ledgerBefore, goal.completionVerification))));
3110
2961
  if (
3111
2962
  goal.status === input.status &&
3112
2963
  goal.evidence === evidence &&
@@ -3155,9 +3006,8 @@ export async function checkpointUltragoalGoal(input: {
3155
3006
  return plan;
3156
3007
  }
3157
3008
  const changeSet = input.status === "complete" ? await computeCheckpointChangeSet(input.cwd) : undefined;
3158
- if (input.status === "complete") {
3159
- validatePipelineCheckpointSafety(plan, goal, changeSet);
3160
- if (!staleCompleteReceiptReplay) validateCompleteCheckpointTargetGoal(goal);
3009
+ if (input.status === "complete" && !staleCompleteReceiptReplay) {
3010
+ validateCompleteCheckpointTargetGoal(goal);
3161
3011
  }
3162
3012
  const qualityGateJson =
3163
3013
  input.status === "complete"
@@ -3414,7 +3264,6 @@ async function addUltragoalSubgoalToPlan(input: {
3414
3264
  createdAt: now,
3415
3265
  updatedAt: now,
3416
3266
  steering: { kind, evidence, rationale },
3417
- pipelineMetadata: legacyPipelineMetadata(nextId),
3418
3267
  });
3419
3268
  input.plan.updatedAt = now;
3420
3269
  await writePlan(input.cwd, input.plan);
@@ -3465,7 +3314,6 @@ async function splitUltragoalSubgoal(input: {
3465
3314
  target.evidence = evidence;
3466
3315
  target.updatedAt = now;
3467
3316
  target.steering = { kind, evidence, rationale, replacementGoalIds };
3468
- invalidatePipelineMetadata(target, "split_subgoal_superseded", now);
3469
3317
  clearValidationBatchForBatch(input.plan, target.validationBatch);
3470
3318
  const replacementGoals = replacements.map(
3471
3319
  (replacement, index): UltragoalGoal => ({
@@ -3476,7 +3324,6 @@ async function splitUltragoalSubgoal(input: {
3476
3324
  createdAt: now,
3477
3325
  updatedAt: now,
3478
3326
  steering: { kind: "split_replacement", sourceGoalId: target.id, evidence, rationale },
3479
- pipelineMetadata: legacyPipelineMetadata(replacementGoalIds[index]!),
3480
3327
  }),
3481
3328
  );
3482
3329
  const targetIndex = input.plan.goals.findIndex(goal => goal.id === target.id);
@@ -3575,7 +3422,6 @@ async function revisePendingUltragoalWording(input: {
3575
3422
  const now = new Date().toISOString();
3576
3423
  goal.updatedAt = now;
3577
3424
  goal.steering = { kind, evidence, rationale, changedFields };
3578
- invalidatePipelineMetadata(goal, "revised_pending_wording", now);
3579
3425
  clearValidationBatchForBatch(input.plan, goal.validationBatch);
3580
3426
  input.plan.updatedAt = now;
3581
3427
  await writePlan(input.cwd, input.plan);
@@ -4138,17 +3984,7 @@ const FLAGS_WITH_VALUES = new Set([
4138
3984
  "--replacements-json",
4139
3985
  "--order-json",
4140
3986
  "--classification",
4141
- "--goal-metadata-json",
4142
3987
  "--validation-batch-json",
4143
- "--prior-goal-id",
4144
- "--next-goal-id",
4145
- "--review-handles-json",
4146
- "--qa-handles-json",
4147
- "--implementation-handle-json",
4148
- "--overlap-id",
4149
- "--review-result-json",
4150
- "--qa-result-json",
4151
- "--target-state-json",
4152
3988
  ]);
4153
3989
 
4154
3990
  function isHelpArg(arg: string): boolean {
@@ -4295,10 +4131,8 @@ function renderUltragoalHelp(args: readonly string[]): string | null {
4295
4131
  " classify-blocker",
4296
4132
  " record-critic-verdict",
4297
4133
  " record-critic-gate-override",
4134
+ " quality-gate validate",
4298
4135
 
4299
- " start-pipeline-overlap",
4300
- " join-pipeline-overlap",
4301
- " rebaseline-pipeline-overlap",
4302
4136
  "",
4303
4137
  "Run `gjc ultragoal checkpoint --help`, `gjc ultragoal review --help`, `gjc ultragoal classify-blocker --help`, `gjc ultragoal record-critic-verdict --help`, or `gjc ultragoal record-critic-gate-override --help` for command-specific requirements.",
4304
4138
  "",
@@ -4629,18 +4463,11 @@ async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<Ul
4629
4463
  return { status: 0, stdout: renderStatus(await getUltragoalStatus(cwd, sessionId), json) };
4630
4464
  case "create":
4631
4465
  case "create-goals": {
4632
- if (
4633
- flagValue(args, "--goal-metadata-json") !== undefined &&
4634
- flagValue(args, "--validation-batch-json") !== undefined
4635
- ) {
4636
- throw new Error("--validation-batch-json and --goal-metadata-json are mutually exclusive");
4637
- }
4638
4466
  const mode = flagValue(args, "--gjc-goal-mode") === "per-story" ? "per-story" : "aggregate";
4639
4467
  const plan = await createUltragoalPlan({
4640
4468
  cwd,
4641
4469
  brief: await readBrief(cwd, args),
4642
4470
  gjcGoalMode: mode,
4643
- goalMetadataJson: flagValue(args, "--goal-metadata-json"),
4644
4471
  validationBatchJson: flagValue(args, "--validation-batch-json"),
4645
4472
  });
4646
4473
  return {
@@ -4682,6 +4509,38 @@ async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<Ul
4682
4509
  stdout: renderCheckpointContinuation(result, status, json, cwd),
4683
4510
  };
4684
4511
  }
4512
+ case "quality-gate": {
4513
+ const positional = args.filter(arg => !arg.startsWith("--"));
4514
+ const subcommand = positional[1];
4515
+ if (subcommand !== "validate") {
4516
+ return {
4517
+ status: 1,
4518
+ stderr: `Unknown gjc ultragoal quality-gate subcommand: ${subcommand ?? "(missing)"}; only validate is supported\n`,
4519
+ };
4520
+ }
4521
+ const qualityGateJson = flagValue(args, "--quality-gate-json");
4522
+ if (!qualityGateJson?.trim()) {
4523
+ return { status: 1, stderr: "quality-gate validate requires --quality-gate-json\n" };
4524
+ }
4525
+ const result = await validateUltragoalQualityGateReadOnly({
4526
+ cwd,
4527
+ qualityGateJson,
4528
+ goalId: flagValue(args, "--goal-id"),
4529
+ });
4530
+ if (json) {
4531
+ return {
4532
+ status: result.valid ? 0 : 1,
4533
+ stdout: `${JSON.stringify({ valid: result.valid, errors: result.errors }, null, 2)}\n`,
4534
+ };
4535
+ }
4536
+ if (result.valid) return { status: 0, stdout: "quality gate is valid.\n" };
4537
+ return {
4538
+ status: 1,
4539
+ stderr: `${result.errors.length} quality-gate error(s):\n${result.errors
4540
+ .map(diagnostic => ` ${diagnostic.path} [${diagnostic.code}]: ${diagnostic.message}`)
4541
+ .join("\n")}\n`,
4542
+ };
4543
+ }
4685
4544
  case "review": {
4686
4545
  const result = await runUltragoalReview(cwd, args);
4687
4546
  return {
@@ -4781,65 +4640,6 @@ async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<Ul
4781
4640
  : `Recorded terminal critic gate override event-id=${String(event.eventId)}.\n`,
4782
4641
  };
4783
4642
  }
4784
- case "start-pipeline-overlap": {
4785
- const receipt = await startUltragoalPipelineOverlap({
4786
- cwd,
4787
- priorGoalId: flagValue(args, "--prior-goal-id") ?? "",
4788
- nextGoalId: flagValue(args, "--next-goal-id") ?? "",
4789
- reviewHandles: await readRequiredJsonObjectOrArray(
4790
- cwd,
4791
- flagValue(args, "--review-handles-json") ?? "",
4792
- "review handles",
4793
- ),
4794
- qaHandles: await readRequiredJsonObjectOrArray(
4795
- cwd,
4796
- flagValue(args, "--qa-handles-json") ?? "",
4797
- "QA handles",
4798
- ),
4799
- implementationHandle: await readRequiredJsonObject(
4800
- cwd,
4801
- flagValue(args, "--implementation-handle-json") ?? "",
4802
- "implementation handle",
4803
- ),
4804
- });
4805
- return {
4806
- status: 0,
4807
- stdout: json ? renderCliWriteReceipt(receipt) : `Started pipeline overlap ${receipt.overlap_id}.\n`,
4808
- };
4809
- }
4810
- case "join-pipeline-overlap": {
4811
- const receipt = await joinUltragoalPipelineOverlap({
4812
- cwd,
4813
- overlapId: flagValue(args, "--overlap-id") ?? "",
4814
- reviewResult: await readRequiredJsonObject(
4815
- cwd,
4816
- flagValue(args, "--review-result-json") ?? "",
4817
- "review result",
4818
- ),
4819
- qaResult: await readRequiredJsonObject(cwd, flagValue(args, "--qa-result-json") ?? "", "QA result"),
4820
- });
4821
- return {
4822
- status: 0,
4823
- stdout: json ? renderCliWriteReceipt(receipt) : `Joined pipeline overlap ${receipt.overlap_id}.\n`,
4824
- };
4825
- }
4826
- case "rebaseline-pipeline-overlap": {
4827
- const receipt = await rebaselineUltragoalPipelineOverlap({
4828
- cwd,
4829
- overlapId: flagValue(args, "--overlap-id") ?? "",
4830
- goalId: flagValue(args, "--goal-id") ?? "",
4831
- evidence: flagValue(args, "--evidence") ?? "",
4832
- targetState: await readRequiredJsonObject(
4833
- cwd,
4834
- flagValue(args, "--target-state-json") ?? "",
4835
- "target state",
4836
- ),
4837
- });
4838
- return {
4839
- status: 0,
4840
- stdout: json ? renderCliWriteReceipt(receipt) : `Rebaselined pipeline overlap ${receipt.overlap_id}.\n`,
4841
- };
4842
- }
4843
4643
  default:
4844
4644
  return { status: 1, stderr: `Unknown gjc ultragoal command: ${command}\n` };
4845
4645
  }
@@ -4860,9 +4660,6 @@ const RECONCILE_COMMANDS = new Set([
4860
4660
  "classify-blocker",
4861
4661
  "record-critic-verdict",
4862
4662
  "record-critic-gate-override",
4863
- "start-pipeline-overlap",
4864
- "join-pipeline-overlap",
4865
- "rebaseline-pipeline-overlap",
4866
4663
  ]);
4867
4664
 
4868
4665
  /**
@@ -4899,7 +4696,6 @@ async function reconcileUltragoalState(cwd: string): Promise<void> {
4899
4696
  if (summary.nudgeRemaining !== undefined) payload.nudge_remaining = summary.nudgeRemaining;
4900
4697
  if (summary.nudgeGoalId !== undefined) payload.nudge_goal_id = summary.nudgeGoalId;
4901
4698
  if (summary.nudgeTargetKind !== undefined) payload.nudge_target_kind = summary.nudgeTargetKind;
4902
- if (summary.pipelineOverlap) payload.pipeline_overlap = summary.pipelineOverlap;
4903
4699
  const ledgerText = await Bun.file(summary.paths.ledgerPath)
4904
4700
  .text()
4905
4701
  .catch(() => "");