@gajae-code/coding-agent 0.14.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (345) hide show
  1. package/CHANGELOG.md +120 -1
  2. package/dist/types/async/job-manager.d.ts +4 -1
  3. package/dist/types/autoresearch/dashboard.d.ts +29 -4
  4. package/dist/types/autoresearch/data-context.d.ts +25 -0
  5. package/dist/types/autoresearch/git.d.ts +65 -7
  6. package/dist/types/autoresearch/harness.d.ts +58 -0
  7. package/dist/types/autoresearch/prompts.d.ts +51 -0
  8. package/dist/types/autoresearch/runs.d.ts +173 -0
  9. package/dist/types/autoresearch/types.d.ts +9 -139
  10. package/dist/types/capability/types.d.ts +20 -0
  11. package/dist/types/cli/accounts-cli.d.ts +1 -0
  12. package/dist/types/commands/{rlm.d.ts → autoresearch.d.ts} +2 -4
  13. package/dist/types/config/autorouting-contract.d.ts +146 -0
  14. package/dist/types/config/autorouting-generator.d.ts +40 -0
  15. package/dist/types/config/autorouting-tier-map.d.ts +16844 -0
  16. package/dist/types/config/autorouting.d.ts +35 -0
  17. package/dist/types/config/model-discovery-manager.d.ts +8 -1
  18. package/dist/types/config/model-registry.d.ts +30 -1
  19. package/dist/types/config/provider-selection-policy.d.ts +23 -0
  20. package/dist/types/config/settings-schema.d.ts +61 -3
  21. package/dist/types/config/settings.d.ts +3 -0
  22. package/dist/types/coordinator/contract.d.ts +1 -1
  23. package/dist/types/coordinator-mcp/durability.d.ts +29 -0
  24. package/dist/types/coordinator-mcp/event-webhook.d.ts +50 -0
  25. package/dist/types/coordinator-mcp/question-state.d.ts +1 -1
  26. package/dist/types/coordinator-mcp/server.d.ts +14 -0
  27. package/dist/types/crash/index-store.d.ts +3 -0
  28. package/dist/types/crash/record-loader.d.ts +2 -0
  29. package/dist/types/crash/upstream/relay.d.ts +5 -3
  30. package/dist/types/cursor.d.ts +12 -0
  31. package/dist/types/defaults/gjc-defaults.d.ts +35 -1
  32. package/dist/types/defaults/gjc-skills.generated.d.ts +1 -1
  33. package/dist/types/discovery/helpers.d.ts +1 -1
  34. package/dist/types/edit/index.d.ts +2 -1
  35. package/dist/types/eval/py/executor.d.ts +2 -0
  36. package/dist/types/extensibility/extensions/loader.d.ts +32 -0
  37. package/dist/types/extensibility/gjc-plugins/types.d.ts +1 -1
  38. package/dist/types/extensibility/skills.d.ts +2 -0
  39. package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +194 -0
  40. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  41. package/dist/types/gjc-runtime/psmux-detect.d.ts +1 -1
  42. package/dist/types/gjc-runtime/python-transcript.d.ts +19 -0
  43. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +2 -8
  44. package/dist/types/gjc-runtime/session-layout.d.ts +6 -3
  45. package/dist/types/gjc-runtime/state-renderer.d.ts +0 -22
  46. package/dist/types/gjc-runtime/state-schema.d.ts +7 -7
  47. package/dist/types/gjc-runtime/tmux-common.d.ts +2 -2
  48. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +38 -1
  49. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -24
  50. package/dist/types/gjc-runtime/ultragoal-validation-policy.d.ts +73 -0
  51. package/dist/types/gjc-runtime/workflow-recovery-projection.d.ts +100 -0
  52. package/dist/types/hooks/skill-keywords.d.ts +1 -1
  53. package/dist/types/main.d.ts +0 -10
  54. package/dist/types/modes/acp/acp-agent.d.ts +13 -6
  55. package/dist/types/modes/components/model-selector.d.ts +14 -0
  56. package/dist/types/modes/components/skill-hud/render.d.ts +6 -1
  57. package/dist/types/modes/components/smart-routing-panel.d.ts +82 -0
  58. package/dist/types/modes/controllers/selector-controller.d.ts +14 -0
  59. package/dist/types/modes/interactive-mode.d.ts +3 -0
  60. package/dist/types/modes/types.d.ts +9 -0
  61. package/dist/types/rlm/artifacts.d.ts +4 -4
  62. package/dist/types/runtime/optional-runtime-services.d.ts +2 -1
  63. package/dist/types/runtime/workspace-tree-service.d.ts +5 -1
  64. package/dist/types/runtime-mcp/config.d.ts +6 -0
  65. package/dist/types/runtime-mcp/manager.d.ts +10 -0
  66. package/dist/types/sdk/acp/adapter.d.ts +0 -4
  67. package/dist/types/sdk/broker/broker.d.ts +1 -1
  68. package/dist/types/sdk/broker/lifecycle.d.ts +57 -2
  69. package/dist/types/sdk/broker/session-index.d.ts +15 -0
  70. package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -0
  71. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -0
  72. package/dist/types/sdk/bus/index.d.ts +8 -0
  73. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +2 -0
  74. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -2
  75. package/dist/types/sdk/client/client.d.ts +39 -4
  76. package/dist/types/sdk/host/host.d.ts +1 -0
  77. package/dist/types/sdk/host/internal-autorouting-state.d.ts +3 -0
  78. package/dist/types/sdk/host/model-pin.d.ts +29 -0
  79. package/dist/types/sdk/host/session-runtime.d.ts +2 -1
  80. package/dist/types/sdk/lifecycle-session.d.ts +13 -0
  81. package/dist/types/sdk/router/session-router.d.ts +27 -1
  82. package/dist/types/sdk/session.d.ts +11 -1
  83. package/dist/types/session/agent-session.d.ts +36 -6
  84. package/dist/types/session/artifacts.d.ts +16 -1
  85. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  86. package/dist/types/session/session-manager.d.ts +84 -2
  87. package/dist/types/session/session-staging-paths.d.ts +3 -0
  88. package/dist/types/skill-state/canonical-skills.d.ts +1 -1
  89. package/dist/types/skill-state/workflow-hud.d.ts +12 -21
  90. package/dist/types/skill-state/workflow-mutation-guard.d.ts +1 -0
  91. package/dist/types/slash-commands/helpers/autorouting-status.d.ts +8 -0
  92. package/dist/types/system-prompt.d.ts +23 -0
  93. package/dist/types/task/executor.d.ts +35 -4
  94. package/dist/types/task/index.d.ts +17 -3
  95. package/dist/types/task/receipt.d.ts +3 -1
  96. package/dist/types/task/types.d.ts +144 -0
  97. package/dist/types/tools/atomic-file-write.d.ts +72 -0
  98. package/dist/types/tools/descriptors.d.ts +1 -0
  99. package/dist/types/tools/index.d.ts +36 -1
  100. package/dist/types/tools/move-session.d.ts +40 -0
  101. package/dist/types/tools/python.d.ts +12 -0
  102. package/dist/types/tools/resource-gc.d.ts +0 -3
  103. package/dist/types/tools/search.d.ts +8 -1
  104. package/dist/types/tools/skill.d.ts +9 -4
  105. package/dist/types/tools/subagent.d.ts +33 -3
  106. package/dist/types/utils/xml-escape.d.ts +2 -0
  107. package/dist/types/workflow/workflow-intent-diff.d.ts +2 -2
  108. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +1 -1
  109. package/package.json +11 -12
  110. package/scripts/build-sdk-package-smoke.ts +7 -5
  111. package/scripts/check-autorouting-tier-map.ts +183 -0
  112. package/scripts/generate-sdk-operation-inventory.ts +12 -0
  113. package/scripts/generate-tool-catalog.ts +40 -5
  114. package/src/async/job-manager.ts +5 -2
  115. package/src/autoresearch/dashboard.ts +134 -314
  116. package/src/autoresearch/data-context.ts +35 -0
  117. package/src/autoresearch/git.ts +186 -26
  118. package/src/autoresearch/{helpers.ts → harness.ts} +65 -40
  119. package/src/autoresearch/{prompt-setup.md → prompts/prompt-setup.md} +7 -7
  120. package/src/autoresearch/prompts/prompt.md +103 -0
  121. package/src/autoresearch/prompts.ts +126 -0
  122. package/src/autoresearch/runs.ts +708 -0
  123. package/src/autoresearch/types.ts +9 -150
  124. package/src/capability/index.ts +4 -4
  125. package/src/capability/types.ts +20 -0
  126. package/src/cli/accounts-cli.ts +21 -5
  127. package/src/cli/config-cli.ts +25 -1
  128. package/src/cli/fast-help.ts +1 -2
  129. package/src/cli/list-models.ts +5 -8
  130. package/src/cli/setup-cli.ts +8 -0
  131. package/src/cli.ts +2 -21
  132. package/src/commands/autoresearch.ts +33 -0
  133. package/src/commands/coordinator.ts +15 -0
  134. package/src/commands/launch.ts +2 -4
  135. package/src/commands/quick-lane.ts +1 -1
  136. package/src/commands/sdk.ts +24 -0
  137. package/src/commands/state.ts +1 -1
  138. package/src/config/autorouting-contract.ts +474 -0
  139. package/src/config/autorouting-generator.ts +238 -0
  140. package/src/config/autorouting-tier-map.ts +6108 -0
  141. package/src/config/autorouting.ts +120 -0
  142. package/src/config/model-discovery-manager.ts +42 -11
  143. package/src/config/model-registry.ts +348 -68
  144. package/src/config/provider-selection-policy.ts +60 -9
  145. package/src/config/settings-schema.ts +195 -6
  146. package/src/config/settings.ts +62 -1
  147. package/src/config.ts +2 -3
  148. package/src/coordinator/contract.ts +0 -1
  149. package/src/coordinator-mcp/codex-handoff.ts +68 -42
  150. package/src/coordinator-mcp/durability.ts +200 -0
  151. package/src/coordinator-mcp/event-webhook.ts +278 -0
  152. package/src/coordinator-mcp/question-state.ts +109 -46
  153. package/src/coordinator-mcp/server.ts +932 -504
  154. package/src/crash/index-store.ts +126 -22
  155. package/src/crash/record-loader.ts +7 -0
  156. package/src/crash/upstream/relay.ts +82 -34
  157. package/src/cursor.ts +84 -1
  158. package/src/debug/index.ts +26 -0
  159. package/src/defaults/gjc/skills/autoresearch/SKILL.md +116 -0
  160. package/src/defaults/gjc/skills/autoresearch/auto-critic.md +46 -0
  161. package/src/defaults/gjc/skills/autoresearch/auto-iterate.md +42 -0
  162. package/src/defaults/gjc/skills/deep-interview/SKILL.md +17 -37
  163. package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -18
  164. package/src/defaults/gjc/skills/ultragoal/SKILL.md +6 -17
  165. package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +2 -2
  166. package/src/defaults/gjc-defaults.ts +89 -2
  167. package/src/defaults/gjc-skills.generated.ts +24 -17
  168. package/src/discovery/builtin.ts +18 -5
  169. package/src/discovery/helpers.ts +6 -6
  170. package/src/edit/index.ts +5 -2
  171. package/src/eval/py/executor.ts +8 -0
  172. package/src/extensibility/extensions/loader.ts +135 -13
  173. package/src/extensibility/extensions/wrapper.ts +1 -2
  174. package/src/extensibility/gjc-plugins/schema.ts +1 -1
  175. package/src/extensibility/runtime-skill-discovery.ts +2 -2
  176. package/src/extensibility/skill-management.ts +2 -3
  177. package/src/extensibility/skills.ts +16 -8
  178. package/src/gjc-runtime/autoresearch-runtime.ts +1509 -0
  179. package/src/gjc-runtime/gc-render.ts +0 -1
  180. package/src/gjc-runtime/gc-runtime.ts +1 -11
  181. package/src/gjc-runtime/launch-tmux.ts +3 -3
  182. package/src/gjc-runtime/psmux-detect.ts +2 -2
  183. package/src/gjc-runtime/python-transcript.ts +58 -0
  184. package/src/gjc-runtime/ralplan-runtime.ts +6 -18
  185. package/src/gjc-runtime/session-layout.ts +18 -7
  186. package/src/gjc-runtime/session-state-sidecar.ts +58 -9
  187. package/src/gjc-runtime/state-renderer.ts +0 -38
  188. package/src/gjc-runtime/state-runtime.ts +43 -22
  189. package/src/gjc-runtime/state-schema.ts +1 -1
  190. package/src/gjc-runtime/tmux-common.ts +7 -7
  191. package/src/gjc-runtime/ultragoal-change-set.ts +232 -11
  192. package/src/gjc-runtime/ultragoal-runtime.ts +309 -114
  193. package/src/gjc-runtime/ultragoal-validation-policy.ts +247 -0
  194. package/src/gjc-runtime/workflow-command-ref.ts +15 -11
  195. package/src/gjc-runtime/workflow-manifest.generated.json +413 -532
  196. package/src/gjc-runtime/workflow-manifest.ts +43 -83
  197. package/src/gjc-runtime/workflow-recovery-projection.ts +616 -0
  198. package/src/hooks/skill-keywords.ts +3 -3
  199. package/src/hooks/skill-state.ts +2 -2
  200. package/src/internal-urls/docs-index.generated.ts +26 -24
  201. package/src/lsp/index.ts +25 -5
  202. package/src/main.ts +2 -27
  203. package/src/modes/acp/acp-agent.ts +200 -75
  204. package/src/modes/components/model-selector.ts +216 -13
  205. package/src/modes/components/oauth-selector.ts +5 -5
  206. package/src/modes/components/skill-hud/render.ts +173 -35
  207. package/src/modes/components/smart-routing-panel.ts +531 -0
  208. package/src/modes/components/tool-status-header.ts +6 -3
  209. package/src/modes/controllers/command-controller.ts +17 -7
  210. package/src/modes/controllers/input-controller.ts +44 -9
  211. package/src/modes/controllers/runtime-mcp-command-controller.ts +4 -0
  212. package/src/modes/controllers/selector-controller.ts +298 -65
  213. package/src/modes/interactive-mode.ts +8 -1
  214. package/src/modes/shared/agent-wire/workflow-gate-types.ts +1 -1
  215. package/src/modes/types.ts +9 -1
  216. package/src/prompts/agents/planner.md +1 -1
  217. package/src/prompts/system/system-prompt.md +1 -1
  218. package/src/prompts/system/volatile-project-context.md +2 -1
  219. package/src/prompts/tools/move-session.md +15 -0
  220. package/src/prompts/tools/python.md +12 -0
  221. package/src/prompts/tools/skill-discovery.md +1 -1
  222. package/src/prompts/tools/skill.md +2 -2
  223. package/src/prompts/tools/task-summary.md +1 -0
  224. package/src/prompts/tools/task.md +6 -0
  225. package/src/rlm/artifacts.ts +12 -10
  226. package/src/runtime/optional-runtime-services.ts +3 -2
  227. package/src/runtime/workspace-tree-service.ts +11 -3
  228. package/src/runtime-mcp/config.ts +8 -1
  229. package/src/runtime-mcp/manager.ts +33 -0
  230. package/src/sdk/acp/adapter.ts +48 -1
  231. package/src/sdk/broker/broker.ts +30 -2
  232. package/src/sdk/broker/discovery.ts +217 -2
  233. package/src/sdk/broker/lifecycle.ts +385 -59
  234. package/src/sdk/broker/session-index.ts +243 -48
  235. package/src/sdk/broker/transport.ts +1 -0
  236. package/src/sdk/bus/chat-daemon-control.ts +8 -2
  237. package/src/sdk/bus/chat-daemon-runtime.ts +5 -0
  238. package/src/sdk/bus/index.ts +208 -11
  239. package/src/sdk/bus/kind-aware-reconciliation.ts +16 -0
  240. package/src/sdk/bus/reconciliation-store.ts +27 -5
  241. package/src/sdk/bus/telegram-daemon-contract.ts +12 -2
  242. package/src/sdk/client/client.ts +204 -22
  243. package/src/sdk/host/host.ts +16 -0
  244. package/src/sdk/host/internal-autorouting-state.ts +13 -0
  245. package/src/sdk/host/model-pin.ts +101 -0
  246. package/src/sdk/host/reverse-leases.ts +2 -1
  247. package/src/sdk/host/session-runtime.test.ts +224 -1
  248. package/src/sdk/host/session-runtime.ts +125 -37
  249. package/src/sdk/lifecycle/service.ts +3 -1
  250. package/src/sdk/lifecycle-session.ts +42 -1
  251. package/src/sdk/protocol/operation-inventory.generated.json +66 -0
  252. package/src/sdk/router/session-router.ts +375 -36
  253. package/src/sdk/session.ts +547 -47
  254. package/src/session/account-inventory.ts +1 -1
  255. package/src/session/agent-session.ts +765 -189
  256. package/src/session/artifacts.ts +234 -4
  257. package/src/session/internal/managed-session-storage.ts +91 -4
  258. package/src/session/provider-safety-stop-hint.ts +3 -2
  259. package/src/session/session-manager.ts +775 -16
  260. package/src/session/session-staging-paths.ts +8 -0
  261. package/src/setup/hermes/templates/operator-instructions.v1.md +0 -1
  262. package/src/setup/host-plugin-setup.ts +2 -2
  263. package/src/skill-state/active-state.ts +2 -2
  264. package/src/skill-state/canonical-skills.ts +1 -1
  265. package/src/skill-state/initial-phase.ts +1 -1
  266. package/src/skill-state/workflow-hud.ts +41 -41
  267. package/src/skill-state/workflow-mutation-guard.ts +26 -7
  268. package/src/slash-commands/builtin-registry.ts +77 -0
  269. package/src/slash-commands/helpers/autorouting-status.ts +55 -0
  270. package/src/system-prompt.ts +102 -5
  271. package/src/task/discovery.ts +2 -3
  272. package/src/task/executor.ts +617 -43
  273. package/src/task/index.ts +262 -15
  274. package/src/task/receipt.ts +30 -0
  275. package/src/task/render.ts +11 -1
  276. package/src/task/types.ts +316 -0
  277. package/src/tools/atomic-file-write.ts +594 -0
  278. package/src/tools/bisect.ts +3 -2
  279. package/src/tools/descriptors.test.ts +3 -0
  280. package/src/tools/descriptors.ts +24 -0
  281. package/src/tools/image-gen.ts +5 -1
  282. package/src/tools/index.ts +62 -5
  283. package/src/tools/move-session.ts +107 -0
  284. package/src/tools/python.ts +205 -0
  285. package/src/tools/read.ts +88 -15
  286. package/src/tools/renderers.ts +2 -0
  287. package/src/tools/resource-gc.ts +8 -177
  288. package/src/tools/search.ts +30 -6
  289. package/src/tools/skill-discovery.ts +1 -0
  290. package/src/tools/skill.ts +37 -7
  291. package/src/tools/subagent-render.ts +119 -13
  292. package/src/tools/subagent.ts +91 -74
  293. package/src/tools/tool-catalog.generated.ts +64 -2
  294. package/src/tools/write.ts +16 -12
  295. package/src/utils/xml-escape.ts +14 -0
  296. package/src/web/search/providers/codex.ts +2 -2
  297. package/src/workflow/workflow-intent-diff.ts +13 -12
  298. package/src/workflow/workflow-intent-report-consumer.ts +6 -6
  299. package/dist/types/autoresearch/helpers.d.ts +0 -24
  300. package/dist/types/autoresearch/index.d.ts +0 -2
  301. package/dist/types/autoresearch/state.d.ts +0 -17
  302. package/dist/types/autoresearch/storage.d.ts +0 -142
  303. package/dist/types/autoresearch/tools/init-experiment.d.ts +0 -31
  304. package/dist/types/autoresearch/tools/log-experiment.d.ts +0 -23
  305. package/dist/types/autoresearch/tools/run-experiment.d.ts +0 -8
  306. package/dist/types/autoresearch/tools/update-notes.d.ts +0 -12
  307. package/dist/types/commands/team.d.ts +0 -24
  308. package/dist/types/gjc-runtime/team-gc.d.ts +0 -7
  309. package/dist/types/gjc-runtime/team-launch.d.ts +0 -61
  310. package/dist/types/gjc-runtime/team-notify.d.ts +0 -45
  311. package/dist/types/gjc-runtime/team-runtime.d.ts +0 -336
  312. package/dist/types/gjc-runtime/team-store.d.ts +0 -168
  313. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +0 -43
  314. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +0 -92
  315. package/dist/types/gjc-runtime/team-workers.d.ts +0 -83
  316. package/dist/types/rlm/index.d.ts +0 -47
  317. package/dist/types/rlm/preset.d.ts +0 -23
  318. package/dist/types/rlm/python-tool.d.ts +0 -16
  319. package/src/autoresearch/command-resume.md +0 -14
  320. package/src/autoresearch/index.ts +0 -536
  321. package/src/autoresearch/prompt.md +0 -103
  322. package/src/autoresearch/resume-message.md +0 -10
  323. package/src/autoresearch/state.ts +0 -273
  324. package/src/autoresearch/storage.ts +0 -699
  325. package/src/autoresearch/tools/init-experiment.ts +0 -272
  326. package/src/autoresearch/tools/log-experiment.ts +0 -524
  327. package/src/autoresearch/tools/run-experiment.ts +0 -475
  328. package/src/autoresearch/tools/update-notes.ts +0 -109
  329. package/src/commands/rlm.ts +0 -19
  330. package/src/commands/team.ts +0 -238
  331. package/src/defaults/gjc/skills/deep-interview/auto-research-greenfield.md +0 -42
  332. package/src/defaults/gjc/skills/team/SKILL.md +0 -467
  333. package/src/gjc-runtime/team-gc.ts +0 -49
  334. package/src/gjc-runtime/team-launch.ts +0 -404
  335. package/src/gjc-runtime/team-notify.ts +0 -326
  336. package/src/gjc-runtime/team-runtime.ts +0 -5923
  337. package/src/gjc-runtime/team-store.ts +0 -878
  338. package/src/gjc-runtime/team-worker-heartbeat.ts +0 -149
  339. package/src/gjc-runtime/team-worker-memory-guard.ts +0 -363
  340. package/src/gjc-runtime/team-workers.ts +0 -1123
  341. package/src/prompts/system/rlm-report-command.md +0 -1
  342. package/src/prompts/system/rlm-research.md +0 -23
  343. package/src/rlm/index.ts +0 -365
  344. package/src/rlm/preset.ts +0 -76
  345. package/src/rlm/python-tool.ts +0 -68
