@gajae-code/coding-agent 0.11.1 → 0.11.2

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 (404) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/types/cli/config-cli.d.ts +11 -1
  3. package/dist/types/cli/notify-cli.d.ts +1 -0
  4. package/dist/types/cli/session-picker.d.ts +2 -1
  5. package/dist/types/cli/setup-cli.d.ts +1 -1
  6. package/dist/types/commands/mcp-serve.d.ts +33 -0
  7. package/dist/types/commands/mcp.d.ts +1 -1
  8. package/dist/types/commands/notify.d.ts +0 -2
  9. package/dist/types/commands/sdk.d.ts +1 -1
  10. package/dist/types/config/atomic-yaml-patch.d.ts +18 -0
  11. package/dist/types/config/file-lock.d.ts +14 -4
  12. package/dist/types/config/keybindings.d.ts +42 -1
  13. package/dist/types/config/model-auth.d.ts +3 -0
  14. package/dist/types/config/model-bindings-applier.d.ts +13 -0
  15. package/dist/types/config/model-discovery-manager.d.ts +49 -0
  16. package/dist/types/config/model-equivalence.d.ts +20 -0
  17. package/dist/types/config/model-registry.d.ts +16 -15
  18. package/dist/types/config/model-resolver.d.ts +28 -18
  19. package/dist/types/config/models-config-schema.d.ts +11 -0
  20. package/dist/types/config/settings-schema.d.ts +56 -1
  21. package/dist/types/config/settings.d.ts +14 -11
  22. package/dist/types/coordinator-mcp/policy.d.ts +5 -0
  23. package/dist/types/coordinator-mcp/question-gate-codec.d.ts +129 -0
  24. package/dist/types/coordinator-mcp/question-state.d.ts +356 -0
  25. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  26. package/dist/types/discovery/helpers.d.ts +0 -6
  27. package/dist/types/edit/index.d.ts +3 -0
  28. package/dist/types/eval/py/env.d.ts +9 -0
  29. package/dist/types/extensibility/extensions/loader.d.ts +2 -1
  30. package/dist/types/extensibility/extensions/runner.d.ts +2 -2
  31. package/dist/types/extensibility/extensions/types.d.ts +34 -3
  32. package/dist/types/extensibility/hooks/runner.d.ts +1 -1
  33. package/dist/types/extensibility/shared-events.d.ts +2 -0
  34. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
  35. package/dist/types/gjc-runtime/deep-interview-state.d.ts +44 -0
  36. package/dist/types/gjc-runtime/goal-mode-request.d.ts +11 -1
  37. package/dist/types/gjc-runtime/launch-worktree.d.ts +4 -2
  38. package/dist/types/gjc-runtime/linux-proc.d.ts +29 -0
  39. package/dist/types/gjc-runtime/session-resolution.d.ts +2 -2
  40. package/dist/types/gjc-runtime/state-migrations.d.ts +1 -1
  41. package/dist/types/gjc-runtime/state-writer.d.ts +2 -1
  42. package/dist/types/gjc-runtime/team-launch.d.ts +59 -0
  43. package/dist/types/gjc-runtime/team-notify.d.ts +45 -0
  44. package/dist/types/gjc-runtime/team-runtime.d.ts +29 -121
  45. package/dist/types/gjc-runtime/team-store.d.ts +160 -0
  46. package/dist/types/gjc-runtime/team-workers.d.ts +78 -0
  47. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +8 -0
  48. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +13 -0
  49. package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +38 -0
  50. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -0
  51. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +23 -0
  52. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +55 -33
  53. package/dist/types/gjc-runtime/workflow-cli-common.d.ts +8 -0
  54. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -0
  55. package/dist/types/goals/continuation-timeout-guard.d.ts +22 -0
  56. package/dist/types/goals/runtime.d.ts +1 -0
  57. package/dist/types/goals/state.d.ts +8 -0
  58. package/dist/types/harness-control-plane/lifecycle-reconciliation.d.ts +17 -0
  59. package/dist/types/harness-control-plane/phase-rollup.d.ts +4 -7
  60. package/dist/types/harness-control-plane/receipt-ingest.d.ts +5 -0
  61. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  62. package/dist/types/hindsight/state.d.ts +11 -4
  63. package/dist/types/lsp/lspmux.d.ts +5 -3
  64. package/dist/types/lsp/path-trust.d.ts +1 -0
  65. package/dist/types/main.d.ts +3 -2
  66. package/dist/types/memories/storage.d.ts +1 -0
  67. package/dist/types/modes/action-registry.d.ts +29 -0
  68. package/dist/types/modes/components/chrome.d.ts +14 -0
  69. package/dist/types/modes/components/command-palette.d.ts +27 -0
  70. package/dist/types/modes/components/custom-provider-wizard.d.ts +1 -0
  71. package/dist/types/modes/components/index.d.ts +2 -1
  72. package/dist/types/modes/components/pet-selector.d.ts +1 -1
  73. package/dist/types/modes/components/plan-preview-overlay.d.ts +43 -0
  74. package/dist/types/modes/components/queue-mode-selector.d.ts +1 -1
  75. package/dist/types/modes/components/queue-pane.d.ts +12 -0
  76. package/dist/types/modes/components/read-tool-group.d.ts +6 -0
  77. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -4
  78. package/dist/types/modes/components/sessions-dashboard.d.ts +32 -0
  79. package/dist/types/modes/components/settings-selector.d.ts +1 -1
  80. package/dist/types/modes/components/show-images-selector.d.ts +1 -1
  81. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  82. package/dist/types/modes/components/status-line.d.ts +1 -94
  83. package/dist/types/modes/components/tasks-pane.d.ts +15 -0
  84. package/dist/types/modes/components/theme-selector.d.ts +1 -1
  85. package/dist/types/modes/components/tool-execution.d.ts +15 -0
  86. package/dist/types/modes/components/tool-status-header.d.ts +111 -0
  87. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +42 -0
  88. package/dist/types/modes/controllers/goal-mode-controller.d.ts +62 -0
  89. package/dist/types/modes/controllers/input-controller.d.ts +8 -1
  90. package/dist/types/modes/controllers/mode-gate.d.ts +8 -0
  91. package/dist/types/modes/controllers/plan-mode-controller.d.ts +65 -0
  92. package/dist/types/modes/controllers/selector-controller.d.ts +15 -3
  93. package/dist/types/modes/controllers/stt-controller.d.ts +26 -0
  94. package/dist/types/modes/interactive-mode.d.ts +19 -17
  95. package/dist/types/modes/jobs-observer.d.ts +2 -0
  96. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -11
  97. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +20 -2
  98. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +31 -0
  99. package/dist/types/modes/tasks-aggregator.d.ts +39 -0
  100. package/dist/types/modes/transcript-item-registry.d.ts +60 -0
  101. package/dist/types/modes/types.d.ts +20 -13
  102. package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
  103. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  104. package/dist/types/sdk/broker/broker.d.ts +78 -12
  105. package/dist/types/sdk/broker/lifecycle-codec.d.ts +4 -0
  106. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +15 -1
  107. package/dist/types/sdk/broker/lifecycle.d.ts +6 -2
  108. package/dist/types/sdk/broker/process-incarnation.d.ts +2 -0
  109. package/dist/types/sdk/broker/session-index.d.ts +0 -1
  110. package/dist/types/sdk/broker/state-version.d.ts +2 -0
  111. package/dist/types/sdk/bus/chat-effect-journal.d.ts +1 -0
  112. package/dist/types/sdk/bus/config-commands.d.ts +1 -1
  113. package/dist/types/sdk/bus/config.d.ts +11 -0
  114. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  115. package/dist/types/sdk/bus/html-format.d.ts +3 -2
  116. package/dist/types/sdk/bus/index.d.ts +27 -0
  117. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  118. package/dist/types/sdk/bus/notification-service.d.ts +20 -0
  119. package/dist/types/sdk/bus/rate-limit-pool.d.ts +13 -2
  120. package/dist/types/sdk/bus/recent-activity.d.ts +21 -12
  121. package/dist/types/sdk/bus/rich-document.d.ts +7 -0
  122. package/dist/types/sdk/bus/rich-render.d.ts +18 -41
  123. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
  124. package/dist/types/sdk/bus/telegram-daemon.d.ts +46 -3
  125. package/dist/types/sdk/bus/threaded-inbound.d.ts +1 -1
  126. package/dist/types/sdk/bus/threaded-render.d.ts +13 -0
  127. package/dist/types/sdk/bus/topic-registry.d.ts +24 -2
  128. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  129. package/dist/types/sdk/host/host.d.ts +2 -0
  130. package/dist/types/sdk/index.d.ts +1 -0
  131. package/dist/types/sdk/session-directory.d.ts +56 -0
  132. package/dist/types/session/agent-session.d.ts +82 -47
  133. package/dist/types/session/agent-storage.d.ts +7 -0
  134. package/dist/types/session/fallback-chain-controller.d.ts +10 -2
  135. package/dist/types/session/internal/managed-session-scope.d.ts +102 -0
  136. package/dist/types/session/internal/managed-session-storage.d.ts +38 -0
  137. package/dist/types/session/messages.d.ts +16 -0
  138. package/dist/types/session/session-manager-internal.d.ts +14 -0
  139. package/dist/types/session/session-manager.d.ts +82 -12
  140. package/dist/types/session/session-storage.d.ts +43 -3
  141. package/dist/types/session/volatile-context-pruning.d.ts +15 -0
  142. package/dist/types/skill-state/{deep-interview-mutation-guard.d.ts → workflow-mutation-guard.d.ts} +24 -8
  143. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -3
  144. package/dist/types/slash-commands/builtin-registry.d.ts +4 -1
  145. package/dist/types/system-prompt.d.ts +2 -11
  146. package/dist/types/task/executor.d.ts +3 -1
  147. package/dist/types/thinking.d.ts +7 -0
  148. package/dist/types/tool-discovery/tool-index.d.ts +1 -50
  149. package/dist/types/tools/ask.d.ts +64 -2
  150. package/dist/types/tools/bash.d.ts +3 -0
  151. package/dist/types/tools/browser/dead-tab-recovery.d.ts +24 -0
  152. package/dist/types/tools/browser/tab-supervisor.d.ts +21 -0
  153. package/dist/types/tools/cron.d.ts +2 -0
  154. package/dist/types/tools/fetch.d.ts +1 -0
  155. package/dist/types/tools/index.d.ts +30 -15
  156. package/dist/types/tools/output-meta.d.ts +1 -0
  157. package/dist/types/tools/read.d.ts +3 -0
  158. package/dist/types/tools/search-tool-bm25.d.ts +3 -4
  159. package/dist/types/utils/event-bus.d.ts +1 -1
  160. package/dist/types/web/search/index.d.ts +3 -1
  161. package/package.json +21 -9
  162. package/scripts/build-sdk-package-smoke.ts +34 -1
  163. package/scripts/generate-hotkeys-docs.ts +74 -0
  164. package/scripts/generate-sdk-operation-inventory.ts +64 -20
  165. package/scripts/verify-gjc-sdk-canonicalization.ts +182 -2
  166. package/src/async/job-manager.ts +49 -6
  167. package/src/cli/config-cli.ts +75 -2
  168. package/src/cli/notify-cli.ts +92 -26
  169. package/src/cli/session-picker.ts +20 -3
  170. package/src/cli/setup-cli.ts +1 -1
  171. package/src/cli.ts +1 -1
  172. package/src/commands/config.ts +1 -1
  173. package/src/commands/harness.ts +34 -106
  174. package/src/commands/launch.ts +6 -4
  175. package/src/commands/mcp-serve.ts +85 -8
  176. package/src/commands/mcp.ts +2 -2
  177. package/src/commands/notify.ts +8 -4
  178. package/src/commands/sdk.ts +61 -40
  179. package/src/commands/ultragoal.ts +4 -1
  180. package/src/config/atomic-yaml-patch.ts +80 -13
  181. package/src/config/file-lock.ts +105 -43
  182. package/src/config/keybindings.ts +64 -20
  183. package/src/config/model-auth.ts +9 -0
  184. package/src/config/model-bindings-applier.ts +139 -0
  185. package/src/config/model-discovery-manager.ts +224 -0
  186. package/src/config/model-equivalence.ts +66 -0
  187. package/src/config/model-registry.ts +169 -302
  188. package/src/config/model-resolver.ts +361 -369
  189. package/src/config/models-config-schema.ts +5 -4
  190. package/src/config/settings-schema.ts +159 -2
  191. package/src/config/settings.ts +458 -482
  192. package/src/coordinator-mcp/policy.ts +76 -11
  193. package/src/coordinator-mcp/question-gate-codec.ts +274 -0
  194. package/src/coordinator-mcp/question-state.ts +778 -0
  195. package/src/coordinator-mcp/server.ts +1702 -238
  196. package/src/debug/index.ts +1 -1
  197. package/src/defaults/gjc/skills/deep-interview/SKILL.md +13 -1
  198. package/src/defaults/gjc/skills/team/SKILL.md +16 -0
  199. package/src/discovery/helpers.ts +1 -50
  200. package/src/edit/index.ts +64 -0
  201. package/src/eval/py/env.ts +26 -0
  202. package/src/eval/py/kernel.ts +5 -3
  203. package/src/extensibility/extensions/loader.ts +9 -1
  204. package/src/extensibility/extensions/runner.ts +12 -3
  205. package/src/extensibility/extensions/types.ts +39 -1
  206. package/src/extensibility/gjc-plugins/installer.ts +16 -16
  207. package/src/extensibility/hooks/runner.ts +2 -2
  208. package/src/extensibility/runtime-skill-discovery.ts +33 -13
  209. package/src/extensibility/shared-events.ts +2 -0
  210. package/src/gjc-runtime/deep-interview-recorder.ts +63 -3
  211. package/src/gjc-runtime/deep-interview-runtime.ts +76 -27
  212. package/src/gjc-runtime/deep-interview-state.ts +317 -5
  213. package/src/gjc-runtime/goal-mode-request.ts +55 -8
  214. package/src/gjc-runtime/launch-tmux.ts +2 -5
  215. package/src/gjc-runtime/launch-worktree.ts +5 -5
  216. package/src/gjc-runtime/linux-proc.ts +71 -0
  217. package/src/gjc-runtime/ralplan-runtime.ts +74 -90
  218. package/src/gjc-runtime/session-resolution.ts +18 -3
  219. package/src/gjc-runtime/session-state-sidecar.ts +6 -107
  220. package/src/gjc-runtime/state-migrations.ts +1 -1
  221. package/src/gjc-runtime/state-runtime.ts +320 -249
  222. package/src/gjc-runtime/state-writer.ts +36 -40
  223. package/src/gjc-runtime/team-launch.ts +319 -0
  224. package/src/gjc-runtime/team-notify.ts +326 -0
  225. package/src/gjc-runtime/team-runtime.ts +1099 -1482
  226. package/src/gjc-runtime/team-store.ts +828 -0
  227. package/src/gjc-runtime/team-workers.ts +962 -0
  228. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +2 -11
  229. package/src/gjc-runtime/tmux-owner-isolation.ts +14 -9
  230. package/src/gjc-runtime/tmux-sessions.ts +4 -21
  231. package/src/gjc-runtime/ultragoal-change-set.ts +172 -0
  232. package/src/gjc-runtime/ultragoal-evidence.ts +874 -0
  233. package/src/gjc-runtime/ultragoal-guard.ts +29 -5
  234. package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
  235. package/src/gjc-runtime/ultragoal-runtime.ts +84 -1311
  236. package/src/gjc-runtime/workflow-cli-common.ts +30 -0
  237. package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
  238. package/src/gjc-runtime/workflow-manifest.ts +18 -0
  239. package/src/goals/continuation-timeout-guard.ts +79 -0
  240. package/src/goals/runtime.ts +7 -5
  241. package/src/goals/state.ts +18 -0
  242. package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
  243. package/src/harness-control-plane/phase-rollup.ts +4 -7
  244. package/src/harness-control-plane/receipt-ingest.ts +5 -0
  245. package/src/harness-control-plane/receipt-spool.ts +51 -7
  246. package/src/harness-control-plane/session-lease.ts +12 -130
  247. package/src/hindsight/backend.ts +26 -27
  248. package/src/hindsight/state.ts +101 -32
  249. package/src/hooks/skill-state.ts +17 -11
  250. package/src/internal-urls/docs-index.generated.ts +21 -18
  251. package/src/lsp/client.ts +1 -1
  252. package/src/lsp/config.ts +145 -26
  253. package/src/lsp/index.ts +1 -1
  254. package/src/lsp/lspmux.ts +39 -10
  255. package/src/lsp/path-trust.ts +81 -0
  256. package/src/main.ts +28 -8
  257. package/src/memories/index.ts +1 -0
  258. package/src/memories/storage.ts +6 -2
  259. package/src/modes/action-registry.ts +165 -0
  260. package/src/modes/components/chrome.ts +40 -0
  261. package/src/modes/components/command-palette.ts +168 -0
  262. package/src/modes/components/custom-editor.ts +1 -1
  263. package/src/modes/components/custom-provider-wizard.ts +51 -14
  264. package/src/modes/components/index.ts +2 -1
  265. package/src/modes/components/pet-selector.ts +11 -17
  266. package/src/modes/components/plan-preview-overlay.ts +295 -0
  267. package/src/modes/components/queue-mode-selector.ts +10 -27
  268. package/src/modes/components/queue-pane.ts +79 -0
  269. package/src/modes/components/read-tool-group.ts +13 -0
  270. package/src/modes/components/session-observer-overlay.ts +207 -772
  271. package/src/modes/components/sessions-dashboard.ts +181 -0
  272. package/src/modes/components/settings-selector.ts +1 -1
  273. package/src/modes/components/show-images-selector.ts +10 -24
  274. package/src/modes/components/status-line/types.ts +1 -1
  275. package/src/modes/components/status-line.ts +1 -969
  276. package/src/modes/components/tasks-pane.ts +73 -0
  277. package/src/modes/components/theme-selector.ts +11 -31
  278. package/src/modes/components/tool-execution.ts +22 -0
  279. package/src/modes/components/tool-status-header.ts +1056 -0
  280. package/src/modes/components/transcript-viewer-overlay.ts +384 -0
  281. package/src/modes/controllers/btw-controller.ts +1 -0
  282. package/src/modes/controllers/event-controller.ts +3 -2
  283. package/src/modes/controllers/extension-ui-controller.ts +24 -1
  284. package/src/modes/controllers/goal-mode-controller.ts +474 -0
  285. package/src/modes/controllers/input-controller.ts +764 -229
  286. package/src/modes/controllers/mode-gate.ts +24 -0
  287. package/src/modes/controllers/plan-mode-controller.ts +517 -0
  288. package/src/modes/controllers/selector-controller.ts +225 -10
  289. package/src/modes/controllers/stt-controller.ts +125 -0
  290. package/src/modes/interactive-mode.ts +289 -1150
  291. package/src/modes/jobs-observer.ts +3 -0
  292. package/src/modes/runtime-init.ts +8 -0
  293. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -106
  294. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +71 -16
  295. package/src/modes/shared/agent-wire/workflow-gate-types.ts +177 -0
  296. package/src/modes/tasks-aggregator.ts +199 -0
  297. package/src/modes/transcript-item-registry.ts +229 -0
  298. package/src/modes/types.ts +45 -18
  299. package/src/modes/utils/ui-helpers.ts +17 -9
  300. package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
  301. package/src/prompts/agent-fragments/restricted-bash.md +1 -0
  302. package/src/prompts/agents/architect.md +2 -8
  303. package/src/prompts/agents/critic.md +2 -8
  304. package/src/prompts/agents/executor.md +3 -0
  305. package/src/prompts/agents/planner.md +4 -12
  306. package/src/prompts/system/custom-system-prompt.md +1 -23
  307. package/src/prompts/system/plan-mode-approved.md +8 -0
  308. package/src/prompts/system/system-prompt.md +17 -96
  309. package/src/prompts/tools/bash.md +4 -4
  310. package/src/prompts/tools/browser.md +15 -52
  311. package/src/prompts/tools/find.md +2 -2
  312. package/src/prompts/tools/hashline.md +13 -55
  313. package/src/prompts/tools/read.md +3 -6
  314. package/src/prompts/tools/search-tool-bm25.md +0 -2
  315. package/src/prompts/tools/search.md +2 -4
  316. package/src/prompts/tools/skill-discovery.md +1 -2
  317. package/src/prompts/tools/task.md +1 -1
  318. package/src/runtime-mcp/manager.ts +1 -1
  319. package/src/sdk/broker/broker.ts +206 -78
  320. package/src/sdk/broker/discovery.ts +1 -1
  321. package/src/sdk/broker/ensure.ts +3 -1
  322. package/src/sdk/broker/lifecycle-codec.ts +11 -0
  323. package/src/sdk/broker/lifecycle-ledger.ts +371 -74
  324. package/src/sdk/broker/lifecycle.ts +1399 -142
  325. package/src/sdk/broker/process-incarnation.ts +5 -13
  326. package/src/sdk/broker/session-index.ts +145 -56
  327. package/src/sdk/broker/state-version.ts +21 -0
  328. package/src/sdk/broker/transport.ts +1 -1
  329. package/src/sdk/bus/chat-daemon-cli.ts +2 -0
  330. package/src/sdk/bus/chat-daemon-control.ts +8 -28
  331. package/src/sdk/bus/chat-effect-journal.ts +10 -2
  332. package/src/sdk/bus/config-commands.ts +5 -4
  333. package/src/sdk/bus/config.ts +98 -0
  334. package/src/sdk/bus/conversation-store.ts +5 -29
  335. package/src/sdk/bus/daemon-paths.ts +2 -0
  336. package/src/sdk/bus/html-format.ts +20 -3
  337. package/src/sdk/bus/index.ts +778 -93
  338. package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
  339. package/src/sdk/bus/natives-augment.d.ts +26 -1
  340. package/src/sdk/bus/notification-service.ts +168 -20
  341. package/src/sdk/bus/rate-limit-pool.ts +58 -14
  342. package/src/sdk/bus/recent-activity.ts +188 -74
  343. package/src/sdk/bus/rich-document.ts +258 -0
  344. package/src/sdk/bus/rich-render.ts +62 -70
  345. package/src/sdk/bus/telegram-daemon-cli.ts +15 -59
  346. package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
  347. package/src/sdk/bus/telegram-daemon.ts +1389 -237
  348. package/src/sdk/bus/threaded-inbound.ts +8 -3
  349. package/src/sdk/bus/threaded-render.ts +57 -1
  350. package/src/sdk/bus/topic-registry.ts +120 -13
  351. package/src/sdk/host/control/dispatch.ts +14 -2
  352. package/src/sdk/host/control/operations.ts +6 -1
  353. package/src/sdk/host/host.ts +31 -1
  354. package/src/sdk/host/query/handlers.ts +20 -22
  355. package/src/sdk/host/query/revision-store.ts +80 -57
  356. package/src/sdk/index.ts +1 -0
  357. package/src/sdk/protocol/operation-inventory.generated.json +943 -1907
  358. package/src/sdk/session-directory.ts +141 -0
  359. package/src/sdk/session.ts +458 -479
  360. package/src/session/agent-session.ts +1700 -583
  361. package/src/session/agent-storage.ts +18 -0
  362. package/src/session/cache-economics.ts +25 -8
  363. package/src/session/fallback-chain-controller.ts +53 -3
  364. package/src/session/internal/managed-session-scope.ts +2226 -0
  365. package/src/session/internal/managed-session-storage.ts +453 -0
  366. package/src/session/messages.ts +91 -2
  367. package/src/session/session-manager-internal.ts +33 -0
  368. package/src/session/session-manager.ts +1112 -333
  369. package/src/session/session-storage.ts +444 -39
  370. package/src/session/volatile-context-pruning.ts +78 -0
  371. package/src/setup/credential-auto-import.ts +2 -1
  372. package/src/setup/hermes/templates/operator-instructions.v1.md +1 -1
  373. package/src/setup/provider-onboarding.ts +36 -61
  374. package/src/setup/provider-presets.json +5 -0
  375. package/src/skill-state/active-state.ts +3 -13
  376. package/src/skill-state/{deep-interview-mutation-guard.ts → workflow-mutation-guard.ts} +47 -29
  377. package/src/skill-state/workflow-state-contract.ts +6 -41
  378. package/src/slash-commands/builtin-registry.ts +46 -11
  379. package/src/system-prompt.ts +37 -44
  380. package/src/task/agents.ts +19 -1
  381. package/src/task/executor.ts +90 -6
  382. package/src/task/index.ts +36 -21
  383. package/src/thinking.ts +21 -6
  384. package/src/tool-discovery/tool-index.ts +2 -134
  385. package/src/tools/ask.ts +157 -21
  386. package/src/tools/ast-edit.ts +1 -1
  387. package/src/tools/bash.ts +38 -0
  388. package/src/tools/browser/dead-tab-recovery.ts +78 -0
  389. package/src/tools/browser/tab-supervisor.ts +282 -36
  390. package/src/tools/computer.ts +10 -3
  391. package/src/tools/cron.ts +30 -16
  392. package/src/tools/eval.ts +1 -2
  393. package/src/tools/fetch.ts +11 -4
  394. package/src/tools/index.ts +76 -24
  395. package/src/tools/output-meta.ts +13 -5
  396. package/src/tools/read.ts +37 -1
  397. package/src/tools/resource-gc.ts +2 -2
  398. package/src/tools/search-tool-bm25.ts +11 -74
  399. package/src/utils/event-bus.ts +9 -4
  400. package/src/web/search/index.ts +3 -3
  401. package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
  402. package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
  403. package/src/discovery/plugin-dir-roots.ts +0 -28
  404. package/src/runtime-mcp/discoverable-tool-metadata.ts +0 -25
