@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,708 @@
1
+ /**
2
+ * Durable autoresearch run storage: session-scoped JSONL run records plus
3
+ * baseline/best-metric math, ported from the deleted extension's
4
+ * `storage.ts` + `state.ts` without SQLite.
5
+ *
6
+ * Everything lives under the session autoresearch root
7
+ * (`<cwd>/.gjc/_session-{id}/autoresearch/`): `runs.jsonl` holds one JSON
8
+ * object per run and `experiment.json` holds the metric/session configuration.
9
+ * The legacy global store (a per-project SQLite DB keyed outside the session)
10
+ * is intentionally dead — no code path here resolves or writes it.
11
+ *
12
+ * Flagged runs MUST be excluded from baseline and best-metric computation;
13
+ * metric direction (lower-is-better vs higher-is-better) is respected.
14
+ */
15
+ import * as crypto from "node:crypto";
16
+ import * as path from "node:path";
17
+ import { sessionAutoresearchDir } from "../gjc-runtime/session-layout";
18
+ import { resolveGjcSessionForWrite } from "../gjc-runtime/session-resolution";
19
+ import {
20
+ appendJsonl,
21
+ type StateWriterOptions,
22
+ withWorkflowStateLock,
23
+ writeTextAtomic,
24
+ } from "../gjc-runtime/state-writer";
25
+ import { dedupeStrings, inferMetricUnitFromName, isBetter, normalizePathSpec } from "./harness";
26
+ import type { ASIData, ExperimentStatus, MetricDef, MetricDirection, NumericMetricMap } from "./types";
27
+
28
+ export type { ASIData, ExperimentStatus, MetricDirection, NumericMetricMap };
29
+
30
+ export interface AutoresearchExperimentConfig {
31
+ name: string;
32
+ goal: string | null;
33
+ primaryMetric: string;
34
+ metricUnit: string;
35
+ direction: MetricDirection;
36
+ preferredCommand: string | null;
37
+ branch: string | null;
38
+ baselineCommit: string | null;
39
+ currentSegment: number;
40
+ maxIterations: number | null;
41
+ scopePaths: string[];
42
+ offLimits: string[];
43
+ constraints: string[];
44
+ secondaryMetrics: string[];
45
+ notes: string;
46
+ createdAt: number;
47
+ closedAt: number | null;
48
+ }
49
+
50
+ export interface AutoresearchRunRecord {
51
+ runId: string;
52
+ runNumber: number;
53
+ segment: number;
54
+ command: string;
55
+ startedAt: number;
56
+ completedAt: number | null;
57
+ durationMs: number | null;
58
+ exitCode: number | null;
59
+ timedOut: boolean;
60
+ status: ExperimentStatus | null;
61
+ description: string | null;
62
+ metric: number | null;
63
+ metrics: NumericMetricMap;
64
+ asi: ASIData | null;
65
+ commitHash: string | null;
66
+ confidence: number | null;
67
+ preRunDirtyPaths: string[];
68
+ modifiedPaths: string[];
69
+ scopeDeviations: string[];
70
+ justification: string | null;
71
+ flagged: boolean;
72
+ flaggedReason: string | null;
73
+ loggedAt: number | null;
74
+ abandonedAt: number | null;
75
+ }
76
+
77
+ export interface AutoresearchRunsPaths {
78
+ dir: string;
79
+ runsPath: string;
80
+ configPath: string;
81
+ }
82
+
83
+ export function autoresearchRunsPaths(cwd: string, sessionId?: string | null): AutoresearchRunsPaths {
84
+ const resolvedSessionId =
85
+ sessionId?.trim() || resolveGjcSessionForWrite(cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
86
+ const dir = sessionAutoresearchDir(cwd, resolvedSessionId);
87
+ return {
88
+ dir,
89
+ runsPath: path.join(dir, "runs.jsonl"),
90
+ configPath: path.join(dir, "experiment.json"),
91
+ };
92
+ }
93
+
94
+ function auditFor(
95
+ cwd: string,
96
+ sessionId: string,
97
+ verb: string,
98
+ category: "state" | "ledger" = "state",
99
+ ): StateWriterOptions {
100
+ return {
101
+ cwd,
102
+ audit: {
103
+ category,
104
+ verb,
105
+ owner: "gjc-runtime",
106
+ skill: "autoresearch",
107
+ sessionId,
108
+ },
109
+ };
110
+ }
111
+
112
+ const DENIED_KEY_NAMES = new Set(["__proto__", "constructor", "prototype"]);
113
+
114
+ function sanitizeMetrics(value: unknown): NumericMetricMap {
115
+ if (typeof value !== "object" || value === null) return {};
116
+ const out: NumericMetricMap = {};
117
+ for (const [key, entryValue] of Object.entries(value as Record<string, unknown>)) {
118
+ if (DENIED_KEY_NAMES.has(key)) continue;
119
+ if (typeof entryValue === "number" && Number.isFinite(entryValue)) out[key] = entryValue;
120
+ }
121
+ return out;
122
+ }
123
+
124
+ function normalizeRunRecord(value: unknown): AutoresearchRunRecord | null {
125
+ if (typeof value !== "object" || value === null) return null;
126
+ const record = value as Record<string, unknown>;
127
+ if (typeof record.runId !== "string" || record.runId === "") return null;
128
+ const status = record.status as AutoresearchRunRecord["status"];
129
+ if (
130
+ status !== null &&
131
+ status !== undefined &&
132
+ status !== "keep" &&
133
+ status !== "discard" &&
134
+ status !== "crash" &&
135
+ status !== "checks_failed"
136
+ ) {
137
+ return null;
138
+ }
139
+ return {
140
+ runId: record.runId,
141
+ runNumber: typeof record.runNumber === "number" ? record.runNumber : 0,
142
+ segment: typeof record.segment === "number" ? record.segment : 0,
143
+ command: typeof record.command === "string" ? record.command : "",
144
+ startedAt: typeof record.startedAt === "number" ? record.startedAt : 0,
145
+ completedAt: typeof record.completedAt === "number" ? record.completedAt : null,
146
+ durationMs: typeof record.durationMs === "number" ? record.durationMs : null,
147
+ exitCode: typeof record.exitCode === "number" ? record.exitCode : null,
148
+ timedOut: record.timedOut === true,
149
+ status: status ?? null,
150
+ description: typeof record.description === "string" ? record.description : null,
151
+ metric: typeof record.metric === "number" && Number.isFinite(record.metric) ? record.metric : null,
152
+ metrics: sanitizeMetrics(record.metrics),
153
+ asi: typeof record.asi === "object" && record.asi !== null ? (record.asi as ASIData) : null,
154
+ commitHash: typeof record.commitHash === "string" ? record.commitHash : null,
155
+ confidence:
156
+ typeof record.confidence === "number" && Number.isFinite(record.confidence) ? record.confidence : null,
157
+ preRunDirtyPaths: toStringArray(record.preRunDirtyPaths),
158
+ modifiedPaths: toStringArray(record.modifiedPaths),
159
+ scopeDeviations: toStringArray(record.scopeDeviations),
160
+ justification: typeof record.justification === "string" ? record.justification : null,
161
+ flagged: record.flagged === true,
162
+ flaggedReason: typeof record.flaggedReason === "string" ? record.flaggedReason : null,
163
+ loggedAt: typeof record.loggedAt === "number" ? record.loggedAt : null,
164
+ abandonedAt: typeof record.abandonedAt === "number" ? record.abandonedAt : null,
165
+ };
166
+ }
167
+
168
+ function toStringArray(value: unknown): string[] {
169
+ if (!Array.isArray(value)) return [];
170
+ return value.filter((item): item is string => typeof item === "string");
171
+ }
172
+
173
+ export function createAutoresearchExperimentConfig(input: {
174
+ name: string;
175
+ goal?: string | null;
176
+ primaryMetric: string;
177
+ metricUnit?: string;
178
+ direction?: MetricDirection;
179
+ preferredCommand?: string | null;
180
+ branch?: string | null;
181
+ baselineCommit?: string | null;
182
+ maxIterations?: number | null;
183
+ scopePaths?: string[];
184
+ offLimits?: string[];
185
+ constraints?: string[];
186
+ secondaryMetrics?: string[];
187
+ notes?: string;
188
+ createdAt?: number;
189
+ }): AutoresearchExperimentConfig {
190
+ return {
191
+ name: input.name,
192
+ goal: input.goal ?? null,
193
+ primaryMetric: input.primaryMetric,
194
+ metricUnit: input.metricUnit ?? "",
195
+ direction: input.direction ?? "lower",
196
+ preferredCommand: input.preferredCommand ?? null,
197
+ branch: input.branch ?? null,
198
+ baselineCommit: input.baselineCommit ?? null,
199
+ currentSegment: 0,
200
+ maxIterations: input.maxIterations ?? null,
201
+ scopePaths: dedupeStrings((input.scopePaths ?? []).map(normalizePathSpec)),
202
+ offLimits: dedupeStrings((input.offLimits ?? []).map(normalizePathSpec)),
203
+ constraints: dedupeStrings(input.constraints ?? []),
204
+ secondaryMetrics: dedupeStrings(input.secondaryMetrics ?? []),
205
+ notes: input.notes ?? "",
206
+ createdAt: input.createdAt ?? Date.now(),
207
+ closedAt: null,
208
+ };
209
+ }
210
+
211
+ function normalizeExperimentConfig(value: unknown): AutoresearchExperimentConfig | null {
212
+ if (typeof value !== "object" || value === null) return null;
213
+ const record = value as Record<string, unknown>;
214
+ if (typeof record.name !== "string" || record.name === "") return null;
215
+ const direction: MetricDirection = record.direction === "higher" ? "higher" : "lower";
216
+ return {
217
+ name: record.name,
218
+ goal: typeof record.goal === "string" ? record.goal : null,
219
+ primaryMetric: typeof record.primaryMetric === "string" ? record.primaryMetric : "metric",
220
+ metricUnit: typeof record.metricUnit === "string" ? record.metricUnit : "",
221
+ direction,
222
+ preferredCommand: typeof record.preferredCommand === "string" ? record.preferredCommand : null,
223
+ branch: typeof record.branch === "string" ? record.branch : null,
224
+ baselineCommit: typeof record.baselineCommit === "string" ? record.baselineCommit : null,
225
+ currentSegment: typeof record.currentSegment === "number" ? record.currentSegment : 0,
226
+ maxIterations: typeof record.maxIterations === "number" ? record.maxIterations : null,
227
+ scopePaths: toStringArray(record.scopePaths),
228
+ offLimits: toStringArray(record.offLimits),
229
+ constraints: toStringArray(record.constraints),
230
+ secondaryMetrics: toStringArray(record.secondaryMetrics),
231
+ notes: typeof record.notes === "string" ? record.notes : "",
232
+ createdAt: typeof record.createdAt === "number" ? record.createdAt : Date.now(),
233
+ closedAt: typeof record.closedAt === "number" ? record.closedAt : null,
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Session-scoped JSONL run store. Reads are served from a cached in-memory
239
+ * snapshot; every mutation routes through the sanctioned `.gjc/**` state-writer
240
+ * primitives (append for new runs, locked rewrite for updates).
241
+ */
242
+ export class AutoresearchRunsStore {
243
+ readonly #paths: AutoresearchRunsPaths;
244
+ readonly #cwd: string;
245
+ readonly #sessionId: string;
246
+ #config: AutoresearchExperimentConfig | null;
247
+ #runs: AutoresearchRunRecord[];
248
+
249
+ private constructor(input: {
250
+ paths: AutoresearchRunsPaths;
251
+ cwd: string;
252
+ sessionId: string;
253
+ config: AutoresearchExperimentConfig | null;
254
+ runs: AutoresearchRunRecord[];
255
+ }) {
256
+ this.#paths = input.paths;
257
+ this.#cwd = input.cwd;
258
+ this.#sessionId = input.sessionId;
259
+ this.#config = input.config;
260
+ this.#runs = input.runs;
261
+ }
262
+
263
+ static async open(cwd: string, sessionId?: string | null): Promise<AutoresearchRunsStore> {
264
+ const resolvedSessionId =
265
+ sessionId?.trim() || resolveGjcSessionForWrite(cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
266
+ const paths = autoresearchRunsPaths(cwd, resolvedSessionId);
267
+ const config = await readExperimentConfig(paths.configPath);
268
+ const runs = await readRunRecords(paths.runsPath);
269
+ return new AutoresearchRunsStore({
270
+ paths,
271
+ cwd,
272
+ sessionId: resolvedSessionId,
273
+ config,
274
+ runs,
275
+ });
276
+ }
277
+
278
+ get paths(): AutoresearchRunsPaths {
279
+ return this.#paths;
280
+ }
281
+
282
+ get config(): AutoresearchExperimentConfig | null {
283
+ return this.#config;
284
+ }
285
+
286
+ get runs(): AutoresearchRunRecord[] {
287
+ return [...this.#runs];
288
+ }
289
+
290
+ /** Persist the experiment configuration (first write creates the file). */
291
+ async saveConfig(config: AutoresearchExperimentConfig): Promise<void> {
292
+ await writeTextAtomic(
293
+ this.#paths.configPath,
294
+ `${JSON.stringify(config, null, 2)}\n`,
295
+ auditFor(this.#cwd, this.#sessionId, "write"),
296
+ );
297
+ this.#config = config;
298
+ this.#config = config;
299
+ }
300
+
301
+ /** Seed the in-memory config without persisting (mission-derived defaults). */
302
+ setInMemoryConfig(config: AutoresearchExperimentConfig): void {
303
+ this.#config = config;
304
+ }
305
+
306
+ /** Start a run: append its record line and return the created record. */
307
+ async startRun(input: {
308
+ segment?: number;
309
+ command: string;
310
+ preRunDirtyPaths?: string[];
311
+ startedAt?: number;
312
+ }): Promise<AutoresearchRunRecord> {
313
+ const runNumber = this.#runs.reduce((max, run) => Math.max(max, run.runNumber), 0) + 1;
314
+ const record: AutoresearchRunRecord = {
315
+ runId: cryptoRandomId(),
316
+ runNumber,
317
+ segment: input.segment ?? this.#config?.currentSegment ?? 0,
318
+ command: input.command,
319
+ startedAt: input.startedAt ?? Date.now(),
320
+ completedAt: null,
321
+ durationMs: null,
322
+ exitCode: null,
323
+ timedOut: false,
324
+ status: null,
325
+ description: null,
326
+ metric: null,
327
+ metrics: {},
328
+ asi: null,
329
+ commitHash: null,
330
+ confidence: null,
331
+ preRunDirtyPaths: [...(input.preRunDirtyPaths ?? [])],
332
+ modifiedPaths: [],
333
+ scopeDeviations: [],
334
+ justification: null,
335
+ flagged: false,
336
+ flaggedReason: null,
337
+ loggedAt: null,
338
+ abandonedAt: null,
339
+ };
340
+ await appendJsonl(this.#paths.runsPath, record, auditFor(this.#cwd, this.#sessionId, "run_started", "ledger"));
341
+ this.#runs.push(record);
342
+ return { ...record };
343
+ }
344
+
345
+ /** Mark a completed run (exit code, duration, parsed metrics/ASI). */
346
+ async completeRun(runId: string, patch: Partial<AutoresearchRunRecord>): Promise<AutoresearchRunRecord> {
347
+ return this.#updateRun(runId, current => ({
348
+ ...current,
349
+ ...patch,
350
+ completedAt: patch.completedAt ?? Date.now(),
351
+ }));
352
+ }
353
+
354
+ /** Log a run: final status, description, metric, ASI, paths, confidence. */
355
+ async logRun(runId: string, patch: Partial<AutoresearchRunRecord>): Promise<AutoresearchRunRecord> {
356
+ return this.#updateRun(runId, current => ({
357
+ ...current,
358
+ ...patch,
359
+ status: patch.status ?? null,
360
+ loggedAt: patch.loggedAt ?? Date.now(),
361
+ }));
362
+ }
363
+
364
+ /** Flag a run as suspect; flagged runs are excluded from baseline/best math. */
365
+ async flagRun(runId: string, reason: string): Promise<AutoresearchRunRecord> {
366
+ return this.#updateRun(runId, current => ({
367
+ ...current,
368
+ flagged: true,
369
+ flaggedReason: reason,
370
+ }));
371
+ }
372
+
373
+ async updateRunConfidence(runId: string, confidence: number | null): Promise<AutoresearchRunRecord> {
374
+ return this.#updateRun(runId, current => ({ ...current, confidence }));
375
+ }
376
+
377
+ /** Abandon every pending run; returns how many were abandoned. */
378
+ async abandonPendingRuns(): Promise<number> {
379
+ const pending = this.#runs.filter(run => run.status === null && run.abandonedAt === null);
380
+ if (pending.length === 0) return 0;
381
+ await this.#rewrite(runs => {
382
+ for (const run of runs) {
383
+ if (run.status === null && run.abandonedAt === null) {
384
+ run.abandonedAt = Date.now();
385
+ }
386
+ }
387
+ });
388
+ return pending.length;
389
+ }
390
+
391
+ /** Most recent pending (started, unlogged, unabandoned) run, or null. */
392
+ getPendingRun(): AutoresearchRunRecord | null {
393
+ for (let index = this.#runs.length - 1; index >= 0; index -= 1) {
394
+ const run = this.#runs[index];
395
+ if (run.status === null && run.abandonedAt === null) return { ...run };
396
+ }
397
+ return null;
398
+ }
399
+
400
+ listRuns(): AutoresearchRunRecord[] {
401
+ return this.#runs.map(run => ({ ...run }));
402
+ }
403
+
404
+ listLoggedRuns(): AutoresearchRunRecord[] {
405
+ return this.#runs.filter(run => run.status !== null).map(run => ({ ...run }));
406
+ }
407
+
408
+ async #updateRun(
409
+ runId: string,
410
+ mutate: (current: AutoresearchRunRecord) => AutoresearchRunRecord,
411
+ ): Promise<AutoresearchRunRecord> {
412
+ let updated: AutoresearchRunRecord | null = null;
413
+ await this.#rewrite(runs => {
414
+ for (const run of runs) {
415
+ if (run.runId === runId) {
416
+ const next = mutate(run);
417
+ Object.assign(run, next);
418
+ updated = { ...next };
419
+ break;
420
+ }
421
+ }
422
+ });
423
+ if (updated === null) {
424
+ throw new Error(`Autoresearch run ${runId} not found`);
425
+ }
426
+ return updated;
427
+ }
428
+
429
+ /** Locked read-modify-write of runs.jsonl; refreshes the in-memory snapshot. */
430
+ async #rewrite(mutate: (runs: AutoresearchRunRecord[]) => void): Promise<void> {
431
+ await withWorkflowStateLock(
432
+ this.#paths.runsPath,
433
+ async () => {
434
+ const runs = await readRunRecords(this.#paths.runsPath);
435
+ mutate(runs);
436
+ const text = runs.map(run => JSON.stringify(run)).join("\n");
437
+ await writeTextAtomic(
438
+ this.#paths.runsPath,
439
+ text.length > 0 ? `${text}\n` : "",
440
+ auditFor(this.#cwd, this.#sessionId, "run_updated", "ledger"),
441
+ );
442
+ this.#runs = runs.map(run => ({ ...run }));
443
+ },
444
+ { cwd: this.#cwd },
445
+ );
446
+ }
447
+ }
448
+
449
+ function cryptoRandomId(): string {
450
+ return crypto.randomUUID();
451
+ }
452
+
453
+ async function readRunRecords(runsPath: string): Promise<AutoresearchRunRecord[]> {
454
+ const records: AutoresearchRunRecord[] = [];
455
+ try {
456
+ const raw = await Bun.file(runsPath).text();
457
+ for (const line of raw.split(/\r?\n/)) {
458
+ const trimmed = line.trim();
459
+ if (trimmed.length === 0) continue;
460
+ const record = normalizeRunRecord(JSON.parse(trimmed) as unknown);
461
+ if (record) records.push(record);
462
+ }
463
+ } catch (error) {
464
+ if (isEnoent(error)) return [];
465
+ throw error;
466
+ }
467
+ return records;
468
+ }
469
+
470
+ async function readExperimentConfig(configPath: string): Promise<AutoresearchExperimentConfig | null> {
471
+ try {
472
+ return normalizeExperimentConfig(await Bun.file(configPath).json());
473
+ } catch (error) {
474
+ if (isEnoent(error)) return null;
475
+ throw error;
476
+ }
477
+ }
478
+
479
+ function isEnoent(error: unknown): boolean {
480
+ return (
481
+ typeof error === "object" && error !== null && "code" in error && (error as { code?: unknown }).code === "ENOENT"
482
+ );
483
+ }
484
+
485
+ // ---------------------------------------------------------------------------
486
+ // Baseline / best-metric math (ported from the extension's state.ts)
487
+ // ---------------------------------------------------------------------------
488
+
489
+ /**
490
+ * Minimal structural shape the math operates on; both persisted run records
491
+ * and rebuilt experiment results satisfy it.
492
+ */
493
+ export interface BaselineCandidate {
494
+ segment: number;
495
+ status: ExperimentStatus | null;
496
+ metric: number | null;
497
+ metrics: NumericMetricMap;
498
+ flagged: boolean;
499
+ runNumber: number;
500
+ }
501
+
502
+ export function currentResults(results: readonly BaselineCandidate[], segment: number): BaselineCandidate[] {
503
+ return results.filter(result => result.segment === segment);
504
+ }
505
+
506
+ /** First kept, unflagged run in the segment — the baseline. */
507
+ export function findBaselineResult(results: readonly BaselineCandidate[], segment: number): BaselineCandidate | null {
508
+ return currentResults(results, segment).find(result => result.status === "keep" && !result.flagged) ?? null;
509
+ }
510
+
511
+ export function findBaselineMetric(results: readonly BaselineCandidate[], segment: number): number | null {
512
+ const baseline = findBaselineResult(results, segment);
513
+ return baseline && baseline.metric !== null ? baseline.metric : null;
514
+ }
515
+
516
+ /** Best kept, unflagged metric in the segment, honoring direction. */
517
+ export function findBestKeptMetric(
518
+ results: readonly BaselineCandidate[],
519
+ segment: number,
520
+ direction: MetricDirection,
521
+ ): number | null {
522
+ let best: number | null = null;
523
+ for (const result of currentResults(results, segment)) {
524
+ if (result.status !== "keep" || result.flagged || result.metric === null) continue;
525
+ if (best === null || isBetter(result.metric, best, direction)) {
526
+ best = result.metric;
527
+ }
528
+ }
529
+ return best;
530
+ }
531
+
532
+ export function findBaselineRunNumber(results: readonly BaselineCandidate[], segment: number): number | null {
533
+ const baseline = findBaselineResult(results, segment);
534
+ return baseline ? baseline.runNumber : null;
535
+ }
536
+
537
+ /** Baseline secondary-metric values, filling gaps from other unflagged runs. */
538
+ export function findBaselineSecondary(
539
+ results: readonly BaselineCandidate[],
540
+ segment: number,
541
+ knownMetrics: MetricDef[],
542
+ ): NumericMetricMap {
543
+ const baseline = findBaselineResult(results, segment);
544
+ const values: NumericMetricMap = baseline ? { ...baseline.metrics } : {};
545
+ for (const metric of knownMetrics) {
546
+ if (values[metric.name] !== undefined) continue;
547
+ for (const result of currentResults(results, segment)) {
548
+ if (result.flagged) continue;
549
+ const value = result.metrics[metric.name];
550
+ if (value !== undefined) {
551
+ values[metric.name] = value;
552
+ break;
553
+ }
554
+ }
555
+ }
556
+ return values;
557
+ }
558
+
559
+ export function sortedMedian(values: number[]): number {
560
+ if (values.length === 0) return 0;
561
+ const sorted = [...values].sort((left, right) => left - right);
562
+ const midpoint = Math.floor(sorted.length / 2);
563
+ if (sorted.length % 2 === 0) {
564
+ return (sorted[midpoint - 1]! + sorted[midpoint]!) / 2;
565
+ }
566
+ return sorted[midpoint]!;
567
+ }
568
+
569
+ /**
570
+ * Confidence score: how many multiples of the observed noise floor (median
571
+ * absolute deviation) separate the best kept metric from the baseline. Null
572
+ * when there are too few runs, no baseline, or no measurable spread.
573
+ */
574
+ export function computeConfidence(
575
+ results: readonly BaselineCandidate[],
576
+ segment: number,
577
+ direction: MetricDirection,
578
+ ): number | null {
579
+ const current = currentResults(results, segment).filter(
580
+ result => !result.flagged && result.metric !== null && result.metric > 0,
581
+ );
582
+ if (current.length < 3) return null;
583
+
584
+ const values = current.map(result => result.metric as number);
585
+ const median = sortedMedian(values);
586
+ const mad = sortedMedian(values.map(value => Math.abs(value - median)));
587
+ if (mad === 0) return null;
588
+
589
+ const baseline = findBaselineMetric(results, segment);
590
+ if (baseline === null) return null;
591
+
592
+ let bestKept: number | null = null;
593
+ for (const result of current) {
594
+ if (result.status !== "keep" || (result.metric ?? 0) <= 0) continue;
595
+ if (bestKept === null || isBetter(result.metric as number, bestKept, direction)) {
596
+ bestKept = result.metric as number;
597
+ }
598
+ }
599
+ if (bestKept === null || bestKept === baseline) return null;
600
+
601
+ return Math.abs(bestKept - baseline) / mad;
602
+ }
603
+
604
+ export interface AutoresearchExperimentResult {
605
+ runNumber: number;
606
+ commit: string;
607
+ metric: number | null;
608
+ metrics: NumericMetricMap;
609
+ status: ExperimentStatus;
610
+ description: string;
611
+ timestamp: number;
612
+ segment: number;
613
+ confidence: number | null;
614
+ asi?: ASIData;
615
+ modifiedPaths: string[];
616
+ scopeDeviations: string[];
617
+ justification: string | null;
618
+ flagged: boolean;
619
+ flaggedReason: string | null;
620
+ }
621
+
622
+ export interface AutoresearchExperimentState {
623
+ results: AutoresearchExperimentResult[];
624
+ bestMetric: number | null;
625
+ bestDirection: MetricDirection;
626
+ metricName: string;
627
+ metricUnit: string;
628
+ secondaryMetrics: MetricDef[];
629
+ name: string | null;
630
+ goal: string | null;
631
+ currentSegment: number;
632
+ maxExperiments: number | null;
633
+ confidence: number | null;
634
+ scopePaths: string[];
635
+ offLimits: string[];
636
+ constraints: string[];
637
+ notes: string;
638
+ branch: string | null;
639
+ baselineCommit: string | null;
640
+ }
641
+
642
+ /** Rebuild the experiment state view from the persisted config + logged runs. */
643
+ export function buildAutoresearchExperimentState(
644
+ config: AutoresearchExperimentConfig,
645
+ loggedRuns: AutoresearchRunRecord[],
646
+ ): AutoresearchExperimentState {
647
+ const secondaryMetrics: MetricDef[] = config.secondaryMetrics.map(name => ({
648
+ name,
649
+ unit: inferMetricUnitFromName(name),
650
+ }));
651
+ const results: AutoresearchExperimentResult[] = [];
652
+ for (const run of loggedRuns) {
653
+ if (run.status === null) continue;
654
+ const result: AutoresearchExperimentResult = {
655
+ runNumber: run.runNumber,
656
+ commit: run.commitHash ?? "",
657
+ metric: run.metric,
658
+ metrics: { ...run.metrics },
659
+ status: run.status,
660
+ description: run.description ?? "",
661
+ timestamp: run.loggedAt ?? run.startedAt,
662
+ segment: run.segment,
663
+ confidence: run.confidence,
664
+ asi: run.asi ?? undefined,
665
+ modifiedPaths: [...run.modifiedPaths],
666
+ scopeDeviations: [...run.scopeDeviations],
667
+ justification: run.justification,
668
+ flagged: run.flagged,
669
+ flaggedReason: run.flaggedReason,
670
+ };
671
+ results.push(result);
672
+ if (run.segment === config.currentSegment) {
673
+ registerSecondaryMetrics(secondaryMetrics, run.metrics);
674
+ }
675
+ }
676
+
677
+ const bestMetric = findBaselineMetric(results, config.currentSegment);
678
+ const confidence = computeConfidence(results, config.currentSegment, config.direction);
679
+ return {
680
+ results,
681
+ bestMetric,
682
+ bestDirection: config.direction,
683
+ metricName: config.primaryMetric,
684
+ metricUnit: config.metricUnit,
685
+ secondaryMetrics,
686
+ name: config.name,
687
+ goal: config.goal,
688
+ currentSegment: config.currentSegment,
689
+ maxExperiments: config.maxIterations,
690
+ confidence,
691
+ scopePaths: [...config.scopePaths],
692
+ offLimits: [...config.offLimits],
693
+ constraints: [...config.constraints],
694
+ notes: config.notes,
695
+ branch: config.branch,
696
+ baselineCommit: config.baselineCommit,
697
+ };
698
+ }
699
+
700
+ function registerSecondaryMetrics(metrics: MetricDef[], values: NumericMetricMap): void {
701
+ for (const name of Object.keys(values)) {
702
+ if (metrics.some(metric => metric.name === name)) continue;
703
+ metrics.push({
704
+ name,
705
+ unit: inferMetricUnitFromName(name),
706
+ });
707
+ }
708
+ }