@@ -0,0 +1,1509 @@
1
+ /**
2
+ * Native implementation of `gjc autoresearch` — the session-scoped mission
3
+ * runtime for the autoresearch workflow skill.
4
+ *
5
+ * Everything the runtime persists lives under
6
+ * `<cwd>/.gjc/_session-{gjcSessionId}/autoresearch/` (`sessionAutoresearchDir`):
7
+ * the mission artifact (`mission.json`) and the append-only ledger
8
+ * (`ledger.jsonl`). The legacy global autoresearch store is intentionally
9
+ * dead: no code path in this module writes there, and every mutation routes
10
+ * through the sanctioned `.gjc/**` state-writer primitives, which structurally
11
+ * refuse targets outside the project `.gjc/` tree.
12
+ *
13
+ * Intake contract (AC-14..AC-16): two entrypoints write the one mission
14
+ * artifact. Spec intake (`intake --spec <path>`, or the bare `--spec` flag) consumes a persisted deep-interview
15
+ * spec and asks zero clarification questions; cold intake (positional goal or
16
+ * bare invocation) signals that goal/constraints/deliverables clarification must
17
+ * run before research begins and writes nothing. The mission mode is one of
18
+ * `web` / `mixed` / `data` and is always supplied explicitly — the write
19
+ * boundary REJECTS a missing or invalid mode and never infers one from the
20
+ * presence of a data file.
21
+ *
22
+ * The ledger is append-only and carries the six event kinds: `mission_created`,
23
+ * `mode_set`, `run_logged`, `verdict_issued`, `critic_recorded`,
24
+ * `mission_cleared`. A mission completes only on a structured best-effort
25
+ * verdict (status, evidence, caveats, evaluator); an optional critic pass
26
+ * records a `critic_receipt` whose evaluator identity is distinct from the
27
+ * mission agent.
28
+ */
29
+ import * as crypto from "node:crypto";
30
+ import * as fs from "node:fs/promises";
31
+ import * as path from "node:path";
32
+ import { type AutoresearchDashboardInput, renderExpandedDashboard } from "../autoresearch/dashboard";
33
+ import { loadAutoresearchDataContext, type RlmDataContext } from "../autoresearch/data-context";
34
+ import { type EnsureAutoresearchBranchResult, ensureAutoresearchBranch } from "../autoresearch/git";
35
+ import { type ParsedHarnessOutput, parseHarnessOutput } from "../autoresearch/harness";
36
+ import { renderAutoresearchIteratePrompt, renderAutoresearchSetupPrompt } from "../autoresearch/prompts";
37
+ import {
38
+ AutoresearchRunsStore,
39
+ buildAutoresearchExperimentState,
40
+ createAutoresearchExperimentConfig,
41
+ type MetricDirection,
42
+ } from "../autoresearch/runs";
43
+ import { syncSkillActiveState } from "../skill-state/active-state";
44
+ import { buildAutoresearchHudSummary } from "../skill-state/workflow-hud";
45
+ import { renderCliWriteReceipt } from "./cli-write-receipt";
46
+ import { sessionAutoresearchDir } from "./session-layout";
47
+ import {
48
+ resolveGjcSessionForRead,
49
+ resolveGjcSessionForWrite,
50
+ SessionResolutionError,
51
+ writeSessionActivityMarker,
52
+ } from "./session-resolution";
53
+ import {
54
+ appendJsonl,
55
+ persistedStateRevision,
56
+ readExistingStateForMutation,
57
+ writeGuardedJsonAtomic,
58
+ } from "./state-writer";
59
+ import { assertSafePathComponent, CommandError, flagValue, hasFlag } from "./workflow-cli-common";
60
+
61
+ export type AutoresearchMode = "web" | "mixed" | "data";
62
+ export type AutoresearchIntakeKind = "handoff" | "cold";
63
+
64
+ export const AUTORESEARCH_MODES = new Set<AutoresearchMode>(["web", "mixed", "data"]);
65
+
66
+ export const AUTORESEARCH_LEDGER_EVENT_KINDS = [
67
+ "mission_created",
68
+ "mode_set",
69
+ "run_logged",
70
+ "verdict_issued",
71
+ "critic_recorded",
72
+ "mission_cleared",
73
+ ] as const;
74
+ export type AutoresearchLedgerEventKind = (typeof AUTORESEARCH_LEDGER_EVENT_KINDS)[number];
75
+
76
+ /** The one mission artifact both intake entrypoints write. */
77
+ export interface AutoresearchMission {
78
+ objective: string;
79
+ mode: AutoresearchMode;
80
+ deliverables: string[];
81
+ constraints: string[];
82
+ slug: string;
83
+ intake: AutoresearchIntakeKind;
84
+ createdAt: string;
85
+ updatedAt: string;
86
+ /** Absolute path of the deep-interview spec consumed by spec intake. */
87
+ specPath?: string;
88
+ handedOffAt?: string;
89
+ /**
90
+ * Primary metric contract for the mission's harness.
91
+ *
92
+ * Optional: a mission that declares none keeps the historical default
93
+ * (`metric`, lower-is-better). When declared it MUST reach the experiment
94
+ * config -- otherwise a mission whose research contract is
95
+ * higher-is-better silently optimizes the wrong direction.
96
+ */
97
+ primaryMetric?: string;
98
+ metricUnit?: string;
99
+ metricDirection?: MetricDirection;
100
+ }
101
+
102
+ export interface AutoresearchPaths {
103
+ dir: string;
104
+ missionPath: string;
105
+ ledgerPath: string;
106
+ /** Quarantine root for retired missions: `<dir>/retired/<slug>.<timestamp>/`. */
107
+ retiredRoot: string;
108
+ }
109
+
110
+ /** Append-only session ledger row. */
111
+ export interface AutoresearchLedgerEvent {
112
+ eventId: string;
113
+ event: AutoresearchLedgerEventKind;
114
+ timestamp: string;
115
+ [field: string]: unknown;
116
+ }
117
+
118
+ /** Optional per-mission critic pass receipt; evaluator is distinct from the mission agent. */
119
+ export interface AutoresearchCriticReceipt {
120
+ criticId: string;
121
+ status: Record<string, unknown>;
122
+ evidence: string[];
123
+ caveats: string[];
124
+ evaluator: string;
125
+ recordedAt: string;
126
+ }
127
+
128
+ /** Structured mission verdict: status is open data, not a pinned enum — terminality is deliberately deferred. */
129
+ export interface AutoresearchVerdictReceipt {
130
+ receiptId: string;
131
+ status: Record<string, unknown>;
132
+ evidence: string[];
133
+ caveats: string[];
134
+ evaluator: string;
135
+ issuedAt: string;
136
+ criticReceipt?: AutoresearchCriticReceipt;
137
+ }
138
+
139
+ export interface AutoresearchCommandResult {
140
+ status: number;
141
+ stdout?: string;
142
+ stderr?: string;
143
+ intake?: AutoresearchIntakeKind;
144
+ missionCreated?: boolean;
145
+ }
146
+
147
+ export interface AutoresearchReadReceipt {
148
+ ok: true;
149
+ exists: boolean;
150
+ mission?: AutoresearchMission;
151
+ ledger: AutoresearchLedgerEvent[];
152
+ paths: AutoresearchPaths;
153
+ /**
154
+ * GJC session id the mission state was read from. Callers deriving
155
+ * session-scoped identity (notably the kernel owner id) MUST use this
156
+ * instead of re-resolving, so every path agrees on one scope.
157
+ */
158
+ sessionId: string;
159
+ }
160
+
161
+ export interface AutoresearchWriteReceipt {
162
+ ok: true;
163
+ mission: AutoresearchMission;
164
+ missionPath: string;
165
+ intake: AutoresearchIntakeKind;
166
+ ledgerEvent?: AutoresearchLedgerEvent;
167
+ }
168
+
169
+ export interface AutoresearchClearReceipt {
170
+ ok: true;
171
+ cleared: boolean;
172
+ missionPath: string;
173
+ ledgerEvent: AutoresearchLedgerEvent;
174
+ /** Where the retired mission's artifacts were quarantined, when one existed. */
175
+ retiredTo?: string;
176
+ }
177
+
178
+ export interface AutoresearchIntakeReceipt extends AutoresearchWriteReceipt {
179
+ specPath: string;
180
+ }
181
+
182
+ class AutoresearchCommandError extends CommandError {
183
+ constructor(exitStatus: number, message: string) {
184
+ super(exitStatus, message);
185
+ this.name = "AutoresearchCommandError";
186
+ }
187
+ }
188
+
189
+ /** AC-16 write-boundary gate: mode must be explicitly supplied and valid. */
190
+ function assertAutoresearchMode(value: unknown, source: string): asserts value is AutoresearchMode {
191
+ if (typeof value !== "string" || !AUTORESEARCH_MODES.has(value as AutoresearchMode)) {
192
+ throw new AutoresearchCommandError(
193
+ 2,
194
+ `autoresearch mission mode must be one of web, mixed, or data (${source}); received ${JSON.stringify(value)}. ` +
195
+ "Mode is never inferred from the presence of a data file — declare it explicitly at intake.",
196
+ );
197
+ }
198
+ }
199
+
200
+ function assertStructuredStatus(status: unknown, source: string): asserts status is Record<string, unknown> {
201
+ if (!status || typeof status !== "object" || Array.isArray(status)) {
202
+ throw new AutoresearchCommandError(
203
+ 2,
204
+ `autoresearch ${source} must be a structured object; received ${JSON.stringify(status)}`,
205
+ );
206
+ }
207
+ }
208
+
209
+ function requireStringArray(value: unknown, fieldName: string): string[] {
210
+ if (!Array.isArray(value) || !value.every(item => typeof item === "string")) {
211
+ throw new AutoresearchCommandError(2, `autoresearch ${fieldName} must be a string array`);
212
+ }
213
+ return dedupeStrings(value);
214
+ }
215
+
216
+ function dedupeStrings(values: readonly string[]): string[] {
217
+ const out: string[] = [];
218
+ const seen = new Set<string>();
219
+ for (const value of values) {
220
+ const trimmed = value.trim();
221
+ if (trimmed.length === 0 || seen.has(trimmed)) continue;
222
+ seen.add(trimmed);
223
+ out.push(trimmed);
224
+ }
225
+ return out;
226
+ }
227
+
228
+ function isEnoent(error: unknown): boolean {
229
+ return (
230
+ typeof error === "object" && error !== null && "code" in error && (error as { code?: unknown }).code === "ENOENT"
231
+ );
232
+ }
233
+
234
+ export function getAutoresearchPaths(cwd: string, sessionId?: string | null): AutoresearchPaths {
235
+ const resolvedSessionId =
236
+ sessionId?.trim() || resolveGjcSessionForWrite(cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
237
+ const dir = sessionAutoresearchDir(cwd, resolvedSessionId);
238
+ return {
239
+ dir,
240
+ missionPath: path.join(dir, "mission.json"),
241
+ ledgerPath: path.join(dir, "ledger.jsonl"),
242
+ retiredRoot: path.join(dir, "retired"),
243
+ };
244
+ }
245
+
246
+ function normalizeAutoresearchMission(value: unknown): AutoresearchMission {
247
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
248
+ throw new AutoresearchCommandError(2, "autoresearch mission.json must contain a JSON object");
249
+ }
250
+ const record = value as Record<string, unknown>;
251
+ const objective = typeof record.objective === "string" ? record.objective.trim() : "";
252
+ const slug = typeof record.slug === "string" ? record.slug.trim() : "";
253
+ if (!objective) throw new AutoresearchCommandError(2, "autoresearch mission.json is missing objective");
254
+ if (!slug) throw new AutoresearchCommandError(2, "autoresearch mission.json is missing slug");
255
+ assertAutoresearchMode(record.mode, "mission.json");
256
+ const createdAt = typeof record.createdAt === "string" ? record.createdAt : "";
257
+ const updatedAt = typeof record.updatedAt === "string" ? record.updatedAt : createdAt;
258
+ const intake: AutoresearchIntakeKind = record.intake === "handoff" ? "handoff" : "cold";
259
+ return {
260
+ objective,
261
+ mode: record.mode,
262
+ deliverables: requireStringArray(record.deliverables ?? [], "mission.json deliverables"),
263
+ constraints: requireStringArray(record.constraints ?? [], "mission.json constraints"),
264
+ slug,
265
+ intake,
266
+ createdAt,
267
+ updatedAt,
268
+ ...(typeof record.specPath === "string" && record.specPath.trim() !== "" ? { specPath: record.specPath } : {}),
269
+ ...(typeof record.handedOffAt === "string" && record.handedOffAt.trim() !== ""
270
+ ? { handedOffAt: record.handedOffAt }
271
+ : {}),
272
+ // Preserve the declared metric contract across read/write round-trips;
273
+ // dropping it here would silently reinstate the default direction.
274
+ ...(typeof record.primaryMetric === "string" && record.primaryMetric.trim() !== ""
275
+ ? { primaryMetric: record.primaryMetric.trim() }
276
+ : {}),
277
+ ...(typeof record.metricUnit === "string" && record.metricUnit.trim() !== ""
278
+ ? { metricUnit: record.metricUnit.trim() }
279
+ : {}),
280
+ ...(record.metricDirection === "higher" || record.metricDirection === "lower"
281
+ ? { metricDirection: record.metricDirection }
282
+ : {}),
283
+ };
284
+ }
285
+
286
+ export async function readAutoresearchMission(
287
+ cwd: string,
288
+ sessionId?: string | null,
289
+ ): Promise<AutoresearchMission | null> {
290
+ const resolvedSessionId =
291
+ sessionId?.trim() ||
292
+ (await resolveGjcSessionForRead(cwd, { envSessionId: process.env.GJC_SESSION_ID })).gjcSessionId;
293
+ try {
294
+ return normalizeAutoresearchMission(
295
+ await Bun.file(getAutoresearchPaths(cwd, resolvedSessionId).missionPath).json(),
296
+ );
297
+ } catch (error) {
298
+ if (isEnoent(error)) return null;
299
+ throw error;
300
+ }
301
+ }
302
+
303
+ export async function readAutoresearchLedger(
304
+ cwd: string,
305
+ sessionId?: string | null,
306
+ ): Promise<AutoresearchLedgerEvent[]> {
307
+ const resolvedSessionId =
308
+ sessionId?.trim() ||
309
+ (await resolveGjcSessionForRead(cwd, { envSessionId: process.env.GJC_SESSION_ID })).gjcSessionId;
310
+ try {
311
+ const raw = await Bun.file(getAutoresearchPaths(cwd, resolvedSessionId).ledgerPath).text();
312
+ return raw
313
+ .split(/\r?\n/)
314
+ .map(line => line.trim())
315
+ .filter(line => line.length > 0)
316
+ .map(line => JSON.parse(line) as AutoresearchLedgerEvent);
317
+ } catch (error) {
318
+ if (isEnoent(error)) return [];
319
+ throw error;
320
+ }
321
+ }
322
+
323
+ async function appendAutoresearchLedger(
324
+ cwd: string,
325
+ payload: { event: AutoresearchLedgerEventKind } & Record<string, unknown>,
326
+ sessionId?: string | null,
327
+ ): Promise<AutoresearchLedgerEvent> {
328
+ const resolvedSessionId =
329
+ sessionId?.trim() || resolveGjcSessionForWrite(cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
330
+ const paths = getAutoresearchPaths(cwd, resolvedSessionId);
331
+ const entry: AutoresearchLedgerEvent = {
332
+ eventId: crypto.randomUUID(),
333
+ ...payload,
334
+ timestamp: new Date().toISOString(),
335
+ };
336
+ await appendJsonl(paths.ledgerPath, entry, {
337
+ cwd,
338
+ audit: {
339
+ category: "ledger",
340
+ verb: "append",
341
+ owner: "gjc-runtime",
342
+ skill: "autoresearch",
343
+ sessionId: resolvedSessionId,
344
+ },
345
+ });
346
+ await writeSessionActivityMarker(cwd, resolvedSessionId, { writer: "autoresearch-runtime", path: paths.ledgerPath });
347
+ return entry;
348
+ }
349
+
350
+ async function persistAutoresearchMission(input: {
351
+ cwd: string;
352
+ sessionId: string;
353
+ mission: AutoresearchMission;
354
+ }): Promise<AutoresearchMission> {
355
+ const paths = getAutoresearchPaths(input.cwd, input.sessionId);
356
+ const existingRead = await readExistingStateForMutation(paths.missionPath);
357
+ if (existingRead.kind === "corrupt") {
358
+ throw new AutoresearchCommandError(
359
+ 2,
360
+ `existing autoresearch mission is corrupt or tampered (${existingRead.error}); refusing to overwrite ${paths.missionPath}`,
361
+ );
362
+ }
363
+ await writeGuardedJsonAtomic(paths.missionPath, input.mission, {
364
+ cwd: input.cwd,
365
+ policy: "source",
366
+ expectedRevision: existingRead.kind === "valid" ? persistedStateRevision(existingRead.value) : undefined,
367
+ audit: {
368
+ category: "state",
369
+ verb: "write",
370
+ owner: "gjc-runtime",
371
+ skill: "autoresearch",
372
+ sessionId: input.sessionId,
373
+ },
374
+ });
375
+ await writeSessionActivityMarker(input.cwd, input.sessionId, {
376
+ writer: "autoresearch-runtime",
377
+ path: paths.missionPath,
378
+ });
379
+ return input.mission;
380
+ }
381
+
382
+ /* ------------------------------ verbs ------------------------------ */
383
+
384
+ /** read verb: current mission + append-only ledger snapshot. */
385
+ export async function autoresearchRead(cwd: string, sessionId?: string | null): Promise<AutoresearchReadReceipt> {
386
+ const resolvedSessionId =
387
+ sessionId?.trim() ||
388
+ (await resolveGjcSessionForRead(cwd, { envSessionId: process.env.GJC_SESSION_ID })).gjcSessionId;
389
+ const mission = await readAutoresearchMission(cwd, resolvedSessionId);
390
+ const ledger = await readAutoresearchLedger(cwd, resolvedSessionId);
391
+ return {
392
+ ok: true,
393
+ exists: mission !== null,
394
+ ...(mission ? { mission } : {}),
395
+ ledger,
396
+ paths: getAutoresearchPaths(cwd, resolvedSessionId),
397
+ sessionId: resolvedSessionId,
398
+ };
399
+ }
400
+
401
+ /**
402
+ * Validate and normalize the optional primary-metric contract.
403
+ *
404
+ * Direction is validated at the write boundary exactly like mode: an
405
+ * unrecognized value is rejected rather than coerced, so a typo can never
406
+ * silently flip the mission to the default direction.
407
+ */
408
+ function normalizeMetricContract(
409
+ input: { primaryMetric?: string; metricUnit?: string; metricDirection?: string },
410
+ context: string,
411
+ ): Pick<AutoresearchMission, "primaryMetric" | "metricUnit" | "metricDirection"> {
412
+ const result: Pick<AutoresearchMission, "primaryMetric" | "metricUnit" | "metricDirection"> = {};
413
+ const primaryMetric = input.primaryMetric?.trim();
414
+ if (primaryMetric) result.primaryMetric = primaryMetric;
415
+ const metricUnit = input.metricUnit?.trim();
416
+ if (metricUnit) result.metricUnit = metricUnit;
417
+ const direction = input.metricDirection?.trim().toLowerCase();
418
+ if (direction !== undefined && direction !== "") {
419
+ if (direction !== "higher" && direction !== "lower") {
420
+ throw new AutoresearchCommandError(
421
+ 2,
422
+ `${context} metric direction must be "higher" or "lower"; received ${JSON.stringify(input.metricDirection)}. It is never inferred.`,
423
+ );
424
+ }
425
+ result.metricDirection = direction;
426
+ }
427
+ return result;
428
+ }
429
+
430
+ /** write verb: persist the mission after cold-intake clarification. Mode is required. */
431
+ export async function autoresearchWrite(input: {
432
+ cwd: string;
433
+ objective: string;
434
+ mode: AutoresearchMode;
435
+ deliverables?: string[];
436
+ constraints?: string[];
437
+ slug: string;
438
+ sessionId?: string | null;
439
+ primaryMetric?: string;
440
+ metricUnit?: string;
441
+ metricDirection?: string;
442
+ }): Promise<AutoresearchWriteReceipt> {
443
+ const objective = input.objective.trim();
444
+ if (!objective) throw new AutoresearchCommandError(2, "autoresearch mission objective is required");
445
+ const slug = input.slug.trim();
446
+ if (!slug) throw new AutoresearchCommandError(2, "autoresearch mission slug is required");
447
+ assertSafePathComponent(slug, "slug");
448
+ // AC-16: hard fail at the write boundary; mode is never inferred.
449
+ assertAutoresearchMode(input.mode, "write intake");
450
+ const metric = normalizeMetricContract(input, "write intake");
451
+ const resolvedSessionId =
452
+ input.sessionId?.trim() ||
453
+ resolveGjcSessionForWrite(input.cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
454
+ const now = new Date().toISOString();
455
+ const paths = getAutoresearchPaths(input.cwd, resolvedSessionId);
456
+ const existing = await readAutoresearchMission(input.cwd, resolvedSessionId);
457
+ const mission: AutoresearchMission = {
458
+ objective,
459
+ mode: input.mode,
460
+ deliverables: dedupeStrings(input.deliverables ?? []),
461
+ constraints: dedupeStrings(input.constraints ?? []),
462
+ slug,
463
+ intake: "cold",
464
+ createdAt: existing?.createdAt ?? now,
465
+ updatedAt: now,
466
+ ...metric,
467
+ };
468
+ await persistAutoresearchMission({ cwd: input.cwd, sessionId: resolvedSessionId, mission });
469
+ let ledgerEvent: AutoresearchLedgerEvent | undefined;
470
+ if (existing === null) {
471
+ ledgerEvent = await appendAutoresearchLedger(
472
+ input.cwd,
473
+ { event: "mission_created", slug, mode: mission.mode, objective },
474
+ resolvedSessionId,
475
+ );
476
+ } else if (existing.mode !== mission.mode) {
477
+ ledgerEvent = await appendAutoresearchLedger(
478
+ input.cwd,
479
+ { event: "mode_set", slug, mode: mission.mode, previousMode: existing.mode },
480
+ resolvedSessionId,
481
+ );
482
+ }
483
+ return { ok: true, mission, missionPath: paths.missionPath, intake: "cold", ledgerEvent };
484
+ }
485
+
486
+ /**
487
+ * Durable per-mission artifacts that live directly under the session
488
+ * autoresearch dir. `clear` must retire ALL of them, not just `mission.json`:
489
+ * leaving `ledger.jsonl` behind lets a successor mission inherit the previous
490
+ * mission's `verdict_issued` / `critic_recorded` rows. `runs.jsonl` and
491
+ * `experiment.json` leak run history and the metric contract, and `runs/`
492
+ * holds per-run artifacts the same way.
493
+ */
494
+ const AUTORESEARCH_MISSION_ARTIFACTS = ["mission.json", "ledger.jsonl", "runs.jsonl", "experiment.json"] as const;
495
+ const AUTORESEARCH_RUNS_SUBDIR = "runs";
496
+
497
+ /**
498
+ * clear verb: retire the entire mission working set and record the mission clear.
499
+ *
500
+ * Artifacts are QUARANTINED to `<dir>/retired/<slug>.<timestamp>/`, never
501
+ * deleted, so a completed mission's evidence stays auditable while the
502
+ * successor mission starts from genuinely empty state. The `mission_cleared` row
503
+ * is appended to the OUTGOING ledger before it is moved, so the retired ledger
504
+ * is a complete record that ends with its own clear and the successor's ledger
505
+ * starts empty rather than the clear silently vanishing from the audit trail.
506
+ */
507
+ export async function autoresearchClear(cwd: string, sessionId?: string | null): Promise<AutoresearchClearReceipt> {
508
+ const resolvedSessionId =
509
+ sessionId?.trim() || resolveGjcSessionForWrite(cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
510
+ const paths = getAutoresearchPaths(cwd, resolvedSessionId);
511
+ const existing = await readAutoresearchMission(cwd, resolvedSessionId);
512
+ // Close out the outgoing ledger first so the retired copy ends with its clear.
513
+ const ledgerEvent = await appendAutoresearchLedger(
514
+ cwd,
515
+ { event: "mission_cleared", slug: existing?.slug ?? "" },
516
+ resolvedSessionId,
517
+ );
518
+ const retiredTo = existing ? await retireAutoresearchMissionArtifacts(paths, existing.slug) : undefined;
519
+ const deleted = existing !== null;
520
+ await reconcileAutoresearchState(cwd, existing, resolvedSessionId, { active: false, phase: "complete" });
521
+ return {
522
+ ok: true,
523
+ cleared: deleted,
524
+ missionPath: paths.missionPath,
525
+ ledgerEvent,
526
+ ...(retiredTo ? { retiredTo } : {}),
527
+ };
528
+ }
529
+
530
+ /**
531
+ * Move every durable mission artifact aside into a uniquely-reserved directory.
532
+ *
533
+ * A timestamp alone collides when two clears land in the same millisecond,
534
+ * which would overwrite the earlier mission's evidence, so the directory is
535
+ * reserved with an exclusive mkdir and disambiguated with a counter.
536
+ */
537
+ async function retireAutoresearchMissionArtifacts(paths: AutoresearchPaths, slug: string): Promise<string> {
538
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
539
+ await fs.mkdir(paths.retiredRoot, { recursive: true });
540
+ const base = path.join(paths.retiredRoot, `${slug}.${stamp}`);
541
+ let retiredTo = base;
542
+ for (let attempt = 1; ; attempt += 1) {
543
+ retiredTo = attempt === 1 ? base : `${base}-${attempt}`;
544
+ try {
545
+ await fs.mkdir(retiredTo);
546
+ break;
547
+ } catch (error) {
548
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
549
+ }
550
+ }
551
+ for (const name of AUTORESEARCH_MISSION_ARTIFACTS) {
552
+ await moveIfPresent(path.join(paths.dir, name), path.join(retiredTo, name));
553
+ }
554
+ await moveIfPresent(path.join(paths.dir, AUTORESEARCH_RUNS_SUBDIR), path.join(retiredTo, AUTORESEARCH_RUNS_SUBDIR));
555
+ return retiredTo;
556
+ }
557
+
558
+ async function moveIfPresent(from: string, to: string): Promise<void> {
559
+ try {
560
+ await fs.rename(from, to);
561
+ } catch (error) {
562
+ if (!isEnoent(error)) throw error;
563
+ }
564
+ }
565
+
566
+ /* ---------------------------- spec intake ---------------------------- */
567
+
568
+ const AUTORESEARCH_MODE_DECLARATION_RE = /^(?:[-*]\s+)?autoresearch-mode\s*:\s*(web|mixed|data)\s*$/i;
569
+ const AUTORESEARCH_METRIC_DECLARATION_RE = /^(?:[-*]\s+)?autoresearch-metric\s*:\s*(.+?)\s*$/i;
570
+ const AUTORESEARCH_METRIC_UNIT_DECLARATION_RE = /^(?:[-*]\s+)?autoresearch-metric-unit\s*:\s*(.+?)\s*$/i;
571
+ const AUTORESEARCH_METRIC_DIRECTION_DECLARATION_RE = /^(?:[-*]\s+)?autoresearch-metric-direction\s*:\s*(.+?)\s*$/i;
572
+ const HEADING_RE = /^#{1,6}\s+(.+)$/;
573
+ const BULLET_RE = /^[-*]\s+(.+)$/;
574
+ const ACCEPTANCE_CRITERIA_DELIVERABLE_RE = /^[-*]\s+\[[ xX]\]\s+(.+)$/;
575
+ const INTERVIEW_ID_RE = /^[-*]\s*Interview ID:\s*(.+)$/i;
576
+
577
+ interface ParsedAutoresearchSpec {
578
+ objective: string;
579
+ mode: AutoresearchMode;
580
+ deliverables: string[];
581
+ constraints: string[];
582
+ slug: string;
583
+ primaryMetric?: string;
584
+ metricUnit?: string;
585
+ metricDirection?: MetricDirection;
586
+ }
587
+
588
+ function sectionBullets(lines: string[], sectionNames: readonly string[]): string[] {
589
+ const wanted = new Set(sectionNames.map(name => name.toLowerCase()));
590
+ let current: string | null = null;
591
+ const bullets: string[] = [];
592
+ for (const line of lines) {
593
+ const trimmed = line.trim();
594
+ const heading = HEADING_RE.exec(trimmed);
595
+ if (heading) {
596
+ current = heading[1]!.trim().toLowerCase();
597
+ continue;
598
+ }
599
+ if (current === null || !wanted.has(current)) continue;
600
+ const bullet = BULLET_RE.exec(trimmed);
601
+ if (!bullet) continue;
602
+ const text = bullet[1]!.trim();
603
+ if (text) bullets.push(text);
604
+ }
605
+ return dedupeStrings(bullets);
606
+ }
607
+
608
+ function sanitizeSpecSlug(value: string): string {
609
+ const normalized = value
610
+ .trim()
611
+ .toLowerCase()
612
+ .replace(/[^a-z0-9._-]+/g, "-")
613
+ .replace(/^-+|-+$/g, "");
614
+ if (normalized === "") {
615
+ throw new AutoresearchCommandError(2, "autoresearch spec intake could not derive a mission slug from the spec");
616
+ }
617
+ return normalized;
618
+ }
619
+
620
+ /**
621
+ * Parse a persisted deep-interview spec into the mission fields. The spec MUST
622
+ * declare its mission mode explicitly with a line like `autoresearch-mode: web`
623
+ * (one of web, mixed, data); a missing or invalid declaration is a hard fail —
624
+ * mode is never inferred from spec structure or incidental files.
625
+ */
626
+ function parseAutoresearchSpec(specText: string, specPath: string): ParsedAutoresearchSpec {
627
+ const lines = specText.split(/\r?\n/);
628
+ let mode: AutoresearchMode | undefined;
629
+ for (const line of lines) {
630
+ const match = AUTORESEARCH_MODE_DECLARATION_RE.exec(line.trim());
631
+ if (match) {
632
+ mode = match[1]!.toLowerCase() as AutoresearchMode;
633
+ break;
634
+ }
635
+ }
636
+ if (!mode) {
637
+ throw new AutoresearchCommandError(
638
+ 2,
639
+ `autoresearch spec intake requires the spec to declare its mission mode explicitly with a line like "autoresearch-mode: web" (one of web, mixed, data) at ${specPath}. ` +
640
+ "Mode is never inferred from the presence of a data file.",
641
+ );
642
+ }
643
+ // Optional metric contract. Same declaration style as the mode line; the
644
+ // direction is validated rather than coerced.
645
+ const firstMatch = (re: RegExp): string | undefined => {
646
+ for (const line of lines) {
647
+ const found = re.exec(line.trim());
648
+ if (found) return found[1];
649
+ }
650
+ return undefined;
651
+ };
652
+ const metric = normalizeMetricContract(
653
+ {
654
+ primaryMetric: firstMatch(AUTORESEARCH_METRIC_DECLARATION_RE),
655
+ metricUnit: firstMatch(AUTORESEARCH_METRIC_UNIT_DECLARATION_RE),
656
+ metricDirection: firstMatch(AUTORESEARCH_METRIC_DIRECTION_DECLARATION_RE),
657
+ },
658
+ `autoresearch spec intake at ${specPath}`,
659
+ );
660
+
661
+ const h1 = lines.find(line => /^#\s+\S/.test(line.trim()));
662
+ const objective =
663
+ (h1 ?? lines.find(line => line.trim() !== ""))?.trim().replace(/^#\s+/, "") ?? path.basename(specPath);
664
+ const deliverables = sectionBullets(lines, ["deliverables"]);
665
+ const acceptanceCriteria = sectionBullets(lines, ["acceptance criteria"]);
666
+ const finalDeliverables =
667
+ deliverables.length > 0
668
+ ? deliverables
669
+ : acceptanceCriteria.map(text => ACCEPTANCE_CRITERIA_DELIVERABLE_RE.exec(text)?.[1]?.trim() ?? text);
670
+ const constraints = sectionBullets(lines, ["constraints"]);
671
+ const interviewIdLine = lines.find(line => INTERVIEW_ID_RE.test(line.trim()));
672
+ const interviewId = interviewIdLine ? INTERVIEW_ID_RE.exec(interviewIdLine.trim())?.[1]?.trim() : undefined;
673
+ const slug = sanitizeSpecSlug(interviewId ?? path.basename(specPath, path.extname(specPath)));
674
+ return {
675
+ objective,
676
+ mode,
677
+ deliverables: finalDeliverables,
678
+ constraints,
679
+ slug,
680
+ ...metric,
681
+ };
682
+ }
683
+
684
+ /** Spec intake (`intake --spec <path>` or bare `--spec`): read the spec, write the mission, ask zero questions. The persisted intake kind stays "handoff" (seeded by a deep-interview handoff). */
685
+ export async function autoresearchIntake(input: {
686
+ cwd: string;
687
+ specPath: string;
688
+ sessionId?: string | null;
689
+ }): Promise<AutoresearchIntakeReceipt> {
690
+ const resolvedSpecPath = path.resolve(input.cwd, input.specPath);
691
+ let specText: string;
692
+ try {
693
+ specText = await fs.readFile(resolvedSpecPath, "utf-8");
694
+ } catch (error) {
695
+ throw new AutoresearchCommandError(
696
+ 2,
697
+ `autoresearch spec intake could not read spec at ${resolvedSpecPath}: ${error instanceof Error ? error.message : String(error)}`,
698
+ );
699
+ }
700
+ const parsed = parseAutoresearchSpec(specText, resolvedSpecPath);
701
+ const resolvedSessionId =
702
+ input.sessionId?.trim() ||
703
+ resolveGjcSessionForWrite(input.cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
704
+ const now = new Date().toISOString();
705
+ const paths = getAutoresearchPaths(input.cwd, resolvedSessionId);
706
+ const existing = await readAutoresearchMission(input.cwd, resolvedSessionId);
707
+ const mission: AutoresearchMission = {
708
+ objective: parsed.objective,
709
+ mode: parsed.mode,
710
+ deliverables: parsed.deliverables,
711
+ constraints: parsed.constraints,
712
+ slug: parsed.slug,
713
+ intake: "handoff",
714
+ createdAt: existing?.createdAt ?? now,
715
+ updatedAt: now,
716
+ specPath: resolvedSpecPath,
717
+ handedOffAt: now,
718
+ ...(parsed.primaryMetric === undefined ? {} : { primaryMetric: parsed.primaryMetric }),
719
+ ...(parsed.metricUnit === undefined ? {} : { metricUnit: parsed.metricUnit }),
720
+ ...(parsed.metricDirection === undefined ? {} : { metricDirection: parsed.metricDirection }),
721
+ };
722
+ await persistAutoresearchMission({ cwd: input.cwd, sessionId: resolvedSessionId, mission });
723
+ let ledgerEvent: AutoresearchLedgerEvent | undefined;
724
+ if (existing === null) {
725
+ ledgerEvent = await appendAutoresearchLedger(
726
+ input.cwd,
727
+ {
728
+ event: "mission_created",
729
+ slug: mission.slug,
730
+ mode: mission.mode,
731
+ objective: mission.objective,
732
+ specPath: resolvedSpecPath,
733
+ },
734
+ resolvedSessionId,
735
+ );
736
+ } else if (existing.mode !== mission.mode) {
737
+ ledgerEvent = await appendAutoresearchLedger(
738
+ input.cwd,
739
+ { event: "mode_set", slug: mission.slug, mode: mission.mode, previousMode: existing.mode },
740
+ resolvedSessionId,
741
+ );
742
+ }
743
+ return {
744
+ ok: true,
745
+ mission,
746
+ missionPath: paths.missionPath,
747
+ intake: "handoff",
748
+ specPath: resolvedSpecPath,
749
+ ledgerEvent,
750
+ };
751
+ }
752
+
753
+ /* ------------------------------ run/verdict ------------------------------ */
754
+
755
+ /** Record one experiment run in the ledger. */
756
+ export async function autoresearchLogRun(input: {
757
+ cwd: string;
758
+ runId: string;
759
+ status: string;
760
+ description: string;
761
+ metric?: number;
762
+ slug?: string;
763
+ sessionId?: string | null;
764
+ }): Promise<AutoresearchLedgerEvent> {
765
+ const runId = input.runId.trim();
766
+ if (!runId) throw new AutoresearchCommandError(2, "autoresearch run_id is required");
767
+ const status = input.status.trim();
768
+ if (!status) throw new AutoresearchCommandError(2, "autoresearch run status is required");
769
+ const description = input.description.trim();
770
+ if (!description) throw new AutoresearchCommandError(2, "autoresearch run description is required");
771
+ if (status !== "keep" && status !== "discard" && status !== "crash" && status !== "checks_failed") {
772
+ throw new AutoresearchCommandError(2, "autoresearch run status must be keep, discard, crash, or checks_failed");
773
+ }
774
+ const sessionId =
775
+ input.sessionId?.trim() ||
776
+ resolveGjcSessionForWrite(input.cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
777
+ const store = await autoresearchRunsStore(input.cwd, sessionId);
778
+ if (!store.config) throw new AutoresearchCommandError(2, "autoresearch run logging requires an active mission");
779
+ // Reuse the caller-supplied pending run identity when it exists so runs.jsonl and
780
+ // the ledger reference the same run; otherwise synthesize one record.
781
+ const existing = store.listRuns().find(run => run.runId === runId);
782
+ let targetRunId: string;
783
+ if (existing && existing.status === null && existing.abandonedAt === null) {
784
+ targetRunId = existing.runId;
785
+ } else {
786
+ const started = await store.startRun({ command: "research observation" });
787
+ targetRunId = started.runId;
788
+ }
789
+ await store.completeRun(targetRunId, {
790
+ exitCode: status === "crash" || status === "checks_failed" ? 1 : 0,
791
+ timedOut: false,
792
+ });
793
+ await store.logRun(targetRunId, {
794
+ status,
795
+ description,
796
+ ...(input.metric === undefined ? {} : { metric: input.metric }),
797
+ });
798
+ const ledgerEvent = await appendAutoresearchLedger(
799
+ input.cwd,
800
+ {
801
+ event: "run_logged",
802
+ run_id: targetRunId,
803
+ status,
804
+ description,
805
+ ...(input.slug?.trim() ? { slug: input.slug.trim() } : {}),
806
+ ...(typeof input.metric === "number" && Number.isFinite(input.metric) ? { metric: input.metric } : {}),
807
+ },
808
+ sessionId,
809
+ );
810
+ const mission = await readAutoresearchMission(input.cwd, sessionId);
811
+ if (mission) await reconcileAutoresearchState(input.cwd, mission, sessionId, { phase: "research" });
812
+ return ledgerEvent;
813
+ }
814
+
815
+ /** Record the optional per-mission critic pass; its evaluator is distinct from the mission agent. */
816
+ export async function autoresearchRecordCritic(input: {
817
+ cwd: string;
818
+ status: Record<string, unknown>;
819
+ evidence: string[];
820
+ caveats: string[];
821
+ evaluator: string;
822
+ slug?: string;
823
+ sessionId?: string | null;
824
+ }): Promise<AutoresearchCriticReceipt> {
825
+ assertStructuredStatus(input.status, "critic status");
826
+ const evidence = requireStringArray(input.evidence, "critic evidence");
827
+ const caveats = requireStringArray(input.caveats, "critic caveats");
828
+ const evaluator = input.evaluator.trim();
829
+ if (!evaluator) throw new AutoresearchCommandError(2, "autoresearch critic evaluator is required");
830
+ const receipt: AutoresearchCriticReceipt = {
831
+ criticId: crypto.randomUUID(),
832
+ status: input.status,
833
+ evidence,
834
+ caveats,
835
+ evaluator,
836
+ recordedAt: new Date().toISOString(),
837
+ };
838
+ await appendAutoresearchLedger(
839
+ input.cwd,
840
+ {
841
+ event: "critic_recorded",
842
+ ...(input.slug?.trim() ? { slug: input.slug.trim() } : {}),
843
+ criticReceipt: receipt,
844
+ },
845
+ input.sessionId,
846
+ );
847
+ return receipt;
848
+ }
849
+
850
+ /** Issue the mission verdict; an optional critic receipt rides along with its own evaluator identity. */
851
+ export async function autoresearchIssueVerdict(input: {
852
+ cwd: string;
853
+ status: Record<string, unknown>;
854
+ evidence: string[];
855
+ caveats: string[];
856
+ evaluator: string;
857
+ criticReceipt?: AutoresearchCriticReceipt;
858
+ slug?: string;
859
+ sessionId?: string | null;
860
+ }): Promise<AutoresearchVerdictReceipt> {
861
+ assertStructuredStatus(input.status, "verdict status");
862
+ const evidence = requireStringArray(input.evidence, "verdict evidence");
863
+ const caveats = requireStringArray(input.caveats, "verdict caveats");
864
+ const evaluator = input.evaluator.trim();
865
+ if (!evaluator) throw new AutoresearchCommandError(2, "autoresearch verdict evaluator is required");
866
+ const sessionId =
867
+ input.sessionId?.trim() ||
868
+ resolveGjcSessionForWrite(input.cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
869
+ const ledger = await readAutoresearchLedger(input.cwd, sessionId);
870
+ const latestCritic = [...ledger].reverse().find(event => event.event === "critic_recorded" && event.criticReceipt)
871
+ ?.criticReceipt as AutoresearchCriticReceipt | undefined;
872
+ const receipt: AutoresearchVerdictReceipt = {
873
+ receiptId: crypto.randomUUID(),
874
+ status: input.status,
875
+ evidence,
876
+ caveats,
877
+ evaluator,
878
+ issuedAt: new Date().toISOString(),
879
+ ...((input.criticReceipt ?? latestCritic) ? { criticReceipt: input.criticReceipt ?? latestCritic } : {}),
880
+ };
881
+ await appendAutoresearchLedger(
882
+ input.cwd,
883
+ {
884
+ event: "verdict_issued",
885
+ ...(input.slug?.trim() ? { slug: input.slug.trim() } : {}),
886
+ verdictReceipt: receipt,
887
+ },
888
+ sessionId,
889
+ );
890
+ const mission = await readAutoresearchMission(input.cwd, sessionId);
891
+ if (mission) await reconcileAutoresearchState(input.cwd, mission, sessionId, { phase: "verdict" });
892
+ return receipt;
893
+ }
894
+
895
+ /* ------------------------------ CLI dispatch ------------------------------ */
896
+
897
+ function renderAutoresearchHelp(): string {
898
+ return [
899
+ "Run native GJC Autoresearch workflow commands",
900
+ "",
901
+ "USAGE",
902
+ " $ gjc autoresearch [--spec <path>] [--json] [goal...]",
903
+ " $ gjc autoresearch intake --spec <path> [--json]",
904
+ " $ gjc autoresearch read [--json]",
905
+ " $ gjc autoresearch clear [--json]",
906
+ " $ gjc autoresearch write --goal <goal> --mode <web|mixed|data> --slug <slug> [--deliverable <text>] [--constraint <text>] [--json]",
907
+ " $ gjc autoresearch log-run --run-id <id> --status <status> --description <text> [--metric <number>] [--json]",
908
+ " $ gjc autoresearch verdict --status-json <object> --evidence <text> --evaluator <id> [--caveat <text>] [--json]",
909
+ " $ gjc autoresearch critic --status-json <object> --evidence <text> --evaluator <id> [--caveat <text>] [--json]",
910
+ "",
911
+ "INTAKE",
912
+ " intake --spec Spec intake: read a persisted deep-interview spec and start research",
913
+ " with zero clarification questions. The spec must declare its mission",
914
+ " mode explicitly (a line like `autoresearch-mode: web`).",
915
+ " positional goal Cold intake: signals that goal/constraints/deliverables clarification",
916
+ " must run before research begins.",
917
+ " bare invocation Cold intake (no goal text).",
918
+ " write Persist a clarified cold-intake mission. Mode and slug are required.",
919
+ " read Read the current mission and append-only ledger.",
920
+ " clear Clear the mission artifact and retire its working set.",
921
+ " log-run Append one research run receipt to the mission ledger.",
922
+ " verdict Append the structured mission verdict receipt.",
923
+ " critic Append an optional structured critic receipt.",
924
+ " --json Output a machine-readable receipt.",
925
+ "",
926
+ "STATE",
927
+ " Mission/ledger/verdict state persists under .gjc/_session-{sessionid}/autoresearch/.",
928
+ " The global autoresearch store is not written.",
929
+ "",
930
+ "EXAMPLES",
931
+ " $ gjc autoresearch --spec .gjc/_session-abc/specs/deep-interview-my-mission.md --json",
932
+ ' $ gjc autoresearch "Optimize the tokenizer throughput"',
933
+ " $ gjc autoresearch",
934
+ "",
935
+ ].join("\n");
936
+ }
937
+
938
+ function repeatedFlagValues(args: readonly string[], flag: string): string[] {
939
+ const values: string[] = [];
940
+ for (let index = 0; index < args.length; index += 1) {
941
+ if (args[index] !== flag) continue;
942
+ const value = args[index + 1];
943
+ if (!value || value.startsWith("--")) throw new AutoresearchCommandError(2, `${flag} requires a non-empty value`);
944
+ values.push(value);
945
+ index += 1;
946
+ }
947
+ return values;
948
+ }
949
+
950
+ function assertOnlyAutoresearchFlags(args: readonly string[], allowed: readonly string[]): void {
951
+ for (let index = 0; index < args.length; index += 1) {
952
+ const arg = args[index]!;
953
+ if (!arg.startsWith("--")) continue;
954
+ if (!allowed.includes(arg)) throw new AutoresearchCommandError(2, `unknown flag for gjc autoresearch: ${arg}`);
955
+ if (arg !== "--json") index += 1;
956
+ }
957
+ }
958
+
959
+ function requiredFlagValue(args: readonly string[], flag: string): string {
960
+ const value = flagValue(args, flag)?.trim();
961
+ if (!value) throw new AutoresearchCommandError(2, `${flag} requires a non-empty value`);
962
+ return value;
963
+ }
964
+
965
+ function jsonObjectFlag(args: readonly string[], flag: string): Record<string, unknown> {
966
+ const raw = requiredFlagValue(args, flag);
967
+ try {
968
+ const value = JSON.parse(raw) as unknown;
969
+ assertStructuredStatus(value, flag);
970
+ return value;
971
+ } catch (error) {
972
+ if (error instanceof AutoresearchCommandError) throw error;
973
+ throw new AutoresearchCommandError(2, `${flag} must be a JSON object`);
974
+ }
975
+ }
976
+
977
+ function extractPositionalGoal(args: readonly string[]): string {
978
+ const parts: string[] = [];
979
+ let skipNext = false;
980
+ for (const arg of args) {
981
+ if (skipNext) {
982
+ skipNext = false;
983
+ continue;
984
+ }
985
+ if (arg === "--spec") {
986
+ skipNext = true;
987
+ continue;
988
+ }
989
+ if (arg === "--json") continue;
990
+ if (arg.startsWith("-")) {
991
+ throw new AutoresearchCommandError(2, `unknown flag for gjc autoresearch: ${arg}`);
992
+ }
993
+ parts.push(arg);
994
+ }
995
+ return parts.join(" ").trim();
996
+ }
997
+
998
+ function renderSpecIntakeText(receipt: AutoresearchIntakeReceipt): string {
999
+ return [
1000
+ `autoresearch intake=handoff slug=${receipt.mission.slug}`,
1001
+ `mode=${receipt.mission.mode}`,
1002
+ `objective=${receipt.mission.objective}`,
1003
+ `spec_path=${receipt.specPath}`,
1004
+ `mission_path=${receipt.missionPath}`,
1005
+ "research may begin; zero clarification questions.",
1006
+ "",
1007
+ ].join("\n");
1008
+ }
1009
+
1010
+ function renderColdIntakeText(goal: string): string {
1011
+ return [
1012
+ "autoresearch intake=cold clarification_required=true",
1013
+ ...(goal ? [`goal=${goal}`] : []),
1014
+ "next=run goal, constraints, and deliverables clarification before research begins",
1015
+ "",
1016
+ ].join("\n");
1017
+ }
1018
+
1019
+ /**
1020
+ * Reconcile the session-scoped active-state/HUD row after a mission write
1021
+ * (skill "autoresearch" is a plain active-state skill until the canonical slot
1022
+ * swap; the entry and snapshot are the generic per-skill machinery).
1023
+ * Best-effort: a HUD sync failure never changes command semantics.
1024
+ */
1025
+ async function reconcileAutoresearchState(
1026
+ cwd: string,
1027
+ mission: AutoresearchMission | null,
1028
+ sessionId?: string,
1029
+ options: { active?: boolean; phase?: "intake" | "research" | "verdict" | "complete" } = {},
1030
+ ): Promise<void> {
1031
+ const resolvedSessionId =
1032
+ sessionId?.trim() || resolveGjcSessionForWrite(cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
1033
+ try {
1034
+ const ledger = await readAutoresearchLedger(cwd, resolvedSessionId);
1035
+ const store = await AutoresearchRunsStore.open(cwd, resolvedSessionId);
1036
+ const latestVerdict = [...ledger]
1037
+ .reverse()
1038
+ .find(event => event.event === "verdict_issued" && event.verdictReceipt);
1039
+ const verdictReceipt = latestVerdict?.verdictReceipt as AutoresearchVerdictReceipt | undefined;
1040
+ const verdictStatus = verdictReceipt?.status;
1041
+ const verdict = verdictStatus
1042
+ ? typeof verdictStatus.verdict === "string"
1043
+ ? verdictStatus.verdict
1044
+ : typeof verdictStatus.status === "string"
1045
+ ? verdictStatus.status
1046
+ : undefined
1047
+ : undefined;
1048
+ const hud = mission
1049
+ ? buildAutoresearchHudSummary({
1050
+ phase: options.phase ?? "research",
1051
+ mode: mission.mode,
1052
+ intake: mission.intake,
1053
+ slug: mission.slug,
1054
+ specPath: mission.specPath,
1055
+ verdict,
1056
+ experimentStatuses: store.listLoggedRuns().flatMap(run => (run.status ? [run.status] : [])),
1057
+ updatedAt: new Date().toISOString(),
1058
+ })
1059
+ : undefined;
1060
+ await syncSkillActiveState({
1061
+ cwd,
1062
+ skill: "autoresearch",
1063
+ active: options.active ?? true,
1064
+ phase: options.phase ?? "research",
1065
+ sessionId: resolvedSessionId,
1066
+ source: "gjc-autoresearch-native",
1067
+ hud: hud ?? {
1068
+ version: 1,
1069
+ summary: "autoresearch mission cleared",
1070
+ chips: [],
1071
+ updated_at: new Date().toISOString(),
1072
+ },
1073
+ });
1074
+ } catch {
1075
+ // HUD sync is best-effort and must not change command semantics.
1076
+ }
1077
+ }
1078
+
1079
+ export async function runNativeAutoresearchCommand(
1080
+ args: string[],
1081
+ cwd = process.cwd(),
1082
+ ): Promise<AutoresearchCommandResult> {
1083
+ try {
1084
+ if (args.includes("--help") || args.includes("-h") || args[0] === "help") {
1085
+ return { status: 0, stdout: renderAutoresearchHelp() };
1086
+ }
1087
+ const verb = args[0];
1088
+ if (verb === "read") {
1089
+ assertOnlyAutoresearchFlags(args.slice(1), ["--json"]);
1090
+ const receipt = await autoresearchRead(cwd);
1091
+ return {
1092
+ status: 0,
1093
+ stdout: hasFlag(args, "--json")
1094
+ ? renderCliWriteReceipt({
1095
+ ok: receipt.ok,
1096
+ exists: receipt.exists,
1097
+ ...(receipt.mission ? { mission: receipt.mission } : {}),
1098
+ ledger: receipt.ledger,
1099
+ paths: receipt.paths,
1100
+ })
1101
+ : `autoresearch exists=${receipt.exists}\nmission_path=${receipt.paths.missionPath}\nledger_path=${receipt.paths.ledgerPath}\n`,
1102
+ };
1103
+ }
1104
+ if (verb === "clear") {
1105
+ assertOnlyAutoresearchFlags(args.slice(1), ["--json"]);
1106
+ const receipt = await autoresearchClear(cwd);
1107
+ return {
1108
+ status: 0,
1109
+ stdout: hasFlag(args, "--json")
1110
+ ? renderCliWriteReceipt({
1111
+ ok: true,
1112
+ cleared: receipt.cleared,
1113
+ mission_path: receipt.missionPath,
1114
+ ...(receipt.retiredTo ? { retired_to: receipt.retiredTo } : {}),
1115
+ ledger_event: receipt.ledgerEvent.event,
1116
+ })
1117
+ : `autoresearch cleared=${receipt.cleared}\nmission_path=${receipt.missionPath}\n${
1118
+ receipt.retiredTo ? `retired_to=${receipt.retiredTo}\n` : ""
1119
+ }`,
1120
+ };
1121
+ }
1122
+ if (verb === "write") {
1123
+ assertOnlyAutoresearchFlags(args.slice(1), [
1124
+ "--goal",
1125
+ "--mode",
1126
+ "--slug",
1127
+ "--deliverable",
1128
+ "--constraint",
1129
+ "--primary-metric",
1130
+ "--metric-unit",
1131
+ "--metric-direction",
1132
+ "--json",
1133
+ ]);
1134
+ const objective = flagValue(args, "--goal");
1135
+ const mode = flagValue(args, "--mode");
1136
+ const slug = flagValue(args, "--slug");
1137
+ if (objective === undefined || mode === undefined || slug === undefined) {
1138
+ throw new AutoresearchCommandError(2, "write requires --goal, --mode, and --slug");
1139
+ }
1140
+ const receipt = await autoresearchWrite({
1141
+ cwd,
1142
+ objective,
1143
+ mode: mode as AutoresearchMode,
1144
+ slug,
1145
+ deliverables: repeatedFlagValues(args, "--deliverable"),
1146
+ constraints: repeatedFlagValues(args, "--constraint"),
1147
+ primaryMetric: flagValue(args, "--primary-metric"),
1148
+ metricUnit: flagValue(args, "--metric-unit"),
1149
+ metricDirection: flagValue(args, "--metric-direction"),
1150
+ });
1151
+ await reconcileAutoresearchState(cwd, receipt.mission);
1152
+ return {
1153
+ status: 0,
1154
+ intake: "cold",
1155
+ missionCreated: true,
1156
+ stdout: hasFlag(args, "--json")
1157
+ ? renderCliWriteReceipt({
1158
+ ok: true,
1159
+ intake: receipt.intake,
1160
+ mission: receipt.mission,
1161
+ mission_path: receipt.missionPath,
1162
+ ledger_event: receipt.ledgerEvent?.event,
1163
+ })
1164
+ : `autoresearch intake=cold slug=${receipt.mission.slug}\nmode=${receipt.mission.mode}\nmission_path=${receipt.missionPath}\n`,
1165
+ };
1166
+ }
1167
+ if (verb === "log-run") {
1168
+ assertOnlyAutoresearchFlags(args.slice(1), [
1169
+ "--run-id",
1170
+ "--status",
1171
+ "--description",
1172
+ "--metric",
1173
+ "--slug",
1174
+ "--json",
1175
+ ]);
1176
+ const metricRaw = flagValue(args, "--metric");
1177
+ const metric = metricRaw === undefined ? undefined : Number(metricRaw);
1178
+ if (metricRaw !== undefined && !Number.isFinite(metric)) {
1179
+ throw new AutoresearchCommandError(2, "--metric must be a finite number");
1180
+ }
1181
+ const event = await autoresearchLogRun({
1182
+ cwd,
1183
+ runId: requiredFlagValue(args, "--run-id"),
1184
+ status: requiredFlagValue(args, "--status"),
1185
+ description: requiredFlagValue(args, "--description"),
1186
+ metric,
1187
+ slug: flagValue(args, "--slug"),
1188
+ });
1189
+ return { status: 0, stdout: renderCliWriteReceipt({ ok: true, ledger_event: event }) };
1190
+ }
1191
+ if (verb === "critic" || verb === "verdict") {
1192
+ assertOnlyAutoresearchFlags(args.slice(1), [
1193
+ "--status-json",
1194
+ "--evidence",
1195
+ "--caveat",
1196
+ "--evaluator",
1197
+ "--slug",
1198
+ "--json",
1199
+ ]);
1200
+ const input = {
1201
+ cwd,
1202
+ status: jsonObjectFlag(args, "--status-json"),
1203
+ evidence: repeatedFlagValues(args, "--evidence"),
1204
+ caveats: repeatedFlagValues(args, "--caveat"),
1205
+ evaluator: requiredFlagValue(args, "--evaluator"),
1206
+ slug: flagValue(args, "--slug"),
1207
+ };
1208
+ const receipt =
1209
+ verb === "critic" ? await autoresearchRecordCritic(input) : await autoresearchIssueVerdict(input);
1210
+ return { status: 0, stdout: renderCliWriteReceipt({ ok: true, receipt }) };
1211
+ }
1212
+ if (verb === "report") {
1213
+ throw new AutoresearchCommandError(
1214
+ 2,
1215
+ 'unknown verb "report" — the report verb was removed; missions end at verdict and the ledger is the record',
1216
+ );
1217
+ }
1218
+ if (verb === "handoff") {
1219
+ throw new AutoresearchCommandError(
1220
+ 2,
1221
+ 'unknown verb "handoff" — for spec intake use `gjc autoresearch intake --spec <path>`; for workflow handoff invoke the next skill directly (/skill:<callee>) or run `gjc state autoresearch handoff --to <callee>`',
1222
+ );
1223
+ }
1224
+ if (verb === "intake") {
1225
+ assertOnlyAutoresearchFlags(args.slice(1), ["--spec", "--json"]);
1226
+ const intakeSpecPath = requiredFlagValue(args, "--spec").trim();
1227
+ if (intakeSpecPath === "") {
1228
+ return { status: 2, stderr: "--spec requires a non-empty path\n" };
1229
+ }
1230
+ const json = hasFlag(args, "--json");
1231
+ const receipt = await autoresearchIntake({ cwd, specPath: intakeSpecPath });
1232
+ await reconcileAutoresearchState(cwd, receipt.mission);
1233
+ return {
1234
+ status: 0,
1235
+ intake: "handoff",
1236
+ missionCreated: true,
1237
+ stdout: json
1238
+ ? renderCliWriteReceipt({
1239
+ ok: true,
1240
+ intake: "handoff",
1241
+ mission: receipt.mission,
1242
+ mission_path: receipt.missionPath,
1243
+ spec_path: receipt.specPath,
1244
+ ledger_event: receipt.ledgerEvent?.event,
1245
+ })
1246
+ : renderSpecIntakeText(receipt),
1247
+ };
1248
+ }
1249
+ const specPath = flagValue(args, "--spec");
1250
+ if (specPath !== undefined) {
1251
+ if (specPath.trim() === "") {
1252
+ return { status: 2, stderr: "--spec requires a non-empty path\n" };
1253
+ }
1254
+ const json = hasFlag(args, "--json");
1255
+ const receipt = await autoresearchIntake({ cwd, specPath: specPath.trim() });
1256
+ await reconcileAutoresearchState(cwd, receipt.mission);
1257
+ return {
1258
+ status: 0,
1259
+ intake: "handoff",
1260
+ missionCreated: true,
1261
+ stdout: json
1262
+ ? renderCliWriteReceipt({
1263
+ ok: true,
1264
+ intake: "handoff",
1265
+ mission: receipt.mission,
1266
+ mission_path: receipt.missionPath,
1267
+ spec_path: receipt.specPath,
1268
+ ledger_event: receipt.ledgerEvent?.event,
1269
+ })
1270
+ : renderSpecIntakeText(receipt),
1271
+ };
1272
+ }
1273
+ const goal = extractPositionalGoal(args);
1274
+ const json = hasFlag(args, "--json");
1275
+ const payload: Record<string, unknown> = {
1276
+ ok: true,
1277
+ intake: "cold",
1278
+ clarification_required: true,
1279
+ ...(goal ? { goal } : {}),
1280
+ next: "run goal, constraints, and deliverables clarification before research begins",
1281
+ };
1282
+ return {
1283
+ status: 0,
1284
+ intake: "cold",
1285
+ stdout: json ? renderCliWriteReceipt(payload) : renderColdIntakeText(goal),
1286
+ };
1287
+ } catch (error) {
1288
+ if (error instanceof CommandError) return { status: error.exitStatus, stderr: `${error.message}\n` };
1289
+ if (error instanceof SessionResolutionError) return { status: 1, stderr: `${error.message}\n` };
1290
+ return { status: 1, stderr: `${error instanceof Error ? error.message : String(error)}\n` };
1291
+ }
1292
+ }
1293
+
1294
+ /* ------------------------------ capability surface ------------------------------ */
1295
+
1296
+ /**
1297
+ * Rebuilt autoresearch capability surface. Each capability is reachable from
1298
+ * the runtime through a thin, mission-state-aware wrapper. All are read-only
1299
+ * with respect to the ledger — none append events; the verbs above remain the
1300
+ * only writers.
1301
+ */
1302
+
1303
+ /** 1. Branch isolation: ensure the worktree is on an `autoresearch/*` branch. */
1304
+ export function autoresearchBranchIsolation(cwd: string, goal: string): Promise<EnsureAutoresearchBranchResult> {
1305
+ return ensureAutoresearchBranch(cwd, goal);
1306
+ }
1307
+
1308
+ /** 2. Harness contract: parse `METRIC`/`ASI` lines out of captured harness output. */
1309
+ export function autoresearchHarnessOutput(output: string, primaryMetricName?: string): ParsedHarnessOutput {
1310
+ return parseHarnessOutput(output, primaryMetricName);
1311
+ }
1312
+
1313
+ /**
1314
+ * 3. Durable run storage: open the session-scoped JSONL run store, seeding an
1315
+ * in-memory experiment config from the mission when none is persisted yet.
1316
+ */
1317
+ export async function autoresearchRunsStore(cwd: string, sessionId?: string | null): Promise<AutoresearchRunsStore> {
1318
+ const store = await AutoresearchRunsStore.open(cwd, sessionId);
1319
+ if (store.config === null) {
1320
+ const mission = await readAutoresearchMission(cwd, sessionId);
1321
+ if (mission) {
1322
+ store.setInMemoryConfig(
1323
+ createAutoresearchExperimentConfig({
1324
+ name: mission.slug,
1325
+ goal: mission.objective,
1326
+ // A declared metric contract must win over the default, or a
1327
+ // higher-is-better mission optimizes backwards.
1328
+ primaryMetric: mission.primaryMetric ?? "metric",
1329
+ ...(mission.metricUnit === undefined ? {} : { metricUnit: mission.metricUnit }),
1330
+ ...(mission.metricDirection === undefined ? {} : { direction: mission.metricDirection }),
1331
+ branch: null,
1332
+ }),
1333
+ );
1334
+ }
1335
+ }
1336
+ return store;
1337
+ }
1338
+
1339
+ /** 4. TUI dashboard: render the run table (collapsed + expanded) from persisted state. */
1340
+ export async function autoresearchDashboardText(cwd: string, sessionId?: string | null, width = 120): Promise<string> {
1341
+ const store = await autoresearchRunsStore(cwd, sessionId);
1342
+ const mission = await readAutoresearchMission(cwd, sessionId);
1343
+ if (!store.config) return "autoresearch: no experiment config yet";
1344
+ const state = buildAutoresearchExperimentState(store.config, store.listLoggedRuns());
1345
+ const pendingRun = store.getPendingRun();
1346
+ const input: AutoresearchDashboardInput = {
1347
+ state,
1348
+ runtime: {
1349
+ modeOn: mission !== null,
1350
+ running: null,
1351
+ pendingRun: pendingRun
1352
+ ? {
1353
+ runNumber: pendingRun.runNumber,
1354
+ passed: pendingRun.exitCode === 0 && !pendingRun.timedOut,
1355
+ parsedPrimary: pendingRun.metric,
1356
+ }
1357
+ : null,
1358
+ },
1359
+ };
1360
+ return renderExpandedDashboard(input, width);
1361
+ }
1362
+
1363
+ export async function autoresearchDataContext(
1364
+ cwd: string,
1365
+ dataFlag?: string,
1366
+ sessionId?: string | null,
1367
+ ): Promise<RlmDataContext | null> {
1368
+ const mission = await readAutoresearchMission(cwd, sessionId);
1369
+ if (!mission) return null;
1370
+ return loadAutoresearchDataContext({
1371
+ cwd,
1372
+ mode: mission.mode,
1373
+ dataFlag,
1374
+ });
1375
+ }
1376
+
1377
+ /** 8. Two-phase prompts: Phase 1 (harness setup) rendered from mission/branch state. */
1378
+ export async function autoresearchSetupPrompt(
1379
+ baseSystemPrompt: string,
1380
+ cwd: string,
1381
+ goal?: string,
1382
+ sessionId?: string | null,
1383
+ ): Promise<string> {
1384
+ const mission = await readAutoresearchMission(cwd, sessionId);
1385
+ const branch = await ensureAutoresearchBranch(cwd, goal ?? mission?.objective ?? "").then(result =>
1386
+ result.ok ? result.branchName : null,
1387
+ );
1388
+ return renderAutoresearchSetupPrompt({
1389
+ baseSystemPrompt,
1390
+ goal: goal ?? mission?.objective ?? "",
1391
+ workingDir: cwd,
1392
+ branch,
1393
+ baselineWarning: null,
1394
+ });
1395
+ }
1396
+
1397
+ /** 8. Two-phase prompts: Phase 2 (iterate) rendered from the persisted experiment state. */
1398
+ export async function autoresearchIteratePrompt(
1399
+ baseSystemPrompt: string,
1400
+ cwd: string,
1401
+ sessionId?: string | null,
1402
+ ): Promise<string> {
1403
+ const store = await autoresearchRunsStore(cwd, sessionId);
1404
+ const mission = await readAutoresearchMission(cwd, sessionId);
1405
+ if (!store.config) {
1406
+ return renderAutoresearchIteratePrompt({
1407
+ baseSystemPrompt,
1408
+ goal: mission?.objective ?? "",
1409
+ workingDir: cwd,
1410
+ branch: null,
1411
+ baselineCommit: null,
1412
+ metricName: "metric",
1413
+ metricUnit: "",
1414
+ notes: "",
1415
+ currentSegment: 1,
1416
+ currentSegmentRunCount: 0,
1417
+ baselineMetric: null,
1418
+ bestMetric: null,
1419
+ bestRunNumber: null,
1420
+ recentRuns: [],
1421
+ unjustifiedRuns: [],
1422
+ pendingRun: null,
1423
+ });
1424
+ }
1425
+ const state = buildAutoresearchExperimentState(store.config, store.listLoggedRuns());
1426
+ const current = state.results.filter(result => result.segment === state.currentSegment);
1427
+ const recentRuns = current.slice(-3).map(result => {
1428
+ const asiSummary = summarizePromptAsi(result.asi);
1429
+ return {
1430
+ run_number: result.runNumber,
1431
+ status: result.status,
1432
+ metric_display: formatPromptMetricValue(result.metric, state.metricUnit),
1433
+ description: result.description,
1434
+ has_asi_summary: Boolean(asiSummary),
1435
+ asi_summary: asiSummary ?? "",
1436
+ has_deviations: result.scopeDeviations.length > 0,
1437
+ deviations: result.scopeDeviations.join(", "),
1438
+ justified: Boolean(result.justification),
1439
+ flagged: result.flagged,
1440
+ flagged_reason: result.flaggedReason ?? "",
1441
+ };
1442
+ });
1443
+ const unjustifiedRuns = current
1444
+ .filter(
1445
+ result =>
1446
+ result.status === "keep" && !result.flagged && result.scopeDeviations.length > 0 && !result.justification,
1447
+ )
1448
+ .slice(-3)
1449
+ .map(result => ({ run_number: result.runNumber, paths: result.scopeDeviations.join(", ") }));
1450
+ const pendingRun = store.getPendingRun();
1451
+ const bestRun = findPromptBestRun(state);
1452
+ return renderAutoresearchIteratePrompt({
1453
+ baseSystemPrompt,
1454
+ goal: store.config.goal ?? mission?.objective ?? "",
1455
+ workingDir: cwd,
1456
+ branch: store.config.branch,
1457
+ baselineCommit: store.config.baselineCommit,
1458
+ metricName: state.metricName,
1459
+ metricUnit: state.metricUnit,
1460
+ notes: state.notes,
1461
+ currentSegment: state.currentSegment + 1,
1462
+ currentSegmentRunCount: current.length,
1463
+ baselineMetric: state.bestMetric,
1464
+ bestMetric: bestRun?.metric ?? state.bestMetric,
1465
+ bestRunNumber: bestRun?.runNumber ?? null,
1466
+ recentRuns,
1467
+ unjustifiedRuns,
1468
+ pendingRun: pendingRun
1469
+ ? {
1470
+ runNumber: pendingRun.runNumber,
1471
+ command: pendingRun.command,
1472
+ parsedPrimary: pendingRun.metric,
1473
+ passed: pendingRun.exitCode === 0 && !pendingRun.timedOut,
1474
+ }
1475
+ : null,
1476
+ });
1477
+ }
1478
+
1479
+ function summarizePromptAsi(asi: unknown): string | null {
1480
+ if (!asi || typeof asi !== "object") return null;
1481
+ const record = asi as Record<string, unknown>;
1482
+ const hypothesis = typeof record.hypothesis === "string" ? record.hypothesis.trim() : "";
1483
+ const rollback = typeof record.rollback_reason === "string" ? record.rollback_reason.trim() : "";
1484
+ const next = typeof record.next_action_hint === "string" ? record.next_action_hint.trim() : "";
1485
+ const summary = [hypothesis, rollback, next].filter(part => part.length > 0).join(" | ");
1486
+ return summary.length > 0 ? summary.slice(0, 220) : null;
1487
+ }
1488
+
1489
+ function formatPromptMetricValue(value: number | null, unit: string): string {
1490
+ if (value === null) return `n/a${unit ? ` ${unit}` : ""}`;
1491
+ if (Number.isInteger(value)) return `${value}${unit}`;
1492
+ return `${value.toFixed(2)}${unit}`;
1493
+ }
1494
+
1495
+ function findPromptBestRun(state: {
1496
+ results: Array<{ segment: number; status: string; metric: number | null; flagged: boolean; runNumber: number }>;
1497
+ currentSegment: number;
1498
+ bestDirection: "lower" | "higher";
1499
+ }): { metric: number; runNumber: number } | null {
1500
+ let best: { metric: number; runNumber: number } | null = null;
1501
+ for (const result of state.results) {
1502
+ if (result.segment !== state.currentSegment || result.status !== "keep" || result.flagged) continue;
1503
+ if (result.metric === null) continue;
1504
+ if (!best || (state.bestDirection === "lower" ? result.metric < best.metric : result.metric > best.metric)) {
1505
+ best = { metric: result.metric, runNumber: result.runNumber };
1506
+ }
1507
+ }
1508
+ return best;
1509
+ }