@@ -1,7 +1,6 @@
1
1
  import * as crypto from "node:crypto";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import { inflateSync } from "node:zlib";
5
4
  import type { WorkflowHudSummary } from "../skill-state/active-state";
6
5
  import { buildUltragoalHudSummary as buildWorkflowUltragoalHudSummary } from "../skill-state/workflow-hud";
7
6
  import { renderCliWriteReceipt } from "./cli-write-receipt";
@@ -291,18 +290,18 @@ export interface UltragoalCommandResult {
291
290
  createdPlan?: boolean;
292
291
  }
293
292
 
294
- interface JsonObject {
293
+ export interface JsonObject {
295
294
  [key: string]: unknown;
296
295
  }
297
296
 
298
- function currentUltragoalSessionId(cwd: string): string {
297
+ export function currentUltragoalSessionId(cwd: string): string {
299
298
  return resolveGjcSessionForWrite(cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
300
299
  }
301
300
 
302
301
  const TERMINAL_OR_SKIPPED_STATUSES = new Set<UltragoalGoalStatus>(["complete", "superseded"]);
303
302
  const CLEAN_ARCHITECT_STATUS = "CLEAR";
304
303
  const APPROVE_RECOMMENDATION = "APPROVE";
305
- const PASSED_STATUS = "passed";
304
+ export const PASSED_STATUS = "passed";
306
305
  const NOT_APPLICABLE_STATUS = "not_applicable";
307
306
  const COVERED_STATUS = "covered";
308
307
  const ACCEPTED_PROOF_STATUSES = new Set([COVERED_STATUS, "passed", "verified"]);
@@ -364,13 +363,17 @@ export function getUltragoalPaths(cwd: string, sessionId?: string | null): Ultra
364
363
  };
365
364
  }
366
365
 
367
- function isEnoent(error: unknown): boolean {
366
+ export function isEnoent(error: unknown): boolean {
368
367
  return (
369
368
  typeof error === "object" && error !== null && "code" in error && (error as { code?: unknown }).code === "ENOENT"
370
369
  );
371
370
  }
372
371
 
373
- async function appendLedger(cwd: string, event: JsonObject, sessionId?: string | null): Promise<UltragoalLedgerEvent> {
372
+ export async function appendLedger(
373
+ cwd: string,
374
+ event: JsonObject,
375
+ sessionId?: string | null,
376
+ ): Promise<UltragoalLedgerEvent> {
374
377
  const resolvedSessionId =
375
378
  sessionId?.trim() || resolveGjcSessionForWrite(cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
376
379
  const paths = getUltragoalPaths(cwd, resolvedSessionId);
@@ -554,7 +557,7 @@ export async function recordUltragoalNudgeIfBudgetRemaining(input: {
554
557
  );
555
558
  }
556
559
 
557
- async function writePlan(cwd: string, plan: UltragoalPlan, sessionId?: string | null): Promise<void> {
560
+ export async function writePlan(cwd: string, plan: UltragoalPlan, sessionId?: string | null): Promise<void> {
558
561
  const resolvedSessionId =
559
562
  sessionId?.trim() || resolveGjcSessionForWrite(cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
560
563
  const paths = getUltragoalPaths(cwd, resolvedSessionId);
@@ -689,10 +692,10 @@ function buildCompletionReceipt(input: {
689
692
  };
690
693
  }
691
694
 
692
- function nonEmptyString(value: unknown): string | null {
695
+ export function nonEmptyString(value: unknown): string | null {
693
696
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
694
697
  }
695
- function stringArray(value: unknown): string[] | null {
698
+ export function stringArray(value: unknown): string[] | null {
696
699
  return Array.isArray(value) && value.every(item => typeof item === "string") ? value.map(item => item.trim()) : null;
697
700
  }
698
701
 
@@ -742,7 +745,7 @@ function pipelineMetadataHashBasis(metadata: Omit<UltragoalPipelineMetadata, "me
742
745
  };
743
746
  }
744
747
 
745
- function hashPipelineMetadata(metadata: Omit<UltragoalPipelineMetadata, "metadataHash">): string {
748
+ export function hashPipelineMetadata(metadata: Omit<UltragoalPipelineMetadata, "metadataHash">): string {
746
749
  return hashStructuredValue(pipelineMetadataHashBasis(metadata));
747
750
  }
748
751
 
@@ -858,7 +861,7 @@ function pipelineMetadataConflictsWithValidationBatch(metadata: UltragoalPipelin
858
861
  return metadata?.eligible === true || metadata?.source === "original_plan_graph" || metadata?.source === "steering";
859
862
  }
860
863
 
861
- function validateValidationBatchPipelineExclusion(goal: UltragoalGoal): void {
864
+ export function validateValidationBatchPipelineExclusion(goal: UltragoalGoal): void {
862
865
  if (goal.validationBatch && pipelineMetadataConflictsWithValidationBatch(goal.pipelineMetadata)) {
863
866
  throw new Error(`Goal ${goal.id} cannot combine validationBatch with eligible pipeline metadata`);
864
867
  }
@@ -1051,28 +1054,28 @@ function normalizePipelineMetadataRecord(value: unknown, goalIds: ReadonlySet<st
1051
1054
  return withPipelineMetadataHash(basis);
1052
1055
  }
1053
1056
 
1054
- function targetsAreDisjoint(left: UltragoalPipelineTargets, right: UltragoalPipelineTargets): boolean {
1057
+ export function targetsAreDisjoint(left: UltragoalPipelineTargets, right: UltragoalPipelineTargets): boolean {
1055
1058
  return (
1056
1059
  left.files.every(file => !right.files.includes(file)) &&
1057
1060
  left.surfaces.every(surface => !right.surfaces.includes(surface))
1058
1061
  );
1059
1062
  }
1060
1063
 
1061
- function targetsOverlap(left: UltragoalPipelineTargets, right: UltragoalPipelineTargets): boolean {
1064
+ export function targetsOverlap(left: UltragoalPipelineTargets, right: UltragoalPipelineTargets): boolean {
1062
1065
  return (
1063
1066
  left.files.some(file => right.files.includes(file)) ||
1064
1067
  left.surfaces.some(surface => right.surfaces.includes(surface))
1065
1068
  );
1066
1069
  }
1067
1070
 
1068
- function requireNonEmptyPipelineTargets(value: unknown, fieldName: string): UltragoalPipelineTargets {
1071
+ export function requireNonEmptyPipelineTargets(value: unknown, fieldName: string): UltragoalPipelineTargets {
1069
1072
  const targets = normalizePipelineTargets(value, fieldName);
1070
1073
  if (targets.files.length === 0 && targets.surfaces.length === 0)
1071
1074
  throw new Error(`${fieldName} requires files or surfaces`);
1072
1075
  return targets;
1073
1076
  }
1074
1077
 
1075
- function collectPipelineBlockerFootprints(result: JsonObject, fieldName: string): UltragoalPipelineTargets[] {
1078
+ export function collectPipelineBlockerFootprints(result: JsonObject, fieldName: string): UltragoalPipelineTargets[] {
1076
1079
  const raw = Array.isArray(result.blockers)
1077
1080
  ? result.blockers
1078
1081
  : Array.isArray(result.blockerFootprints)
@@ -1090,12 +1093,12 @@ function pipelineTargetsCoverPath(targets: UltragoalPipelineTargets, filePath: s
1090
1093
  return targets.files.some(target => normalized === target || normalized.startsWith(`${target}/`));
1091
1094
  }
1092
1095
 
1093
- function pipelinePeer(plan: UltragoalPlan, metadata: UltragoalPipelineMetadata): UltragoalGoal | undefined {
1096
+ export function pipelinePeer(plan: UltragoalPlan, metadata: UltragoalPipelineMetadata): UltragoalGoal | undefined {
1094
1097
  const peerId = metadata.goalId === metadata.priorGoalId ? metadata.nextGoalId : metadata.priorGoalId;
1095
1098
  return peerId ? plan.goals.find(goal => goal.id === peerId) : undefined;
1096
1099
  }
1097
1100
 
1098
- function handleIdsFromValue(value: JsonObject | JsonObject[], fieldName: string): string[] {
1101
+ export function handleIdsFromValue(value: JsonObject | JsonObject[], fieldName: string): string[] {
1099
1102
  const records = Array.isArray(value) ? value : [value];
1100
1103
  const ids = records.map(
1101
1104
  (record, index) =>
@@ -1108,13 +1111,13 @@ function handleIdsFromValue(value: JsonObject | JsonObject[], fieldName: string)
1108
1111
  return ids;
1109
1112
  }
1110
1113
 
1111
- function resultHandleIds(value: JsonObject, fieldName: string): string[] {
1114
+ export function resultHandleIds(value: JsonObject, fieldName: string): string[] {
1112
1115
  const ids = stringArray(value.handleIds) ?? stringArray(value.handles) ?? [];
1113
1116
  if (ids.length === 0) throw new Error(`${fieldName} requires handleIds`);
1114
1117
  return ids;
1115
1118
  }
1116
1119
 
1117
- function requireCoveredHandles(expected: readonly string[], actual: readonly string[], fieldName: string): void {
1120
+ export function requireCoveredHandles(expected: readonly string[], actual: readonly string[], fieldName: string): void {
1118
1121
  const missing = expected.filter(id => !actual.includes(id));
1119
1122
  if (missing.length > 0) throw new Error(`${fieldName} is missing handle coverage for ${missing.join(", ")}`);
1120
1123
  }
@@ -1202,7 +1205,7 @@ function currentPipelineHash(metadata: UltragoalPipelineMetadata): string {
1202
1205
  return hashPipelineMetadata(metadata);
1203
1206
  }
1204
1207
 
1205
- function requireFreshPipelineMetadata(goal: UltragoalGoal): UltragoalPipelineMetadata {
1208
+ export function requireFreshPipelineMetadata(goal: UltragoalGoal): UltragoalPipelineMetadata {
1206
1209
  const metadata = goal.pipelineMetadata;
1207
1210
  if (!metadata) throw new Error(`Goal ${goal.id} has no pipeline metadata`);
1208
1211
  if (metadata.metadataHash !== currentPipelineHash(metadata))
@@ -1210,7 +1213,7 @@ function requireFreshPipelineMetadata(goal: UltragoalGoal): UltragoalPipelineMet
1210
1213
  return metadata;
1211
1214
  }
1212
1215
 
1213
- function openPipelineOverlap(
1216
+ export function openPipelineOverlap(
1214
1217
  plan: UltragoalPlan,
1215
1218
  ): { prior: UltragoalGoal; next: UltragoalGoal; overlapId: string } | null {
1216
1219
  const openGoals = plan.goals.filter(goal => goal.pipelineMetadata?.overlap === "open");
@@ -1549,14 +1552,14 @@ export interface UltragoalRunCompletionState {
1549
1552
  hasBlockers: boolean;
1550
1553
  needsFinalAggregateReceipt: boolean;
1551
1554
  }
1552
- function requireJsonObjectValue(value: unknown, fieldName: string): JsonObject {
1555
+ export function requireJsonObjectValue(value: unknown, fieldName: string): JsonObject {
1553
1556
  if (typeof value !== "object" || value === null || Array.isArray(value))
1554
1557
  throw new Error(`${fieldName} must be an object`);
1555
1558
  if (Object.keys(value).length === 0) throw new Error(`${fieldName} must be non-empty`);
1556
1559
  return value as JsonObject;
1557
1560
  }
1558
1561
 
1559
- function requireJsonObjectOrArrayValue(value: unknown, fieldName: string): JsonObject | JsonObject[] {
1562
+ export function requireJsonObjectOrArrayValue(value: unknown, fieldName: string): JsonObject | JsonObject[] {
1560
1563
  if (Array.isArray(value)) {
1561
1564
  if (value.length === 0) throw new Error(`${fieldName} must be non-empty`);
1562
1565
  return value.map((item, index) => requireJsonObjectValue(item, `${fieldName}[${index}]`));
@@ -1576,278 +1579,13 @@ async function readRequiredJsonObjectOrArray(
1576
1579
  return requireJsonObjectOrArrayValue(await readStructuredValue(cwd, value), fieldName);
1577
1580
  }
1578
1581
 
1579
- function requirePipelineStartable(plan: UltragoalPlan, prior: UltragoalGoal, next: UltragoalGoal): void {
1580
- if (plan.gjcGoalMode !== "aggregate")
1581
- throw new Error("pipeline overlap is supported only for aggregate ultragoal mode");
1582
- if (openPipelineOverlap(plan))
1583
- throw new Error("Cannot start pipeline overlap because another overlap is already open");
1584
- if (prior.status !== "active")
1585
- throw new Error(`Prior goal ${prior.id} must be active before pipeline overlap starts`);
1586
- if (next.status !== "pending" && next.status !== "failed")
1587
- throw new Error(`Next goal ${next.id} must be pending or retryable failed`);
1588
- validateValidationBatchPipelineExclusion(prior);
1589
- validateValidationBatchPipelineExclusion(next);
1590
- if (prior.validationBatch || next.validationBatch)
1591
- throw new Error("pipeline overlap cannot start for validation batch goals");
1592
- const priorMetadata = requireFreshPipelineMetadata(prior);
1593
- const nextMetadata = requireFreshPipelineMetadata(next);
1594
- if (!priorMetadata.eligible || !nextMetadata.eligible)
1595
- throw new Error("pipeline overlap requires eligible original-plan metadata on both goals");
1596
- if (!priorMetadata.independentOf.includes(next.id) || !nextMetadata.independentOf.includes(prior.id)) {
1597
- throw new Error("pipeline overlap requires symmetric original independence");
1598
- }
1599
- if (!targetsAreDisjoint(priorMetadata.targets, nextMetadata.targets))
1600
- throw new Error("pipeline overlap requires disjoint files and surfaces");
1601
- }
1602
-
1603
- function pipelineEventRefs(prior: UltragoalGoal, next: UltragoalGoal): JsonObject {
1604
- return {
1605
- priorMetadataHash: prior.pipelineMetadata?.metadataHash,
1606
- nextMetadataHash: next.pipelineMetadata?.metadataHash,
1607
- priorTargets: prior.pipelineMetadata?.targets,
1608
- nextTargets: next.pipelineMetadata?.targets,
1609
- };
1610
- }
1611
-
1612
- function pipelineReceipt(
1613
- cwd: string,
1614
- event: string,
1615
- overlapId: string,
1616
- prior: UltragoalGoal,
1617
- next: UltragoalGoal,
1618
- ): UltragoalPipelineOverlapReceipt {
1619
- const paths = getUltragoalPaths(cwd, currentUltragoalSessionId(cwd));
1620
- return {
1621
- ok: true,
1622
- event,
1623
- overlap_id: overlapId,
1624
- prior_goal_id: prior.id,
1625
- next_goal_id: next.id,
1626
- status: next.pipelineMetadata?.overlap,
1627
- next_goal_status: next.status,
1628
- goals_path: paths.goalsPath,
1629
- ledger_path: paths.ledgerPath,
1630
- };
1631
- }
1632
-
1633
- export async function startUltragoalPipelineOverlap(input: {
1634
- cwd: string;
1635
- priorGoalId: string;
1636
- nextGoalId: string;
1637
- reviewHandles: JsonObject | JsonObject[];
1638
- qaHandles: JsonObject | JsonObject[];
1639
- implementationHandle: JsonObject;
1640
- }): Promise<UltragoalPipelineOverlapReceipt> {
1641
- const plan = await readUltragoalPlan(input.cwd);
1642
- if (!plan) throw new Error("No ultragoal plan found. Run `gjc ultragoal create-goals --brief ...` first.");
1643
- const prior = plan.goals.find(goal => goal.id === input.priorGoalId);
1644
- const next = plan.goals.find(goal => goal.id === input.nextGoalId);
1645
- if (!prior || !next) throw new Error("start-pipeline-overlap requires existing prior and next goal ids");
1646
- const reviewHandles = requireJsonObjectOrArrayValue(input.reviewHandles, "review handles");
1647
- const qaHandles = requireJsonObjectOrArrayValue(input.qaHandles, "QA handles");
1648
- requireJsonObjectValue(input.implementationHandle, "implementation handle");
1649
- requirePipelineStartable(plan, prior, next);
1650
- const now = new Date().toISOString();
1651
- const overlapId = `pipeline-${crypto.randomUUID()}`;
1652
- const priorMetadata = requireFreshPipelineMetadata(prior);
1653
- const nextMetadata = requireFreshPipelineMetadata(next);
1654
- const priorOpenMetadata = {
1655
- ...priorMetadata,
1656
- overlap: "open" as const,
1657
- overlapId,
1658
- priorGoalId: prior.id,
1659
- nextGoalId: next.id,
1660
- };
1661
- const nextOpenMetadata = {
1662
- ...nextMetadata,
1663
- overlap: "open" as const,
1664
- overlapId,
1665
- priorGoalId: prior.id,
1666
- nextGoalId: next.id,
1667
- };
1668
- prior.pipelineMetadata = { ...priorOpenMetadata, metadataHash: hashPipelineMetadata(priorOpenMetadata) };
1669
- next.pipelineMetadata = { ...nextOpenMetadata, metadataHash: hashPipelineMetadata(nextOpenMetadata) };
1670
- prior.updatedAt = now;
1671
- next.updatedAt = now;
1672
- next.status = "active";
1673
- next.startedAt = next.startedAt ?? now;
1674
- plan.updatedAt = now;
1675
- await writePlan(input.cwd, plan);
1676
- const refs = pipelineEventRefs(prior, next);
1677
- const expectedReviewHandleIds = handleIdsFromValue(reviewHandles, "review");
1678
- const expectedQaHandleIds = handleIdsFromValue(qaHandles, "QA");
1679
- await appendLedger(input.cwd, {
1680
- event: "pipeline_overlap_started",
1681
- eventId: crypto.randomUUID(),
1682
- timestamp: now,
1683
- schemaVersion: 1,
1684
- overlapId,
1685
- priorGoalId: prior.id,
1686
- nextGoalId: next.id,
1687
- reviewHandles,
1688
- reviewHandleIds: expectedReviewHandleIds,
1689
- qaHandles,
1690
- qaHandleIds: expectedQaHandleIds,
1691
- implementationHandle: input.implementationHandle,
1692
- ...refs,
1693
- });
1694
- await appendLedger(input.cwd, { event: "goal_started", goalId: next.id, pipelineOverlapId: overlapId });
1695
- return pipelineReceipt(input.cwd, "pipeline_overlap_started", overlapId, prior, next);
1696
- }
1697
-
1698
- function resultStatus(value: JsonObject, fieldName: string): string {
1699
- const status = nonEmptyString(value.status) ?? nonEmptyString(value.verdict) ?? nonEmptyString(value.result);
1700
- if (!status) throw new Error(`${fieldName} requires status, verdict, or result`);
1701
- return status.toLowerCase();
1702
- }
1703
-
1704
- function requireCleanPipelineResult(result: JsonObject, expectedHandleIds: readonly string[], fieldName: string): void {
1705
- const status = resultStatus(result, fieldName);
1706
- if (!["passed", "pass", "approved", "clear"].includes(status)) throw new Error(`${fieldName} did not pass`);
1707
- const evidence = nonEmptyString(result.evidence);
1708
- if (!evidence || !isSubstantiveEvidence(evidence)) throw new Error(`${fieldName} requires substantive evidence`);
1709
- requireCoveredHandles(expectedHandleIds, resultHandleIds(result, fieldName), fieldName);
1710
- if (collectPipelineBlockerFootprints(result, fieldName).length > 0)
1711
- throw new Error(`${fieldName} cannot clean-join with blockers`);
1712
- }
1713
-
1714
- function pipelineStartEventHandleIds(
1715
- ledger: UltragoalLedgerEvent[],
1716
- overlapId: string,
1717
- ): { review: string[]; qa: string[] } {
1718
- const event = ledger.find(row => row.event === "pipeline_overlap_started" && row.overlapId === overlapId) as
1719
- | JsonObject
1720
- | undefined;
1721
- if (!event) throw new Error(`No pipeline_overlap_started event found for ${overlapId}`);
1722
- const review =
1723
- stringArray(event.reviewHandleIds) ??
1724
- handleIdsFromValue(requireJsonObjectOrArrayValue(event.reviewHandles, "review handles"), "review");
1725
- const qa =
1726
- stringArray(event.qaHandleIds) ??
1727
- handleIdsFromValue(requireJsonObjectOrArrayValue(event.qaHandles, "QA handles"), "QA");
1728
- return { review, qa };
1729
- }
1730
-
1731
- export async function joinUltragoalPipelineOverlap(input: {
1732
- cwd: string;
1733
- overlapId: string;
1734
- reviewResult: JsonObject;
1735
- qaResult: JsonObject;
1736
- }): Promise<UltragoalPipelineOverlapReceipt> {
1737
- const plan = await readUltragoalPlan(input.cwd);
1738
- if (!plan) throw new Error("No ultragoal plan found. Run `gjc ultragoal create-goals --brief ...` first.");
1739
- const overlap = openPipelineOverlap(plan);
1740
- if (!overlap || overlap.overlapId !== input.overlapId)
1741
- throw new Error(`No open pipeline overlap found for ${input.overlapId}`);
1742
- const { prior, next, overlapId } = overlap;
1743
- const nextMetadata = requireFreshPipelineMetadata(next);
1744
- const ledger = await readUltragoalLedger(input.cwd);
1745
- const expectedHandles = pipelineStartEventHandleIds(ledger, overlapId);
1746
- const reviewBlockers = collectPipelineBlockerFootprints(input.reviewResult, "review result");
1747
- const qaBlockers = collectPipelineBlockerFootprints(input.qaResult, "QA result");
1748
- const blockerFootprints = [...reviewBlockers, ...qaBlockers];
1749
- let state: UltragoalPipelineOverlapState;
1750
- let event: UltragoalPipelineLedgerEventName;
1751
- if (blockerFootprints.length === 0) {
1752
- try {
1753
- requireCleanPipelineResult(input.reviewResult, expectedHandles.review, "review result");
1754
- requireCleanPipelineResult(input.qaResult, expectedHandles.qa, "QA result");
1755
- state = "joined_clean";
1756
- event = "pipeline_overlap_joined";
1757
- } catch {
1758
- state = "quarantine_required";
1759
- event = "pipeline_overlap_quarantined";
1760
- }
1761
- } else if (blockerFootprints.every(footprint => !targetsOverlap(footprint, nextMetadata.targets))) {
1762
- state = "blocked_disjoint_continue";
1763
- event = "pipeline_overlap_joined";
1764
- } else {
1765
- state = "quarantine_required";
1766
- event = "pipeline_overlap_quarantined";
1767
- }
1768
- const now = new Date().toISOString();
1769
- for (const goal of [prior, next]) {
1770
- const metadata = requireFreshPipelineMetadata(goal);
1771
- const joinedMetadata = { ...metadata, overlap: state, blockerFootprints };
1772
- goal.pipelineMetadata = { ...joinedMetadata, metadataHash: hashPipelineMetadata(joinedMetadata) };
1773
- goal.updatedAt = now;
1774
- }
1775
- if (state === "quarantine_required") next.status = "blocked";
1776
- plan.updatedAt = now;
1777
- await writePlan(input.cwd, plan);
1778
- await appendLedger(input.cwd, {
1779
- event,
1780
- eventId: crypto.randomUUID(),
1781
- timestamp: now,
1782
- schemaVersion: 1,
1783
- overlapId,
1784
- priorGoalId: prior.id,
1785
- nextGoalId: next.id,
1786
- status: state,
1787
- reviewResult: input.reviewResult,
1788
- qaResult: input.qaResult,
1789
- blockerFootprints,
1790
- ...pipelineEventRefs(prior, next),
1791
- });
1792
- return pipelineReceipt(input.cwd, event, overlapId, prior, next);
1793
- }
1582
+ import {
1583
+ joinUltragoalPipelineOverlap,
1584
+ rebaselineUltragoalPipelineOverlap,
1585
+ startUltragoalPipelineOverlap,
1586
+ } from "./ultragoal-pipeline";
1794
1587
 
1795
- export async function rebaselineUltragoalPipelineOverlap(input: {
1796
- cwd: string;
1797
- overlapId: string;
1798
- goalId: string;
1799
- evidence: string;
1800
- targetState: JsonObject;
1801
- }): Promise<UltragoalPipelineOverlapReceipt> {
1802
- const plan = await readUltragoalPlan(input.cwd);
1803
- if (!plan) throw new Error("No ultragoal plan found. Run `gjc ultragoal create-goals --brief ...` first.");
1804
- const goal = plan.goals.find(item => item.id === input.goalId);
1805
- if (!goal) throw new Error(`No ultragoal goal found for ${input.goalId}.`);
1806
- const evidence = input.evidence.trim();
1807
- if (!isSubstantiveEvidence(evidence)) throw new Error("rebaseline-pipeline-overlap requires substantive evidence");
1808
- const targetState = requireNonEmptyPipelineTargets(input.targetState, "target state");
1809
- const metadata = requireFreshPipelineMetadata(goal);
1810
- if (metadata.overlap !== "quarantine_required" || metadata.overlapId !== input.overlapId) {
1811
- throw new Error(`Goal ${goal.id} is not quarantined for overlap ${input.overlapId}`);
1812
- }
1813
- for (const footprint of metadata.blockerFootprints ?? []) {
1814
- if (targetsOverlap(footprint, targetState))
1815
- throw new Error("rebaseline-pipeline-overlap target state overlaps unresolved blocker footprints");
1816
- }
1817
- const now = new Date().toISOString();
1818
- const rebaselinedMetadata = { ...metadata, overlap: "rebaseline_complete" as const, targets: targetState };
1819
- goal.pipelineMetadata = { ...rebaselinedMetadata, metadataHash: hashPipelineMetadata(rebaselinedMetadata) };
1820
- goal.status = "active";
1821
- goal.evidence = evidence;
1822
- goal.updatedAt = now;
1823
- plan.updatedAt = now;
1824
- await writePlan(input.cwd, plan);
1825
- const peer = pipelinePeer(plan, metadata);
1826
- await appendLedger(input.cwd, {
1827
- event: "pipeline_overlap_rebaselined",
1828
- eventId: crypto.randomUUID(),
1829
- timestamp: now,
1830
- schemaVersion: 1,
1831
- overlapId: input.overlapId,
1832
- priorGoalId: metadata.priorGoalId ?? peer?.id ?? "",
1833
- nextGoalId: metadata.nextGoalId ?? goal.id,
1834
- goalId: goal.id,
1835
- evidence,
1836
- targetState,
1837
- metadataHash: goal.pipelineMetadata.metadataHash,
1838
- });
1839
- const paths = getUltragoalPaths(input.cwd, currentUltragoalSessionId(input.cwd));
1840
- return {
1841
- ok: true,
1842
- event: "pipeline_overlap_rebaselined",
1843
- overlap_id: input.overlapId,
1844
- prior_goal_id: metadata.priorGoalId ?? peer?.id ?? "",
1845
- goal_id: goal.id,
1846
- status: goal.pipelineMetadata.overlap,
1847
- goals_path: paths.goalsPath,
1848
- ledger_path: paths.ledgerPath,
1849
- };
1850
- }
1588
+ export { joinUltragoalPipelineOverlap, rebaselineUltragoalPipelineOverlap, startUltragoalPipelineOverlap };
1851
1589
 
1852
1590
  export function getUltragoalRunCompletionState(
1853
1591
  plan: UltragoalPlan,
@@ -1902,11 +1640,11 @@ async function readStructuredValue(cwd: string, value: string): Promise<unknown>
1902
1640
  throw error;
1903
1641
  }
1904
1642
  }
1905
- function qualityGateObject(value: unknown): JsonObject | null {
1643
+ export function qualityGateObject(value: unknown): JsonObject | null {
1906
1644
  return typeof value === "object" && value !== null && !Array.isArray(value) ? (value as JsonObject) : null;
1907
1645
  }
1908
1646
 
1909
- function nonEmptyStringArray(value: unknown): string[] | null {
1647
+ export function nonEmptyStringArray(value: unknown): string[] | null {
1910
1648
  if (!Array.isArray(value)) return null;
1911
1649
  const strings = value.filter(item => typeof item === "string" && item.trim().length > 0);
1912
1650
  return strings.length === value.length && strings.length > 0 ? strings : null;
@@ -1923,20 +1661,20 @@ function requireEmptyBlockers(value: unknown, fieldName: string): void {
1923
1661
  throw new Error(`qualityGate ${fieldName} must be an empty blockers array`);
1924
1662
  }
1925
1663
  }
1926
- function requireQualityGateObject(value: unknown, fieldName: string): JsonObject {
1664
+ export function requireQualityGateObject(value: unknown, fieldName: string): JsonObject {
1927
1665
  const object = qualityGateObject(value);
1928
1666
  if (!object) throw new Error(`qualityGate ${fieldName} must be an object`);
1929
1667
  return object;
1930
1668
  }
1931
1669
 
1932
- function requireObjectArray(value: unknown, fieldName: string): JsonObject[] {
1670
+ export function requireObjectArray(value: unknown, fieldName: string): JsonObject[] {
1933
1671
  if (!Array.isArray(value) || value.length === 0) {
1934
1672
  throw new Error(`qualityGate ${fieldName} must be a non-empty object array`);
1935
1673
  }
1936
1674
  return value.map((item, index) => requireQualityGateObject(item, `${fieldName}[${index}]`));
1937
1675
  }
1938
1676
 
1939
- function requiredStringField(row: JsonObject, key: string, fieldName: string): string {
1677
+ export function requiredStringField(row: JsonObject, key: string, fieldName: string): string {
1940
1678
  const value = row[key];
1941
1679
  if (typeof value !== "string" || value.trim().length === 0) {
1942
1680
  const hint =
@@ -1985,7 +1723,7 @@ function requireSuccessfulRowOutcome(row: JsonObject, fieldName: string): void {
1985
1723
  }
1986
1724
  }
1987
1725
 
1988
- function requireStringLinks(value: unknown, fieldName: string): string[] {
1726
+ export function requireStringLinks(value: unknown, fieldName: string): string[] {
1989
1727
  const strings = nonEmptyStringArray(value);
1990
1728
  if (!strings) throw new Error(`qualityGate ${fieldName} must be a non-empty string array`);
1991
1729
  return strings.map(item => item.trim());
@@ -2006,7 +1744,7 @@ function buildRowIdMap(rows: JsonObject[], fieldName: string): Map<string, JsonO
2006
1744
  return ids;
2007
1745
  }
2008
1746
 
2009
- function requireResolvedLinks(ids: string[], map: Map<string, JsonObject>, fieldName: string): void {
1747
+ export function requireResolvedLinks(ids: string[], map: Map<string, JsonObject>, fieldName: string): void {
2010
1748
  for (const id of ids) {
2011
1749
  if (!map.has(id)) throw new Error(`qualityGate ${fieldName} references unknown id ${id}`);
2012
1750
  }
@@ -2022,11 +1760,11 @@ function successfulLinkedRows(ids: string[], map: Map<string, JsonObject>, field
2022
1760
  return rows;
2023
1761
  }
2024
1762
 
2025
- function normalizedEvidenceKind(row: JsonObject): string {
1763
+ export function normalizedEvidenceKind(row: JsonObject): string {
2026
1764
  return requiredStringField(row, "kind", "executorQa.artifactRefs[]").toLowerCase().replaceAll("_", "-");
2027
1765
  }
2028
1766
 
2029
- function evidenceKindMatches(kind: string, words: string[]): boolean {
1767
+ export function evidenceKindMatches(kind: string, words: string[]): boolean {
2030
1768
  return words.some(word => kind.includes(word));
2031
1769
  }
2032
1770
  function formatActualArtifactKinds(artifactIds: string[], kinds: string[]): string {
@@ -2038,10 +1776,10 @@ function formatExpectedKindWords(words: string[]): string {
2038
1776
  return words.map(word => `"${word}"`).join(", ");
2039
1777
  }
2040
1778
 
2041
- type SurfaceFamily = "web" | "cli" | "native" | "api-package" | "algorithm-math" | "unknown";
1779
+ export type SurfaceFamily = "web" | "cli" | "native" | "api-package" | "algorithm-math" | "unknown";
2042
1780
 
2043
- type UltragoalChangeStatus = "added" | "modified" | "deleted" | "renamed" | "copied" | "unknown";
2044
- type UltragoalChangeCategory =
1781
+ export type UltragoalChangeStatus = "added" | "modified" | "deleted" | "renamed" | "copied" | "unknown";
1782
+ export type UltragoalChangeCategory =
2045
1783
  | "code"
2046
1784
  | "generated-binding"
2047
1785
  | "tool"
@@ -2049,13 +1787,13 @@ type UltragoalChangeCategory =
2049
1787
  | "prompt-doc-behavior"
2050
1788
  | "docs-static"
2051
1789
  | "other";
2052
- interface UltragoalChangeSetPath extends JsonObject {
1790
+ export interface UltragoalChangeSetPath extends JsonObject {
2053
1791
  path: string;
2054
1792
  status: UltragoalChangeStatus;
2055
1793
  oldPath?: string;
2056
1794
  category?: UltragoalChangeCategory;
2057
1795
  }
2058
- interface UltragoalChangeSet extends JsonObject {
1796
+ export interface UltragoalChangeSet extends JsonObject {
2059
1797
  source: "checkpoint-git" | "review-pr" | "review-branch" | "review-worktree" | "review-spec";
2060
1798
  baseRef?: string;
2061
1799
  headRef?: string;
@@ -2077,7 +1815,7 @@ const MANDATORY_COMPUTER_CASE_IDS = [
2077
1815
  ] as const;
2078
1816
  const TOOLS_INDEX_PATH = "packages/coding-agent/src/tools/index.ts";
2079
1817
 
2080
- function normalizeRepoPath(value: string): string {
1818
+ export function normalizeRepoPath(value: string): string {
2081
1819
  return value.replaceAll("\\\\", "/").replace(/^\.\//, "");
2082
1820
  }
2083
1821
 
@@ -2085,7 +1823,7 @@ function isToolsIndexPath(value: string): boolean {
2085
1823
  return normalizeRepoPath(value) === TOOLS_INDEX_PATH;
2086
1824
  }
2087
1825
 
2088
- function categorizeComputerChangePath(value: string): UltragoalChangeCategory {
1826
+ export function categorizeComputerChangePath(value: string): UltragoalChangeCategory {
2089
1827
  const normalized = normalizeRepoPath(value);
2090
1828
  if (normalized.startsWith("crates/pi-natives/src/computer/")) return "code";
2091
1829
  if (/^packages\/natives\/native\/index\.(?:d\.ts|js)$/.test(normalized)) return "generated-binding";
@@ -2153,7 +1891,7 @@ function isComputerSpecificToolsIndexDiff(diff: string | undefined, targetPath:
2153
1891
  /** Settings registry file that holds ALL settings, most of them unrelated to computer control. */
2154
1892
  const SETTINGS_SCHEMA_PATH = "packages/coding-agent/src/config/settings-schema.ts";
2155
1893
 
2156
- function isSettingsSchemaPath(value: string): boolean {
1894
+ export function isSettingsSchemaPath(value: string): boolean {
2157
1895
  return normalizeRepoPath(value) === SETTINGS_SCHEMA_PATH;
2158
1896
  }
2159
1897
 
@@ -2245,7 +1983,7 @@ export function surfaceFamily(value: string): SurfaceFamily {
2245
1983
  return "unknown";
2246
1984
  }
2247
1985
 
2248
- function isLiveSurfaceFamily(family: SurfaceFamily): boolean {
1986
+ export function isLiveSurfaceFamily(family: SurfaceFamily): boolean {
2249
1987
  return family === "web" || family === "cli" || family === "native";
2250
1988
  }
2251
1989
 
@@ -2297,7 +2035,7 @@ function validateSurfaceArtifactCompatibility(
2297
2035
  }
2298
2036
  }
2299
2037
 
2300
- function isSubstantiveEvidence(value: unknown): boolean {
2038
+ export function isSubstantiveEvidence(value: unknown): boolean {
2301
2039
  if (typeof value !== "string") return false;
2302
2040
  const trimmed = value.trim();
2303
2041
  if (trimmed.length < MIN_SUBSTANTIVE_EVIDENCE_CHARS) return false;
@@ -2307,7 +2045,7 @@ function isSubstantiveEvidence(value: unknown): boolean {
2307
2045
  return !["todo", "tbd", "n/a", "na", "none", "placeholder", "empty", "stub"].includes(normalized);
2308
2046
  }
2309
2047
 
2310
- function hasTypedVerifiedReceipt(value: unknown): boolean {
2048
+ export function hasTypedVerifiedReceipt(value: unknown): boolean {
2311
2049
  const receipt = qualityGateObject(value);
2312
2050
  if (!receipt) return false;
2313
2051
  const type = nonEmptyString(receipt.type) ?? nonEmptyString(receipt.kind) ?? nonEmptyString(receipt.receiptType);
@@ -2316,7 +2054,7 @@ function hasTypedVerifiedReceipt(value: unknown): boolean {
2316
2054
  return Boolean(type && id && (status === "verified" || status === "passed"));
2317
2055
  }
2318
2056
 
2319
- async function hasExistingNonEmptyArtifact(cwd: string, value: unknown): Promise<boolean> {
2057
+ export async function hasExistingNonEmptyArtifact(cwd: string, value: unknown): Promise<boolean> {
2320
2058
  const artifactPath = nonEmptyString(value);
2321
2059
  if (!artifactPath) return false;
2322
2060
  const resolved = path.resolve(cwd, artifactPath);
@@ -2329,7 +2067,7 @@ async function hasExistingNonEmptyArtifact(cwd: string, value: unknown): Promise
2329
2067
  }
2330
2068
  }
2331
2069
 
2332
- async function readArtifactBytes(cwd: string, row: JsonObject, fieldName: string): Promise<Buffer | null> {
2070
+ export async function readArtifactBytes(cwd: string, row: JsonObject, fieldName: string): Promise<Buffer | null> {
2333
2071
  const artifactPath = nonEmptyString(row.path);
2334
2072
  if (!artifactPath) return null;
2335
2073
  const resolved = path.resolve(cwd, artifactPath);
@@ -2343,839 +2081,27 @@ async function readArtifactBytes(cwd: string, row: JsonObject, fieldName: string
2343
2081
  }
2344
2082
  }
2345
2083
 
2346
- const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
2347
- const JPEG_START_OF_IMAGE = 0xd8;
2348
- const JPEG_END_OF_IMAGE = 0xd9;
2349
- const JPEG_START_OF_SCAN = 0xda;
2350
- const JPEG_STANDALONE_MARKERS = new Set([0x01, 0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7]);
2351
- const PNG_CRC_TABLE = new Uint32Array(256).map((_, index) => {
2352
- let crc = index;
2353
- for (let bit = 0; bit < 8; bit++) crc = crc & 1 ? 0xedb88320 ^ (crc >>> 1) : crc >>> 1;
2354
- return crc >>> 0;
2355
- });
2356
-
2357
- function pngCrc32(bytes: Buffer): number {
2358
- let crc = 0xffffffff;
2359
- for (const byte of bytes) crc = PNG_CRC_TABLE[(crc ^ byte) & 0xff]! ^ (crc >>> 8);
2360
- return (crc ^ 0xffffffff) >>> 0;
2361
- }
2362
-
2363
- function parsePngDimensions(
2364
- bytes: Buffer,
2365
- ): { width: number; height: number; headerBytes: number; sampleBytes?: Buffer } | null {
2366
- if (bytes.length < 45) return null;
2367
- if (!bytes.subarray(0, 8).equals(PNG_SIGNATURE)) return null;
2368
- let offset = 8;
2369
- let width = 0;
2370
- let height = 0;
2371
- let sawIhdr = false;
2372
- let sawIdat = false;
2373
- const idatChunks: Buffer[] = [];
2374
- while (offset + 12 <= bytes.length) {
2375
- const chunkStart = offset;
2376
- const length = bytes.readUInt32BE(offset);
2377
- offset += 4;
2378
- const type = bytes.toString("ascii", offset, offset + 4);
2379
- offset += 4;
2380
- if (offset + length + 4 > bytes.length) return null;
2381
- const data = bytes.subarray(offset, offset + length);
2382
- offset += length;
2383
- const expectedCrc = bytes.readUInt32BE(offset);
2384
- offset += 4;
2385
- if (pngCrc32(bytes.subarray(chunkStart + 4, offset - 4)) !== expectedCrc) return null;
2386
- if (!sawIhdr) {
2387
- if (type !== "IHDR" || length !== 13) return null;
2388
- width = data.readUInt32BE(0);
2389
- height = data.readUInt32BE(4);
2390
- if (
2391
- width === 0 ||
2392
- height === 0 ||
2393
- data[8] !== 8 ||
2394
- ![2, 6].includes(data[9]!) ||
2395
- data[10] !== 0 ||
2396
- data[11] !== 0 ||
2397
- data[12] !== 0
2398
- )
2399
- return null;
2400
- sawIhdr = true;
2401
- } else if (type === "IHDR") return null;
2402
- if (type === "IDAT") {
2403
- if (!sawIhdr || length === 0) return null;
2404
- sawIdat = true;
2405
- idatChunks.push(data);
2406
- }
2407
- if (type === "IEND") {
2408
- if (length !== 0 || !sawIhdr || !sawIdat || offset !== bytes.length) return null;
2409
- try {
2410
- return { width, height, headerBytes: 8, sampleBytes: inflateSync(Buffer.concat(idatChunks)) };
2411
- } catch {
2412
- return null;
2413
- }
2414
- }
2415
- }
2416
- return null;
2417
- }
2418
-
2419
- function parseJpegDimensions(
2420
- bytes: Buffer,
2421
- ): { width: number; height: number; headerBytes: number; sampleBytes?: Buffer } | null {
2422
- if (bytes.length < 8 || bytes[0] !== 0xff || bytes[1] !== JPEG_START_OF_IMAGE) return null;
2423
- let offset = 2;
2424
- let dimensions: { width: number; height: number; headerBytes: number } | null = null;
2425
- let sawStartOfScan = false;
2426
- let scanStart = -1;
2427
- while (offset < bytes.length) {
2428
- if (bytes[offset] !== 0xff) return null;
2429
- while (offset < bytes.length && bytes[offset] === 0xff) offset++;
2430
- if (offset >= bytes.length) return null;
2431
- const marker = bytes[offset++];
2432
- if (marker === 0x00) return null;
2433
- if (marker === JPEG_END_OF_IMAGE) return null;
2434
- if (JPEG_STANDALONE_MARKERS.has(marker)) continue;
2435
- if (offset + 2 > bytes.length) return null;
2436
- const segmentLength = bytes.readUInt16BE(offset);
2437
- if (segmentLength < 2 || offset + segmentLength > bytes.length) return null;
2438
- const segmentDataEnd = offset + segmentLength;
2439
- if (marker === 0xc0 || marker === 0xc1 || marker === 0xc2) {
2440
- if (segmentLength < 8) return null;
2441
- dimensions = {
2442
- width: bytes.readUInt16BE(offset + 5),
2443
- height: bytes.readUInt16BE(offset + 3),
2444
- headerBytes: offset + segmentLength,
2445
- };
2446
- }
2447
- if (marker === JPEG_START_OF_SCAN) {
2448
- if (!dimensions || segmentDataEnd >= bytes.length) return null;
2449
- sawStartOfScan = true;
2450
- scanStart = segmentDataEnd;
2451
- break;
2452
- }
2453
- offset += segmentLength;
2454
- }
2455
- if (!dimensions || !sawStartOfScan || scanStart < 0) return null;
2456
- let scanOffset = scanStart;
2457
- let entropyBytes = 0;
2458
- while (scanOffset < bytes.length) {
2459
- const byte = bytes[scanOffset++]!;
2460
- if (byte !== 0xff) {
2461
- entropyBytes++;
2462
- continue;
2463
- }
2464
- if (scanOffset >= bytes.length) return null;
2465
- const marker = bytes[scanOffset++]!;
2466
- if (marker === 0x00) {
2467
- entropyBytes++;
2468
- continue;
2469
- }
2470
- if (JPEG_STANDALONE_MARKERS.has(marker)) continue;
2471
- if (marker === JPEG_END_OF_IMAGE) {
2472
- if (scanOffset !== bytes.length || entropyBytes < 32) return null;
2473
- return { ...dimensions, sampleBytes: bytes.subarray(scanStart, scanOffset - 2) };
2474
- }
2475
- return null;
2476
- }
2477
- return null;
2478
- }
2479
-
2480
- function unsupportedScreenshotFormat(bytes: Buffer): string | null {
2481
- if (bytes.toString("ascii", 0, 6) === "GIF87a" || bytes.toString("ascii", 0, 6) === "GIF89a") return "GIF";
2482
- if (bytes.toString("ascii", 0, 2) === "BM") return "BMP";
2483
- if (bytes.length >= 12 && bytes.toString("ascii", 0, 4) === "RIFF" && bytes.toString("ascii", 8, 12) === "WEBP")
2484
- return "WebP";
2485
- return null;
2486
- }
2487
-
2488
- function parseImageDimensions(
2489
- bytes: Buffer,
2490
- ): { width: number; height: number; headerBytes: number; sampleBytes?: Buffer } | null {
2491
- return parsePngDimensions(bytes) ?? parseJpegDimensions(bytes);
2492
- }
2493
-
2494
- function hasNonUniformImageBytes(bytes: Buffer, headerBytes: number, sampleBytes?: Buffer): boolean {
2495
- const source = sampleBytes ?? bytes;
2496
- const sampleStart = sampleBytes ? 0 : Math.min(Math.max(headerBytes, 0), source.length);
2497
- const sampleLength = source.length - sampleStart;
2498
- if (sampleLength < 32) return false;
2499
- const windows: Buffer[] = [];
2500
- for (let index = 0; index < 64; index++) {
2501
- const offset = sampleStart + Math.floor(((sampleLength - 32) * index) / 63);
2502
- windows.push(source.subarray(offset, offset + 32));
2503
- }
2504
- const byteCounts = new Map<number, number>();
2505
- let total = 0;
2506
- for (const window of windows) {
2507
- for (const byte of window) {
2508
- byteCounts.set(byte, (byteCounts.get(byte) ?? 0) + 1);
2509
- total++;
2510
- }
2511
- }
2512
- const first = windows[0]!;
2513
- const differingWindows = windows.slice(1).filter(window => !window.equals(first)).length;
2514
- const maxCount = Math.max(...byteCounts.values());
2515
- return byteCounts.size >= 16 && differingWindows >= 8 && maxCount / total <= 0.95;
2516
- }
2517
-
2518
- async function validateScreenshotArtifact(cwd: string, row: JsonObject, fieldName: string): Promise<boolean> {
2519
- const bytes = await readArtifactBytes(cwd, row, fieldName);
2520
- if (!bytes) throw new Error(`qualityGate ${fieldName} screenshot artifact path must resolve to an existing file`);
2521
- if (bytes.length < 4096) throw new Error(`qualityGate ${fieldName} screenshot artifact must be at least 4096 bytes`);
2522
- const unsupportedFormat = unsupportedScreenshotFormat(bytes);
2523
- if (unsupportedFormat) {
2524
- throw new Error(
2525
- `qualityGate ${fieldName} unsupported/undecodable screenshot format ${unsupportedFormat}; use PNG or fully marker-validated JPEG`,
2526
- );
2527
- }
2528
- const dimensions = parseImageDimensions(bytes);
2529
- if (!dimensions)
2530
- throw new Error(`qualityGate ${fieldName} screenshot artifact must be a decodable PNG or JPEG image`);
2531
- if (dimensions.width < 320 || dimensions.height < 180) {
2532
- throw new Error(`qualityGate ${fieldName} screenshot artifact must be at least 320x180 pixels`);
2533
- }
2534
- if (!hasNonUniformImageBytes(bytes, dimensions.headerBytes, dimensions.sampleBytes)) {
2535
- throw new Error(
2536
- `qualityGate ${fieldName} screenshot artifact must be non-uniform, not blank, solid, tiny, or placeholder imagery`,
2537
- );
2538
- }
2539
- return true;
2540
- }
2541
-
2542
- function normalizeTranscriptTimestamp(value: unknown): number | null {
2543
- if (typeof value === "number" && Number.isFinite(value)) return value;
2544
- if (typeof value !== "string" || value.trim().length === 0) return null;
2545
- const numeric = Number(value);
2546
- if (Number.isFinite(numeric)) return numeric;
2547
- const parsed = Date.parse(value);
2548
- return Number.isFinite(parsed) ? parsed : null;
2549
- }
2550
-
2551
- function transcriptSurfaceCompatible(value: unknown, family: SurfaceFamily): boolean {
2552
- const surface = nonEmptyString(value);
2553
- return !surface || family === "unknown" || surfaceFamily(surface) === family;
2554
- }
2555
-
2556
- function actionSelectorRequired(type: string): boolean {
2557
- return ["click", "fill", "press", "assert", "screenshot", "observe"].includes(type);
2558
- }
2559
-
2560
- async function validateAutomationTranscriptArtifact(
2561
- cwd: string,
2562
- row: JsonObject,
2563
- fieldName: string,
2564
- options: { surfaceFamily: SurfaceFamily },
2565
- ): Promise<boolean> {
2566
- const bytes = await readArtifactBytes(cwd, row, fieldName);
2567
- if (!bytes) throw new Error(`qualityGate ${fieldName} automation transcript path must resolve to an existing file`);
2568
- let transcript: JsonObject;
2569
- try {
2570
- const parsed = JSON.parse(bytes.toString("utf8"));
2571
- transcript = requireQualityGateObject(parsed, `${fieldName}.transcript`);
2572
- } catch (error) {
2573
- throw new Error(`qualityGate ${fieldName} automation transcript must be valid JSON: ${String(error)}`);
2574
- }
2575
- if (transcript.schemaVersion !== 1)
2576
- throw new Error(`qualityGate ${fieldName} automation transcript schemaVersion must be 1`);
2577
- if (!transcriptSurfaceCompatible(transcript.surface, options.surfaceFamily)) {
2578
- throw new Error(
2579
- `qualityGate ${fieldName} automation transcript surface is not compatible with ${options.surfaceFamily}`,
2580
- );
2581
- }
2582
- if (!nonEmptyString(transcript.tool))
2583
- throw new Error(`qualityGate ${fieldName} automation transcript tool must be non-empty`);
2584
- const actions = requireObjectArray(transcript.actions, `${fieldName}.actions`);
2585
- if (actions.length < 1) throw new Error(`qualityGate ${fieldName} automation transcript actions must be non-empty`);
2586
- const assertionsValue = transcript.assertions;
2587
- const assertions =
2588
- assertionsValue === undefined ? [] : requireObjectArray(assertionsValue, `${fieldName}.assertions`);
2589
- const timestamps: number[] = [];
2590
- let hasSelectorBearingEntry = false;
2591
- for (const [index, action] of actions.entries()) {
2592
- const actionField = `${fieldName}.actions[${index}]`;
2593
- const type = requiredStringField(action, "type", actionField).toLowerCase();
2594
- const timestamp = normalizeTranscriptTimestamp(action.timestamp);
2595
- if (timestamp === null) throw new Error(`qualityGate ${actionField}.timestamp must be present and parseable`);
2596
- timestamps.push(timestamp);
2597
- const selector = nonEmptyString(action.selector);
2598
- if (actionSelectorRequired(type) && !selector)
2599
- throw new Error(`qualityGate ${actionField}.selector must be non-empty`);
2600
- if (type === "goto" && !nonEmptyString(action.url))
2601
- throw new Error(`qualityGate ${actionField}.url must be non-empty`);
2602
- if (type === "custom" && !selector && !nonEmptyString(action.target)) {
2603
- throw new Error(`qualityGate ${actionField}.selector or target must be non-empty`);
2604
- }
2605
- if (selector) hasSelectorBearingEntry = true;
2606
- }
2607
- for (const [index, assertion] of assertions.entries()) {
2608
- const assertionField = `${fieldName}.assertions[${index}]`;
2609
- const timestamp = normalizeTranscriptTimestamp(assertion.timestamp);
2610
- if (timestamp === null) throw new Error(`qualityGate ${assertionField}.timestamp must be present and parseable`);
2611
- timestamps.push(timestamp);
2612
- if (nonEmptyString(assertion.status)?.toLowerCase() !== PASSED_STATUS) {
2613
- throw new Error(`qualityGate ${assertionField}.status must be passed`);
2614
- }
2615
- if (nonEmptyString(assertion.selector)) hasSelectorBearingEntry = true;
2616
- }
2617
- for (let index = 1; index < timestamps.length; index++) {
2618
- if (timestamps[index]! < timestamps[index - 1]!) {
2619
- throw new Error(`qualityGate ${fieldName} automation transcript timestamps must be monotonic non-decreasing`);
2620
- }
2621
- }
2622
- if (!hasSelectorBearingEntry) {
2623
- throw new Error(
2624
- `qualityGate ${fieldName} automation transcript must include at least one selector-bearing action or assertion`,
2625
- );
2626
- }
2627
- return true;
2628
- }
2629
-
2630
- async function validatePtyCaptureArtifact(cwd: string, row: JsonObject, fieldName: string): Promise<boolean> {
2631
- const bytes = await readArtifactBytes(cwd, row, fieldName);
2632
- if (!bytes) throw new Error(`qualityGate ${fieldName} PTY capture path must resolve to an existing file`);
2633
- if (bytes.length < 512) throw new Error(`qualityGate ${fieldName} PTY capture must be at least 512 bytes`);
2634
- const text = bytes.toString("utf8");
2635
- const hasCsi = /\x1b\[[0-?]*[ -/]*[@-~]/.test(text);
2636
- const hasOsc = /\x1b\][^\x07]*(?:\x07|\x1b\\)/.test(text);
2637
- const hasAltOrCursor = /\x1b\[\?1049[hl]|\x1b\[H|\x1b\[2J/.test(text);
2638
- const hasRedraw = /[\r\b]/.test(text) && hasCsi;
2639
- if (!hasCsi && !hasOsc && !hasAltOrCursor && !hasRedraw) {
2640
- throw new Error(`qualityGate ${fieldName} PTY capture must contain terminal control sequences`);
2641
- }
2642
- if (!/[\x20-\x7e]{10,}/.test(text)) {
2643
- throw new Error(
2644
- `qualityGate ${fieldName} PTY capture must contain a printable text run of at least 10 characters`,
2645
- );
2646
- }
2647
- return true;
2648
- }
2649
-
2650
- function structuralArtifactKind(row: JsonObject): "screenshot" | "automation" | "pty" | null {
2651
- const kind = normalizedEvidenceKind(row);
2652
- if (evidenceKindMatches(kind, ["screenshot", "image", "visual"])) return "screenshot";
2653
- if (evidenceKindMatches(kind, ["browser", "playwright", "pandawright", "automation", "app-automation"]))
2654
- return "automation";
2655
- if (evidenceKindMatches(kind, ["pty", "tui", "terminal-capture"])) return "pty";
2656
- return null;
2657
- }
2658
-
2659
- async function validateStructuralArtifact(
2660
- cwd: string,
2661
- row: JsonObject,
2662
- fieldName: string,
2663
- options: { surfaceFamily: SurfaceFamily; live: boolean },
2664
- ): Promise<boolean> {
2665
- void options.live;
2666
- const kind = structuralArtifactKind(row);
2667
- if (!kind) return false;
2668
- if (kind === "screenshot") return validateScreenshotArtifact(cwd, row, fieldName);
2669
- if (kind === "automation") return validateAutomationTranscriptArtifact(cwd, row, fieldName, options);
2670
- if (kind === "pty") return validatePtyCaptureArtifact(cwd, row, fieldName);
2671
- return false;
2672
- }
2673
-
2674
- const CLI_REPLAY_MAX_OUTPUT_BYTES = 1024 * 1024;
2675
- const CLI_REPLAY_DEFAULT_TIMEOUT_MS = 10_000;
2676
- const CLI_REPLAY_MIN_TIMEOUT_MS = 1_000;
2677
- const CLI_REPLAY_MAX_TIMEOUT_MS = 30_000;
2678
- const CLI_REPLAY_EXEMPT_REASON_CODES = [
2679
- "unsafe_side_effect",
2680
- "requires_credentials",
2681
- "requires_network",
2682
- "non_deterministic_external",
2683
- "destructive",
2684
- "interactive_only",
2685
- "platform_unavailable",
2686
- ] as const;
2687
- const CLI_REPLAY_EXEMPT_REASON_CODE_SET = new Set<string>(CLI_REPLAY_EXEMPT_REASON_CODES);
2688
- const CLI_REPLAY_ENV_BASE: Record<string, string> = { CI: "1", NO_COLOR: "1", GJC_ULTRAGOAL_REPLAY: "1" };
2689
- const CLI_REPLAY_EXEMPT_REASON_CODE_LIST = CLI_REPLAY_EXEMPT_REASON_CODES.join(", ");
2690
- const CLI_REPLAY_SAFE_ENV_NAMES = new Set(["LANG", "LC_ALL", "LC_CTYPE", "TZ"]);
2691
- const CLI_REPLAY_DANGEROUS_ENV_NAME_PATTERN =
2692
- /^(?:NODE_OPTIONS|GIT_EXTERNAL_DIFF|GIT_SSH|GIT_SSH_COMMAND|GIT_PAGER|PATH|LD_PRELOAD|LD_LIBRARY_PATH)$|^(?:GIT_CONFIG|DYLD_|BUN_|NPM_CONFIG_)|(?:^|_)OPTIONS$|PRELOAD$/;
2693
- const ANSI_ESCAPE_PATTERN = /\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\)|[@-Z\\-_])/g;
2694
-
2695
- function clampCliReplayTimeout(value: unknown): number {
2696
- if (value === undefined) return CLI_REPLAY_DEFAULT_TIMEOUT_MS;
2697
- if (typeof value !== "number" || !Number.isFinite(value)) {
2698
- throw new Error("qualityGate CLI replay timeoutMs must be a finite number");
2699
- }
2700
- return Math.min(CLI_REPLAY_MAX_TIMEOUT_MS, Math.max(CLI_REPLAY_MIN_TIMEOUT_MS, Math.trunc(value)));
2701
- }
2702
-
2703
- function basenameCommand(value: string): string {
2704
- return path.basename(value).toLowerCase();
2705
- }
2706
-
2707
- function isDeterministicConsoleLogReplay(code: string): boolean {
2708
- let remaining = code.trim();
2709
- if (remaining.length === 0) return false;
2710
- let matched = false;
2711
- while (remaining.length > 0) {
2712
- const match =
2713
- /^console\.log\(\s*("(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'|`(?:\\[\s\S]|[^`\\$])*`)\s*\)\s*;?\s*/.exec(
2714
- remaining,
2715
- );
2716
- if (!match) return false;
2717
- const statement = match[0]!;
2718
- const literal = match[1]!;
2719
- if (literal.startsWith("`") && literal.includes("${")) return false;
2720
- matched = true;
2721
- remaining = remaining.slice(statement.length);
2722
- }
2723
- return matched;
2724
- }
2725
-
2726
- function hasShellRedirectionToken(value: string): boolean {
2727
- return /^(?:[<>]|\d?[<>]|\d?>&\d|\|\|?|&&|;)$/.test(value) || /(?:^|[^\w])-?>/.test(value);
2728
- }
2729
-
2730
- function isSafeRefOrPathspec(value: string): boolean {
2731
- return value.length > 0 && !value.startsWith("-") && !/[\0\n\r]/.test(value) && !hasShellRedirectionToken(value);
2732
- }
2733
-
2734
- function isAllowedGitReplayCommand(args: readonly string[]): boolean {
2735
- const subcommand = args[0];
2736
- const rest = args.slice(1);
2737
- if (subcommand === "status") return rest.every(arg => ["--short", "--porcelain", "--branch"].includes(arg));
2738
- if (subcommand === "rev-parse" || subcommand === "merge-base")
2739
- return rest.length > 0 && rest.every(isSafeRefOrPathspec);
2740
- if (subcommand !== "diff" && subcommand !== "show" && subcommand !== "log") return false;
2741
- let pathspecMode = false;
2742
- for (const arg of rest) {
2743
- if (arg === "--") {
2744
- pathspecMode = true;
2745
- continue;
2746
- }
2747
- if (pathspecMode) {
2748
- if (!isSafeRefOrPathspec(arg)) return false;
2749
- continue;
2750
- }
2751
- if (["--stat", "--name-only", "--oneline", "--no-ext-diff"].includes(arg)) continue;
2752
- if (!isSafeRefOrPathspec(arg)) return false;
2753
- }
2754
- return true;
2755
- }
2756
-
2757
- function isBareExecutableName(value: string): boolean {
2758
- // The allowlist is keyed on the basename, but the raw command[0] is what gets spawned.
2759
- // Reject path-qualified or case-spoofed executables (e.g. ./git, /tmp/npm, scripts/node, GIT)
2760
- // so an attacker-controlled binary cannot impersonate a trusted tool.
2761
- return (
2762
- value.length > 0 &&
2763
- !value.includes("/") &&
2764
- !value.includes("\\") &&
2765
- value === path.basename(value) &&
2766
- value === value.toLowerCase()
2767
- );
2768
- }
2769
-
2770
- function isAllowedCliReplayCommand(command: readonly string[]): boolean {
2771
- if (
2772
- command.length === 0 ||
2773
- command.some(arg => arg.trim() !== arg || arg.length === 0 || hasShellRedirectionToken(arg))
2774
- )
2775
- return false;
2776
- if (!isBareExecutableName(command[0]!)) return false;
2777
- const executable = basenameCommand(command[0]!);
2778
- const args = command.slice(1);
2779
- if (executable === "bun" || executable === "node") {
2780
- if (args.length === 1 && args[0] === "--version") return true;
2781
- return args.length === 2 && args[0] === "-e" && isDeterministicConsoleLogReplay(args[1]!);
2782
- }
2783
- if (executable === "npm" || executable === "pnpm" || executable === "yarn") {
2784
- return (args.length === 1 && args[0] === "--version") || (args.length === 1 && args[0] === "list");
2785
- }
2786
- if (executable === "git") return isAllowedGitReplayCommand(args);
2787
- if (executable === "gjc") return args.length === 1 && ["read", "status"].includes(args[0] ?? "");
2788
- return false;
2789
- }
2790
- function summarizeBlockedCliReplayCommand(command: readonly string[]): string {
2791
- const executable = command[0] ? basenameCommand(command[0]) : "<missing>";
2792
- const argCount = Math.max(0, command.length - 1);
2793
- return `${JSON.stringify(executable)} with ${argCount} arg${argCount === 1 ? "" : "s"}`;
2794
- }
2795
-
2796
- function cliReplayAllowlistDescription(): string {
2797
- return [
2798
- '`bun --version`, `node --version`, or deterministic `bun/node -e "console.log(...)"`',
2799
- "`npm|pnpm|yarn --version` or `npm|pnpm|yarn list`",
2800
- "read-only `git status|rev-parse|merge-base|diff|show|log` with safe args",
2801
- "`gjc read` or `gjc status`",
2802
- ].join("; ");
2803
- }
2804
-
2805
- function resolveCliReplayCommand(command: string[]): string[] {
2806
- if (basenameCommand(command[0]!) === "bun") return [process.execPath, ...command.slice(1)];
2807
- return command;
2808
- }
2809
-
2810
- function resolveUnderCwd(cwd: string, replayCwd: unknown, fieldName: string): string {
2811
- const relative = replayCwd === undefined ? "." : nonEmptyString(replayCwd);
2812
- if (!relative) throw new Error(`qualityGate ${fieldName}.cwd must be a non-empty string when provided`);
2813
- const root = path.resolve(cwd);
2814
- const resolved = path.resolve(root, relative);
2815
- const relativeToRoot = path.relative(root, resolved);
2816
- if (relativeToRoot === ".." || relativeToRoot.startsWith(`..${path.sep}`) || path.isAbsolute(relativeToRoot)) {
2817
- throw new Error(`qualityGate ${fieldName}.cwd must resolve under the repository cwd`);
2818
- }
2819
- return resolved;
2820
- }
2821
-
2822
- function buildCliReplayEnv(value: unknown, fieldName: string): Record<string, string> {
2823
- const env: Record<string, string> = { ...CLI_REPLAY_ENV_BASE };
2824
- if (value === undefined) return env;
2825
- const object = requireQualityGateObject(value, `${fieldName}.env`);
2826
- for (const [key, envValue] of Object.entries(object)) {
2827
- if (!/^[A-Z_][A-Z0-9_]*$/.test(key))
2828
- throw new Error(`qualityGate ${fieldName}.env.${key} must be an uppercase environment key`);
2829
- if (CLI_REPLAY_DANGEROUS_ENV_NAME_PATTERN.test(key) || !CLI_REPLAY_SAFE_ENV_NAMES.has(key)) {
2830
- throw new Error(`qualityGate ${fieldName}.env.${key} is not in the CLI replay safe environment allowlist`);
2831
- }
2832
- if (typeof envValue !== "string") throw new Error(`qualityGate ${fieldName}.env.${key} must be a string`);
2833
- env[key] = envValue;
2834
- }
2835
- return env;
2836
- }
2837
-
2838
- function normalizeCliReplayOutput(value: string, cwd: string): string {
2839
- let normalized = value.replace(ANSI_ESCAPE_PATTERN, "").replace(/\r\n?/g, "\n");
2840
- const home = process.env.HOME;
2841
- const replacements: Array<[RegExp, string]> = [
2842
- [/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\b/g, "<TIMESTAMP>"],
2843
- [/\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/gi, "<UUID>"],
2844
- [/\b[0-9a-f]{7,}\b/gi, "<HASH>"],
2845
- [/(?:\/private)?\/var\/folders\/[^\s"']+|\/tmp\/[^\s"']+|\/var\/tmp\/[^\s"']+/g, "<TMP>"],
2846
- ];
2847
- for (const candidate of [path.resolve(cwd), home]) {
2848
- if (!candidate) continue;
2849
- const escaped = candidate.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2850
- normalized = normalized.replace(new RegExp(escaped, "g"), candidate === home ? "<HOME>" : "<CWD>");
2851
- }
2852
- for (const [pattern, replacement] of replacements) normalized = normalized.replace(pattern, replacement);
2853
- const lines = normalized.split("\n").map(line => line.replace(/[ \t]+$/g, ""));
2854
- while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
2855
- return lines.join("\n");
2856
- }
2857
-
2858
- async function readCliReplayRecord(cwd: string, row: JsonObject, fieldName: string): Promise<JsonObject | null> {
2859
- const inline = qualityGateObject(row.replay) ?? (row.kind === "cli-replay" ? row : null);
2860
- if (inline) return inline;
2861
- if (!evidenceKindMatches(normalizedEvidenceKind(row), ["cli-replay", "command-replay"])) return null;
2862
- const bytes = await readArtifactBytes(cwd, row, fieldName);
2863
- if (!bytes) return null;
2864
- try {
2865
- return requireQualityGateObject(JSON.parse(bytes.toString("utf8")), `${fieldName}.replay`);
2866
- } catch (error) {
2867
- throw new Error(`qualityGate ${fieldName} CLI replay artifact must be valid JSON: ${String(error)}`);
2868
- }
2869
- }
2870
-
2871
- function parseCliReplayRecord(
2872
- record: JsonObject,
2873
- fieldName: string,
2874
- ): {
2875
- command: string[];
2876
- replayCwd: unknown;
2877
- env: Record<string, string>;
2878
- timeoutMs: number;
2879
- expectedExitCode: number;
2880
- recordedStdout: string;
2881
- invariants: JsonObject[];
2882
- } {
2883
- if (record.schemaVersion !== 1) throw new Error(`qualityGate ${fieldName}.schemaVersion must be 1`);
2884
- if (record.kind !== "cli-replay") throw new Error(`qualityGate ${fieldName}.kind must be cli-replay`);
2885
- if (record.command !== undefined && typeof record.command === "string") {
2886
- throw new Error(`qualityGate ${fieldName}.command must be an argv string array, not a shell string`);
2887
- }
2888
- const command = nonEmptyStringArray(record.command);
2889
- if (!command) throw new Error(`qualityGate ${fieldName}.command must be a non-empty string array`);
2890
- if (record.replaySafe !== true)
2891
- throw new Error(`qualityGate ${fieldName}.replaySafe must be true before CLI replay executes`);
2892
- if (!isAllowedCliReplayCommand(command)) {
2893
- throw new Error(
2894
- `qualityGate ${fieldName}.command is not in the conservative CLI replay allowlist; command ${summarizeBlockedCliReplayCommand(command)} is blocked. Allowed replay commands: ${cliReplayAllowlistDescription()}. For other commands, provide audited replayExempt metadata with reasonCode, reason, approvedBy, and fallbackArtifactRefs that point to a structurally valid fallback artifact.`,
2895
- );
2896
- }
2897
- if (record.normalization !== undefined && record.normalization !== "default") {
2898
- throw new Error(`qualityGate ${fieldName}.normalization must be default when provided`);
2899
- }
2900
- if (typeof record.recordedStdout !== "string")
2901
- throw new Error(`qualityGate ${fieldName}.recordedStdout must be a string`);
2902
- if (record.recordedStderr !== undefined && typeof record.recordedStderr !== "string") {
2903
- throw new Error(`qualityGate ${fieldName}.recordedStderr must be a string when provided`);
2904
- }
2905
- const expectedExitCode = record.expectedExitCode === undefined ? 0 : record.expectedExitCode;
2906
- if (typeof expectedExitCode !== "number" || !Number.isInteger(expectedExitCode)) {
2907
- throw new Error(`qualityGate ${fieldName}.expectedExitCode must be an integer`);
2908
- }
2909
- const invariants =
2910
- record.invariants === undefined ? [] : requireObjectArray(record.invariants, `${fieldName}.invariants`);
2911
- return {
2912
- command: command.map(item => item.trim()),
2913
- replayCwd: record.cwd,
2914
- env: buildCliReplayEnv(record.env, fieldName),
2915
- timeoutMs: clampCliReplayTimeout(record.timeoutMs),
2916
- expectedExitCode,
2917
- recordedStdout: record.recordedStdout,
2918
- invariants,
2919
- };
2920
- }
2921
-
2922
- function validateCliReplayInvariants(invariants: JsonObject[], stdout: string, fieldName: string): void {
2923
- for (const [index, invariant] of invariants.entries()) {
2924
- const invariantField = `${fieldName}.invariants[${index}]`;
2925
- const type = requiredStringField(invariant, "type", invariantField);
2926
- const value = requiredStringField(invariant, "value", invariantField);
2927
- if (type === "substring" && !stdout.includes(value))
2928
- throw new Error(`qualityGate ${invariantField} substring invariant did not match stdout`);
2929
- else if (type === "not_substring" && stdout.includes(value))
2930
- throw new Error(`qualityGate ${invariantField} not_substring invariant matched stdout`);
2931
- else if (type === "regex") {
2932
- const flags = invariant.flags === undefined ? "" : requiredStringField(invariant, "flags", invariantField);
2933
- if (!/^[im]*$/.test(flags)) throw new Error(`qualityGate ${invariantField}.flags may only contain i and m`);
2934
- if (!new RegExp(value, flags).test(stdout))
2935
- throw new Error(`qualityGate ${invariantField} regex invariant did not match stdout`);
2936
- } else if (type !== "substring" && type !== "not_substring") {
2937
- throw new Error(`qualityGate ${invariantField}.type must be substring, regex, or not_substring`);
2938
- }
2939
- }
2940
- }
2941
-
2942
- async function collectCliReplayOutput(
2943
- stream: ReadableStream<Uint8Array> | null,
2944
- ): Promise<{ text: string; truncated: boolean }> {
2945
- if (!stream) return { text: "", truncated: false };
2946
- const reader = stream.getReader();
2947
- const chunks: Buffer[] = [];
2948
- let size = 0;
2949
- let truncated = false;
2950
- try {
2951
- while (true) {
2952
- const { done, value } = await reader.read();
2953
- if (done) break;
2954
- if (size < CLI_REPLAY_MAX_OUTPUT_BYTES) {
2955
- const remaining = CLI_REPLAY_MAX_OUTPUT_BYTES - size;
2956
- const chunk = Buffer.from(value.subarray(0, remaining));
2957
- chunks.push(chunk);
2958
- size += chunk.length;
2959
- }
2960
- if (value.length > 0 && size >= CLI_REPLAY_MAX_OUTPUT_BYTES) {
2961
- truncated = true;
2962
- await reader.cancel().catch(() => undefined);
2963
- break;
2964
- }
2965
- }
2966
- } finally {
2967
- reader.releaseLock();
2968
- }
2969
- return { text: Buffer.concat(chunks).toString("utf8"), truncated };
2970
- }
2971
-
2972
- export interface ReplayProcessHandle {
2973
- readonly exited: Promise<number>;
2974
- kill(signal?: number | NodeJS.Signals): void;
2975
- }
2976
-
2977
- export async function waitForReplayProcessWithTimeout(
2978
- process: ReplayProcessHandle,
2979
- timeoutMs: number,
2980
- graceMs = 2000,
2981
- ): Promise<number> {
2982
- let timeoutTimer: NodeJS.Timeout | undefined;
2983
- let graceTimer: NodeJS.Timeout | undefined;
2984
- const timedOut = Symbol("timedOut");
2985
- const timeout = new Promise<typeof timedOut>(resolve => {
2986
- timeoutTimer = setTimeout(() => resolve(timedOut), timeoutMs);
2987
- });
2988
- const first = await Promise.race([process.exited, timeout]);
2989
- if (first !== timedOut) {
2990
- if (timeoutTimer) clearTimeout(timeoutTimer);
2991
- return first;
2992
- }
2993
- process.kill("SIGTERM");
2994
- const killed = Symbol("killed");
2995
- const grace = new Promise<typeof killed>(resolve => {
2996
- graceTimer = setTimeout(() => {
2997
- process.kill("SIGKILL");
2998
- resolve(killed);
2999
- }, graceMs);
3000
- });
3001
- await Promise.race([process.exited, grace]);
3002
- await process.exited.catch(() => undefined);
3003
- if (timeoutTimer) clearTimeout(timeoutTimer);
3004
- if (graceTimer) clearTimeout(graceTimer);
3005
- throw new Error("timeout");
3006
- }
3007
-
3008
- async function validateReplayExemptFallback(
3009
- cwd: string,
3010
- record: JsonObject,
3011
- fieldName: string,
3012
- artifactRefs: Map<string, JsonObject>,
3013
- options: { surfaceFamily: SurfaceFamily; live: boolean },
3014
- ): Promise<boolean> {
3015
- const exempt = qualityGateObject(record.replayExempt);
3016
- if (!exempt) return false;
3017
- const reasonCode = requiredStringField(exempt, "reasonCode", `${fieldName}.replayExempt`);
3018
- if (!CLI_REPLAY_EXEMPT_REASON_CODE_SET.has(reasonCode))
3019
- throw new Error(
3020
- `qualityGate ${fieldName}.replayExempt.reasonCode must be one of: ${CLI_REPLAY_EXEMPT_REASON_CODE_LIST}`,
3021
- );
3022
- const reason = requiredStringField(exempt, "reason", `${fieldName}.replayExempt`);
3023
- if (!isSubstantiveEvidence(reason) || reason.length < 30)
3024
- throw new Error(`qualityGate ${fieldName}.replayExempt.reason must be audited and substantive`);
3025
- requiredStringField(exempt, "approvedBy", `${fieldName}.replayExempt`);
3026
- const fallbackRefs = requireStringLinks(
3027
- exempt.fallbackArtifactRefs,
3028
- `${fieldName}.replayExempt.fallbackArtifactRefs`,
3029
- );
3030
- requireResolvedLinks(fallbackRefs, artifactRefs, `${fieldName}.replayExempt.fallbackArtifactRefs`);
3031
- let validFallback = false;
3032
- for (const fallbackRef of fallbackRefs) {
3033
- if (fallbackRef === requiredStringField(record, "id", fieldName)) {
3034
- throw new Error(`qualityGate ${fieldName}.replayExempt fallback must not reference the replay record itself`);
3035
- }
3036
- const fallback = artifactRefs.get(fallbackRef)!;
3037
- if (await validateStructuralArtifact(cwd, fallback, `executorQa.artifactRefs.${fallbackRef}`, options))
3038
- validFallback = true;
3039
- }
3040
- if (!validFallback)
3041
- throw new Error(
3042
- `qualityGate ${fieldName}.replayExempt requires at least one structurally-valid fallback artifact`,
3043
- );
3044
- return true;
3045
- }
3046
- async function validateCliReplay(
3047
- cwd: string,
3048
- row: JsonObject,
3049
- fieldName: string,
3050
- options: { live: boolean },
3051
- ): Promise<boolean> {
3052
- const record = await readCliReplayRecord(cwd, row, fieldName);
3053
- if (!record) return false;
3054
- if (record.replayExempt !== undefined) {
3055
- throw new Error(
3056
- `qualityGate ${fieldName}.replayExempt can only be validated from surfaceEvidence with fallback context`,
3057
- );
3058
- }
3059
- void options.live;
3060
- const replay = parseCliReplayRecord(record, fieldName);
3061
- const replayCwd = resolveUnderCwd(cwd, replay.replayCwd, fieldName);
3062
- const process = Bun.spawn(resolveCliReplayCommand(replay.command), {
3063
- cwd: replayCwd,
3064
- env: replay.env,
3065
- stdout: "pipe",
3066
- stderr: "pipe",
3067
- });
3068
- try {
3069
- const [stdout, stderr, exitCode] = await Promise.all([
3070
- collectCliReplayOutput(process.stdout),
3071
- collectCliReplayOutput(process.stderr),
3072
- waitForReplayProcessWithTimeout(process, replay.timeoutMs),
3073
- ]);
3074
- if (stdout.truncated || stderr.truncated)
3075
- throw new Error(`qualityGate ${fieldName} CLI replay output exceeded 1 MiB buffer cap`);
3076
- if (exitCode !== replay.expectedExitCode) {
3077
- throw new Error(
3078
- `qualityGate ${fieldName} CLI replay exit code ${exitCode} did not match expected ${replay.expectedExitCode}`,
3079
- );
3080
- }
3081
- const actualStdout = normalizeCliReplayOutput(stdout.text, cwd);
3082
- const recordedStdout = normalizeCliReplayOutput(replay.recordedStdout, cwd);
3083
- if (replay.invariants.length > 0) {
3084
- validateCliReplayInvariants(replay.invariants, actualStdout, fieldName);
3085
- } else if (actualStdout !== recordedStdout) {
3086
- throw new Error(`qualityGate ${fieldName} CLI replay stdout did not match recordedStdout after normalization`);
3087
- }
3088
- return true;
3089
- } catch (error) {
3090
- if (error instanceof Error && error.message === "timeout") {
3091
- throw new Error(`qualityGate ${fieldName} CLI replay timed out after ${replay.timeoutMs}ms`);
3092
- }
3093
- throw error;
3094
- }
3095
- }
3096
-
3097
- async function hasLiveProofPresence(
3098
- cwd: string,
3099
- row: JsonObject,
3100
- fieldName: string,
3101
- family: SurfaceFamily,
3102
- ): Promise<boolean> {
3103
- if (await hasExistingNonEmptyArtifact(cwd, row.path)) return true;
3104
- if (family === "cli") {
3105
- const record = await readCliReplayRecord(cwd, row, fieldName);
3106
- if (record) return true;
3107
- }
3108
- return false;
3109
- }
3110
-
3111
- async function validateLiveSurfaceProofPresence(
3112
- cwd: string,
3113
- family: SurfaceFamily,
3114
- artifactIds: string[],
3115
- artifactRefs: Map<string, JsonObject>,
3116
- ): Promise<void> {
3117
- if (!isLiveSurfaceFamily(family)) return;
3118
- for (const artifactId of artifactIds) {
3119
- if (
3120
- await hasLiveProofPresence(cwd, artifactRefs.get(artifactId)!, `executorQa.artifactRefs.${artifactId}`, family)
3121
- )
3122
- return;
3123
- }
3124
- throw new Error(
3125
- `qualityGate ${artifactIds.map(id => `executorQa.artifactRefs.${id}`).join(", ")} must reference a live proof artifact, structural capture, or CLI replay; inlineEvidence and typed verifiedReceipt do not prove live surfaces`,
3126
- );
3127
- }
3128
- async function validateSurfaceStructuralRequirement(
3129
- cwd: string,
3130
- family: SurfaceFamily,
3131
- artifactIds: string[],
3132
- artifactRefs: Map<string, JsonObject>,
3133
- fieldName: string,
3134
- ): Promise<void> {
3135
- if (family !== "web" && family !== "native") return;
3136
- let hasScreenshot = false;
3137
- let hasAutomation = false;
3138
- let hasPty = false;
3139
- for (const artifactId of artifactIds) {
3140
- const artifact = artifactRefs.get(artifactId)!;
3141
- const kind = structuralArtifactKind(artifact);
3142
- if (!kind) continue;
3143
- const valid = await validateStructuralArtifact(cwd, artifact, `executorQa.artifactRefs.${artifactId}`, {
3144
- surfaceFamily: family,
3145
- live: true,
3146
- });
3147
- if (kind === "screenshot" && valid) hasScreenshot = true;
3148
- if (kind === "automation" && valid) hasAutomation = true;
3149
- if (kind === "pty" && valid) hasPty = true;
3150
- }
3151
- if (family === "web" && (!hasScreenshot || !hasAutomation)) {
3152
- throw new Error(
3153
- `qualityGate ${fieldName} for GUI/web surfaces must include a valid automation transcript and non-uniform screenshot`,
3154
- );
3155
- }
3156
- if (family === "native" && !hasScreenshot && !hasAutomation && !hasPty) {
3157
- throw new Error(
3158
- `qualityGate ${fieldName} for native surfaces must include a valid screenshot, PTY capture, or app-automation transcript`,
3159
- );
3160
- }
3161
- }
3162
-
3163
- async function validateArtifactProof(
3164
- cwd: string,
3165
- row: JsonObject,
3166
- fieldName: string,
3167
- options: { surfaceFamily: SurfaceFamily; live: boolean },
3168
- ): Promise<void> {
3169
- if (await hasExistingNonEmptyArtifact(cwd, row.path)) return;
3170
- if (await validateStructuralArtifact(cwd, row, fieldName, options)) return;
3171
- if (options.surfaceFamily === "cli" && (await validateCliReplay(cwd, row, fieldName, { live: options.live })))
3172
- return;
3173
- if (!options.live && (hasTypedVerifiedReceipt(row.verifiedReceipt) || hasTypedVerifiedReceipt(row.receipt))) return;
3174
- const proofLabel = options.live
3175
- ? "a live proof artifact, structural capture, or CLI replay; inlineEvidence and typed verifiedReceipt do not prove live surfaces"
3176
- : "an existing non-empty artifact path or a typed verifiedReceipt; inlineEvidence alone is not sufficient";
3177
- throw new Error(`qualityGate ${fieldName} must reference ${proofLabel}`);
3178
- }
2084
+ import {
2085
+ readCliReplayRecord,
2086
+ validateArtifactProof,
2087
+ validateCliReplay,
2088
+ validateLiveSurfaceProofPresence,
2089
+ validateReplayExemptFallback,
2090
+ validateStructuralArtifact,
2091
+ validateSurfaceStructuralRequirement,
2092
+ waitForReplayProcessWithTimeout,
2093
+ } from "./ultragoal-evidence";
2094
+
2095
+ export type { ReplayProcessHandle } from "./ultragoal-evidence";
2096
+ export {
2097
+ validateArtifactProof,
2098
+ validateCliReplay,
2099
+ validateLiveSurfaceProofPresence,
2100
+ validateReplayExemptFallback,
2101
+ validateStructuralArtifact,
2102
+ validateSurfaceStructuralRequirement,
2103
+ waitForReplayProcessWithTimeout,
2104
+ };
3179
2105
 
3180
2106
  async function validateArtifactRefs(cwd: string, executorQa: JsonObject): Promise<Map<string, JsonObject>> {
3181
2107
  void cwd;
@@ -4682,168 +3608,15 @@ async function readOptionalExecutorQa(cwd: string, value: string | undefined): P
4682
3608
  return structured as JsonObject;
4683
3609
  }
4684
3610
 
4685
- async function spawnText(
4686
- command: string[],
4687
- options: { cwd: string; timeoutMs?: number },
4688
- ): Promise<{ ok: boolean; stdout: string; stderr: string }> {
4689
- try {
4690
- const proc = Bun.spawn(command, { cwd: options.cwd, stdout: "pipe", stderr: "pipe" });
4691
- const timeout = setTimeout(() => proc.kill(), options.timeoutMs ?? 5000);
4692
- const [stdout, stderr, exitCode] = await Promise.all([
4693
- new Response(proc.stdout).text(),
4694
- new Response(proc.stderr).text(),
4695
- proc.exited,
4696
- ]);
4697
- clearTimeout(timeout);
4698
- return { ok: exitCode === 0, stdout, stderr };
4699
- } catch (error) {
4700
- return { ok: false, stdout: "", stderr: error instanceof Error ? error.message : String(error) };
4701
- }
4702
- }
4703
-
4704
- export async function resolveGitBase(cwd: string, branch?: string): Promise<string> {
4705
- if (branch) {
4706
- const exists = await spawnText(["git", "rev-parse", "--verify", branch], { cwd, timeoutMs: 3000 });
4707
- if (exists.ok) return branch;
4708
- } else {
4709
- // Prefer the NEAREST integration base (the branch this work actually forks
4710
- // from) rather than always `main`. A branch opened against `dev` must be
4711
- // scoped to `dev`; using a stale `main` sweeps in unrelated trunk history
4712
- // and mis-attributes other people's changes to this story (e.g. falsely
4713
- // tripping change-scoped gates). Among existing candidates, pick the one
4714
- // whose merge-base with HEAD is closest to HEAD (fewest commits ahead).
4715
- const candidates = ["origin/dev", "dev", "origin/main", "origin/master", "main", "master"];
4716
- let best: { ref: string; ahead: number } | undefined;
4717
- for (const candidate of candidates) {
4718
- const exists = await spawnText(["git", "rev-parse", "--verify", candidate], { cwd, timeoutMs: 3000 });
4719
- if (!exists.ok) continue;
4720
- const mergeBase = await spawnText(["git", "merge-base", "HEAD", candidate], { cwd, timeoutMs: 3000 });
4721
- if (!mergeBase.ok || !mergeBase.stdout.trim()) continue;
4722
- const count = await spawnText(["git", "rev-list", "--count", `${mergeBase.stdout.trim()}..HEAD`], {
4723
- cwd,
4724
- timeoutMs: 3000,
4725
- });
4726
- const ahead = Number.parseInt(count.stdout.trim(), 10);
4727
- if (!Number.isFinite(ahead)) continue;
4728
- if (!best || ahead < best.ahead) best = { ref: candidate, ahead };
4729
- }
4730
- if (best) return best.ref;
4731
- }
4732
- const mergeBase = await spawnText(["git", "merge-base", "HEAD", "origin/main"], { cwd, timeoutMs: 3000 });
4733
- if (mergeBase.ok && mergeBase.stdout.trim()) return mergeBase.stdout.trim();
4734
- return "HEAD~1";
4735
- }
4736
-
4737
- function parseGitNameStatus(output: string): UltragoalChangeSetPath[] {
4738
- const rows: UltragoalChangeSetPath[] = [];
4739
- for (const line of output.split("\n")) {
4740
- const trimmed = line.trim();
4741
- if (!trimmed) continue;
4742
- const parts = trimmed.split(/\s+/);
4743
- const statusCode = parts[0] ?? "";
4744
- let status: UltragoalChangeStatus = "unknown";
4745
- if (statusCode.startsWith("A")) status = "added";
4746
- else if (statusCode.startsWith("M")) status = "modified";
4747
- else if (statusCode.startsWith("D")) status = "deleted";
4748
- else if (statusCode.startsWith("R")) status = "renamed";
4749
- else if (statusCode.startsWith("C")) status = "copied";
4750
- const pathValue = status === "renamed" || status === "copied" ? parts[2] : parts[1];
4751
- if (!pathValue) continue;
4752
- const oldPath = status === "renamed" || status === "copied" ? parts[1] : undefined;
4753
- rows.push({
4754
- path: normalizeRepoPath(pathValue),
4755
- oldPath: oldPath ? normalizeRepoPath(oldPath) : undefined,
4756
- status,
4757
- category: categorizeComputerChangePath(pathValue),
4758
- });
4759
- }
4760
- return rows;
4761
- }
4762
-
4763
- function categorizeCiChangedPath(value: string): UltragoalChangeCategory {
4764
- // CI_DEV_CHANGED_PATHS intentionally carries path names only. Mixed registries
4765
- // such as settings-schema.ts require diff-level narrowing; without the diff,
4766
- // treating the whole registry as computer-control source forces the mandatory
4767
- // computer red-team suite on unrelated settings changes.
4768
- if (isSettingsSchemaPath(value)) return "other";
4769
- return categorizeComputerChangePath(value);
4770
- }
4771
-
4772
- function ciDevChangedPathRows(): UltragoalChangeSetPath[] {
4773
- const raw = process.env.CI_DEV_CHANGED_PATHS;
4774
- if (!raw) return [];
4775
- return raw
4776
- .split(/\r?\n/)
4777
- .map(row => row.trim())
4778
- .filter(Boolean)
4779
- .map(pathValue => ({
4780
- path: normalizeRepoPath(pathValue),
4781
- status: "unknown" as UltragoalChangeStatus,
4782
- category: categorizeCiChangedPath(pathValue),
4783
- }));
4784
- }
4785
-
4786
- function mergeChangeSetPaths(groups: UltragoalChangeSetPath[][]): UltragoalChangeSetPath[] {
4787
- const byKey = new Map<string, UltragoalChangeSetPath>();
4788
- for (const row of groups.flat()) byKey.set(`${row.oldPath ?? ""}\u0000${row.path}`, row);
4789
- return [...byKey.values()];
4790
- }
4791
-
4792
- async function computeCheckpointChangeSet(cwd: string): Promise<UltragoalChangeSet | undefined> {
4793
- const ciChangedPaths = ciDevChangedPathRows();
4794
- const inGit = await spawnText(["git", "rev-parse", "--is-inside-work-tree"], { cwd, timeoutMs: 3000 });
4795
- if (!inGit.ok || inGit.stdout.trim() !== "true") {
4796
- if (ciChangedPaths.length === 0) return undefined;
4797
- return { source: "checkpoint-git", paths: ciChangedPaths, trusted: true };
4798
- }
4799
- const baseRef = await resolveGitBase(cwd);
4800
- const base = baseRef;
4801
- const mergeBase = await spawnText(["git", "merge-base", "HEAD", baseRef], { cwd, timeoutMs: 3000 });
4802
- const [committed, unstaged, staged, stat, committedDiff, unstagedDiff, stagedDiff] = await Promise.all([
4803
- spawnText(["git", "diff", "--name-status", `${base}...HEAD`], { cwd, timeoutMs: 5000 }),
4804
- spawnText(["git", "diff", "--name-status"], { cwd, timeoutMs: 5000 }),
4805
- spawnText(["git", "diff", "--cached", "--name-status"], { cwd, timeoutMs: 5000 }),
4806
- spawnText(["git", "diff", "--stat", `${base}...HEAD`], { cwd, timeoutMs: 5000 }),
4807
- spawnText(["git", "diff", `${base}...HEAD`], { cwd, timeoutMs: 5000 }),
4808
- spawnText(["git", "diff"], { cwd, timeoutMs: 5000 }),
4809
- spawnText(["git", "diff", "--cached"], { cwd, timeoutMs: 5000 }),
4810
- ]);
4811
- if (!committed.ok && !unstaged.ok && !staged.ok && ciChangedPaths.length === 0) return undefined;
4812
- const gitPaths = mergeChangeSetPaths([
4813
- parseGitNameStatus(committed.stdout),
4814
- parseGitNameStatus(unstaged.stdout),
4815
- parseGitNameStatus(staged.stdout),
4816
- ]);
4817
- const paths = gitPaths.length > 0 ? gitPaths : ciChangedPaths;
4818
- return {
4819
- source: "checkpoint-git",
4820
- baseRef,
4821
- mergeBase: mergeBase.ok && mergeBase.stdout.trim() ? mergeBase.stdout.trim() : undefined,
4822
- headRef: "HEAD",
4823
- paths,
4824
- rawDiffStat: stat.stdout,
4825
- rawDiff: [committedDiff.stdout, unstagedDiff.stdout, stagedDiff.stdout].filter(Boolean).join("\n"),
4826
- trusted: true,
4827
- };
4828
- }
4829
-
4830
- function parseUnifiedDiffPaths(diff: string): UltragoalChangeSetPath[] {
4831
- const paths: UltragoalChangeSetPath[] = [];
4832
- for (const line of diff.split("\n")) {
4833
- if (!line.startsWith("diff --git ")) continue;
4834
- const match = /^diff --git a\/(.+?) b\/(.+)$/.exec(line);
4835
- if (!match) continue;
4836
- const oldPath = normalizeRepoPath(match[1]!);
4837
- const newPath = normalizeRepoPath(match[2]!);
4838
- paths.push({
4839
- path: newPath,
4840
- oldPath: oldPath === newPath ? undefined : oldPath,
4841
- status: oldPath === newPath ? "modified" : "renamed",
4842
- category: categorizeComputerChangePath(newPath),
4843
- });
4844
- }
4845
- return paths;
4846
- }
3611
+ import {
3612
+ computeCheckpointChangeSet,
3613
+ parseGitNameStatus,
3614
+ parseUnifiedDiffPaths,
3615
+ resolveGitBase,
3616
+ spawnText,
3617
+ } from "./ultragoal-change-set";
3618
+
3619
+ export { computeCheckpointChangeSet, parseGitNameStatus, parseUnifiedDiffPaths, resolveGitBase, spawnText };
4847
3620
 
4848
3621
  function changeSetFromReviewSource(source: JsonObject): UltragoalChangeSet | undefined {
4849
3622
  const kind = nonEmptyString(source.kind);