@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,616 @@
1
+ /**
2
+ * Structured workflow recovery projection for compaction (#4560).
3
+ *
4
+ * Compaction previously preserved only a thin best-effort state projection
5
+ * (active goal objective/status, workflow phase, open todos) plus a generic
6
+ * continuation prompt. Long Ralplan/Ultragoal runs could therefore lose the
7
+ * precise accepted scope/progress/evidence contract, then drift or spin in
8
+ * zero-progress continuation loops after compaction.
9
+ *
10
+ * This module derives a bounded structured projection from the canonical
11
+ * durable state — Ralplan `final`/`index.jsonl` run artifacts and Ultragoal
12
+ * `goals.json` + `ledger.jsonl` — through read-only filesystem access. It
13
+ * never mutates workflow state and degrades safely (undefined) on malformed,
14
+ * stale, or tampered durable state so compaction falls back to the previous
15
+ * thin projection rather than failing.
16
+ */
17
+ import * as crypto from "node:crypto";
18
+ import { constants as nodeFsConstants } from "node:fs";
19
+ import * as fs from "node:fs/promises";
20
+ import * as path from "node:path";
21
+ import { modeStatePath, sessionPlansDir } from "./session-layout";
22
+ import { getUltragoalPaths, readUltragoalLedger, readUltragoalPlan } from "./ultragoal-runtime";
23
+
24
+ /** Skills whose durable state can produce a structured recovery projection. */
25
+ export type WorkflowRecoverySkill = "ralplan" | "ultragoal";
26
+
27
+ export interface WorkflowRecoveryScopeItem {
28
+ kind: "accepted" | "non_goal";
29
+ text: string;
30
+ }
31
+
32
+ export interface WorkflowRecoveryProjection {
33
+ skill: WorkflowRecoverySkill;
34
+ /** Canonical durable state that produced this projection. */
35
+ source: "ralplan-final" | "ralplan-run" | "ultragoal-plan";
36
+ /** Bounded accepted objective for the current work contract. */
37
+ objective: string;
38
+ /** Bounded accepted scope + explicit non-goals (scope reload, not expansion). */
39
+ scope: WorkflowRecoveryScopeItem[];
40
+ /** Bounded acceptance criteria / verification obligations. */
41
+ acceptanceCriteria: string[];
42
+ /** Unresolved decisions carried from the durable contract, bounded. */
43
+ unresolved: string[];
44
+ /** Durable identity + integrity digest of the source state. */
45
+ provenance: {
46
+ planPath?: string;
47
+ runId?: string;
48
+ stage?: string;
49
+ sha256?: string;
50
+ };
51
+ /** Current goal + measurable progress counters from canonical state. */
52
+ currentGoal?: {
53
+ goalId: string;
54
+ status: string;
55
+ objective: string;
56
+ };
57
+ progress: {
58
+ totalGoals?: number;
59
+ completedGoals?: number;
60
+ outstandingGoals?: number;
61
+ /** Latest boundary review generation recorded in the ledger. */
62
+ latestReviewGeneration?: number;
63
+ /** Frozen source hash of the latest joined review cohort, if any. */
64
+ latestCohortSourceHash?: string;
65
+ /** Ledger event id of the newest event backing this projection. */
66
+ latestLedgerEventId?: string;
67
+ };
68
+ /** Exact next bounded action class for resumption. */
69
+ nextAction: {
70
+ actionClass:
71
+ | "continue-current-goal"
72
+ | "start-next-goal"
73
+ | "resolve-review-blockers"
74
+ | "run-boundary-cohort"
75
+ | "run-plan-review"
76
+ | "revise-plan"
77
+ | "reconcile-intent"
78
+ | "final-aggregate-checkpoint"
79
+ | "awaiting-approval"
80
+ | "unknown";
81
+ goalId?: string;
82
+ detail?: string;
83
+ };
84
+ /** #4560: measurable-progress basis for bounding zero-progress cycles. */
85
+ zeroProgress: {
86
+ fingerprint: string;
87
+ unchangedObservations: number;
88
+ stalled: boolean;
89
+ };
90
+ }
91
+ /** #4560: compaction-observation memory for zero-progress bounding. */
92
+ export interface WorkflowRecoveryZeroProgressMemory {
93
+ /** Last observed progress fingerprint per skill. */
94
+ lastFingerprint?: string;
95
+ /** Consecutive compaction observations with an unchanged fingerprint. */
96
+ unchangedObservations: number;
97
+ }
98
+
99
+ /** #4560: bound repeated zero-progress continuation cycles (#4560). */
100
+ export const ZERO_PROGRESS_STALL_THRESHOLD = 2;
101
+
102
+ export function trackWorkflowRecoveryZeroProgress(
103
+ memory: WorkflowRecoveryZeroProgressMemory | undefined,
104
+ projection: WorkflowRecoveryProjection,
105
+ ): WorkflowRecoveryZeroProgressMemory {
106
+ const fingerprint = hashWorkflowRecoveryProjection(projection);
107
+ if (!memory) return { lastFingerprint: fingerprint, unchangedObservations: 0 };
108
+ const unchanged = memory.lastFingerprint === fingerprint ? memory.unchangedObservations + 1 : 0;
109
+ return { lastFingerprint: fingerprint, unchangedObservations: unchanged };
110
+ }
111
+
112
+ export function isWorkflowRecoveryStalled(memory: WorkflowRecoveryZeroProgressMemory | undefined): boolean {
113
+ return (memory?.unchangedObservations ?? 0) >= ZERO_PROGRESS_STALL_THRESHOLD;
114
+ }
115
+
116
+ const MAX_OBJECTIVE_CHARS = 600;
117
+ const MAX_ITEM_CHARS = 240;
118
+ const MAX_SCOPE_ITEMS = 12;
119
+ const MAX_CRITERIA_ITEMS = 12;
120
+ const MAX_UNRESOLVED_ITEMS = 8;
121
+
122
+ function boundText(value: unknown, maxChars: number): string | undefined {
123
+ if (typeof value !== "string") return undefined;
124
+ const trimmed = value.trim();
125
+ if (trimmed.length === 0) return undefined;
126
+ return trimmed.length > maxChars ? `${trimmed.slice(0, maxChars - 1)}…` : trimmed;
127
+ }
128
+
129
+ /**
130
+ * Read an artifact exactly once and derive both its text and its digest from
131
+ * the same bytes. Reading for projection and reopening for hashing lets a
132
+ * concurrent replacement produce a digest over benign bytes while different
133
+ * bytes reach the continuation prompt, so the two must never be split.
134
+ * The handle is opened with `O_NOFOLLOW` and its identity is verified to be a
135
+ * regular file before any bytes are trusted.
136
+ */
137
+ async function readArtifactWithDigest(filePath: string): Promise<{ text: string; sha256: string } | undefined> {
138
+ if (process.platform === "win32") return undefined;
139
+ let handle: fs.FileHandle | undefined;
140
+ try {
141
+ handle = await fs.open(filePath, nodeFsConstants.O_RDONLY | nodeFsConstants.O_NOFOLLOW);
142
+ const stat = await handle.stat();
143
+ if (!stat.isFile()) return undefined;
144
+ const buffer = await handle.readFile();
145
+ return {
146
+ text: buffer.toString("utf8"),
147
+ sha256: `sha256:${crypto.createHash("sha256").update(buffer).digest("hex")}`,
148
+ };
149
+ } catch {
150
+ return undefined;
151
+ } finally {
152
+ await handle?.close().catch(() => {});
153
+ }
154
+ }
155
+
156
+ interface ParsedHeadingSection {
157
+ title: string;
158
+ lines: string[];
159
+ }
160
+
161
+ /** Split a markdown artifact into bounded `## `-level sections. */
162
+ function parseMarkdownSections(markdown: string): ParsedHeadingSection[] {
163
+ const sections: ParsedHeadingSection[] = [];
164
+ let current: ParsedHeadingSection | undefined;
165
+ for (const rawLine of markdown.split(/\r?\n/)) {
166
+ const heading = /^##\s+(.*)$/.exec(rawLine);
167
+ if (heading) {
168
+ current = { title: heading[1].trim(), lines: [] };
169
+ sections.push(current);
170
+ } else if (current) {
171
+ current.lines.push(rawLine);
172
+ }
173
+ }
174
+ return sections.slice(0, 24);
175
+ }
176
+
177
+ /** Extract bounded list items from a section body (normalizes nested bullets). */
178
+ function sectionListItems(section: ParsedHeadingSection | undefined, maxItems: number): string[] {
179
+ if (!section) return [];
180
+ const items: string[] = [];
181
+ for (const line of section.lines) {
182
+ const bullet = /^\s*(?:[-*+]|\d+[.)])\s+(.*)$/.exec(line);
183
+ const text = boundText(bullet ? bullet[1] : line.trim().length > 0 ? line : undefined, MAX_ITEM_CHARS);
184
+ if (text) items.push(text);
185
+ if (items.length >= maxItems) break;
186
+ }
187
+ return items;
188
+ }
189
+
190
+ function findSection(sections: ParsedHeadingSection[], needles: readonly string[]): ParsedHeadingSection | undefined {
191
+ const normalized = needles.map(needle => needle.toLowerCase());
192
+ return sections.find(section => normalized.some(needle => section.title.toLowerCase().includes(needle)));
193
+ }
194
+
195
+ function findSectionExact(
196
+ sections: ParsedHeadingSection[],
197
+ titles: readonly string[],
198
+ ): ParsedHeadingSection | undefined {
199
+ const normalized = new Set(titles.map(title => title.toLowerCase()));
200
+ return sections.find(section => normalized.has(section.title.toLowerCase()));
201
+ }
202
+
203
+ /**
204
+ * Verify that every component of `child` below `root` is a real directory and
205
+ * not a symlink. Checking only the leaf is insufficient: a symlinked ancestor
206
+ * can relocate the effective root outside the expected tree while the leaf's
207
+ * relative-realpath check still succeeds.
208
+ */
209
+ async function isSymlinkFreeDescent(root: string, child: string): Promise<boolean> {
210
+ const relative = path.relative(root, child);
211
+ if (relative.startsWith("..") || path.isAbsolute(relative)) return false;
212
+ let current = root;
213
+ for (const segment of relative.split(path.sep).filter(Boolean)) {
214
+ current = path.join(current, segment);
215
+ const stat = await fs.lstat(current);
216
+ if (stat.isSymbolicLink()) return false;
217
+ }
218
+ return true;
219
+ }
220
+
221
+ async function resolveRalplanArtifactPath(
222
+ runDir: string,
223
+ recordedPath: string,
224
+ plansRoot: string,
225
+ ): Promise<string | undefined> {
226
+ const candidate = path.isAbsolute(recordedPath) ? recordedPath : path.resolve(runDir, recordedPath);
227
+ try {
228
+ // Anchor beneath a canonical, symlink-free session plans root so that no
229
+ // ancestor of the run directory can redirect recovery out of the tree.
230
+ const plansReal = await fs.realpath(plansRoot);
231
+ if (!(await isSymlinkFreeDescent(plansReal, path.resolve(runDir)))) return undefined;
232
+ const runStat = await fs.lstat(runDir);
233
+ if (!runStat.isDirectory() || runStat.isSymbolicLink()) return undefined;
234
+ if (!(await isSymlinkFreeDescent(path.resolve(runDir), candidate))) return undefined;
235
+ const [runReal, artifactReal] = await Promise.all([fs.realpath(runDir), fs.realpath(candidate)]);
236
+ const relative = path.relative(runReal, artifactReal);
237
+ if (relative.startsWith("..") || path.isAbsolute(relative)) return undefined;
238
+ const stat = await fs.lstat(artifactReal);
239
+ return stat.isFile() ? artifactReal : undefined;
240
+ } catch {
241
+ return undefined;
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Extract the bounded objective from a Ralplan final plan artifact. The
247
+ * objective is the first non-empty prose line of the document (before the
248
+ * first `##` heading), which is the durable plan statement of intent.
249
+ */
250
+ function objectiveFromMarkdown(markdown: string): string | undefined {
251
+ for (const rawLine of markdown.split(/\r?\n/)) {
252
+ const line = rawLine.trim();
253
+ if (line.startsWith("#")) continue;
254
+ if (line.length === 0) continue;
255
+ return boundText(line, MAX_OBJECTIVE_CHARS);
256
+ }
257
+ return undefined;
258
+ }
259
+
260
+ export interface RalplanFinalProjectionInput {
261
+ cwd: string;
262
+ sessionId: string;
263
+ runId: string;
264
+ }
265
+
266
+ interface RalplanProjectionInput extends RalplanFinalProjectionInput {
267
+ lastReviewVerdict?: string;
268
+ lastReviewVerdictLane?: string;
269
+ }
270
+
271
+ interface RalplanProjectionRow {
272
+ stage?: unknown;
273
+ stage_n?: unknown;
274
+ path?: unknown;
275
+ sha256?: unknown;
276
+ event?: unknown;
277
+ planning_stuck?: unknown;
278
+ }
279
+
280
+ /**
281
+ * Build a recovery projection from the newest complete Ralplan `final` stage
282
+ * row of a run. Returns undefined when no parseable final artifact exists —
283
+ * compaction then degrades to the thin projection instead of guessing.
284
+ */
285
+ async function projectRalplanRunInternal(
286
+ input: RalplanProjectionInput,
287
+ finalOnly: boolean,
288
+ ): Promise<WorkflowRecoveryProjection | undefined> {
289
+ const plansRoot = sessionPlansDir(input.cwd, input.sessionId);
290
+ const runDir = path.join(plansRoot, "ralplan", input.runId);
291
+ const rows: RalplanProjectionRow[] = [];
292
+ try {
293
+ const text = await fs.readFile(path.join(runDir, "index.jsonl"), "utf8");
294
+ for (const line of text.split(/\r?\n/).map(value => value.trim())) {
295
+ if (line.length === 0) continue;
296
+ const parsed = JSON.parse(line) as unknown;
297
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return undefined;
298
+ rows.push(parsed as RalplanProjectionRow);
299
+ }
300
+ } catch {
301
+ return undefined;
302
+ }
303
+ const finalRow = [...rows].reverse().find(row => row.stage === "final");
304
+ const planRow = [...rows].reverse().find(row => row.stage === "revision" || row.stage === "planner");
305
+ const artifactRow = finalOnly ? finalRow : (finalRow ?? planRow);
306
+ if (
307
+ typeof artifactRow?.path !== "string" ||
308
+ artifactRow.path.trim().length === 0 ||
309
+ typeof artifactRow.sha256 !== "string"
310
+ )
311
+ return undefined;
312
+ const artifactPath = await resolveRalplanArtifactPath(runDir, artifactRow.path, plansRoot);
313
+ if (!artifactPath) return undefined;
314
+ const artifact = await readArtifactWithDigest(artifactPath);
315
+ if (!artifact) return undefined;
316
+ const markdown = artifact.text;
317
+ const objective = objectiveFromMarkdown(markdown);
318
+ if (!objective) return undefined;
319
+ const sections = parseMarkdownSections(markdown);
320
+ const primaryAcceptance = sectionListItems(
321
+ findSection(sections, ["acceptance criteria", "verification", "test plan"]),
322
+ MAX_CRITERIA_ITEMS,
323
+ );
324
+ const acceptance =
325
+ primaryAcceptance.length > 0
326
+ ? primaryAcceptance
327
+ : sectionListItems(findSectionExact(sections, ["acceptance"]), MAX_CRITERIA_ITEMS);
328
+ const nonGoals = sectionListItems(
329
+ findSectionExact(sections, ["non-goals", "non goals", "non-goal", "out of scope"]),
330
+ MAX_SCOPE_ITEMS,
331
+ );
332
+ const unresolved = sectionListItems(
333
+ findSection(sections, ["intent reconciliation", "open confirmation", "unresolved"]),
334
+ MAX_UNRESOLVED_ITEMS,
335
+ );
336
+ const scope: WorkflowRecoveryScopeItem[] = sectionListItems(
337
+ findSectionExact(sections, ["scope", "accepted scope"]),
338
+ MAX_SCOPE_ITEMS,
339
+ ).map(text => ({ kind: "accepted" as const, text }));
340
+ for (const text of nonGoals) scope.push({ kind: "non_goal" as const, text });
341
+ const sha256 = artifact.sha256;
342
+ const recorded = artifactRow.sha256.startsWith("sha256:") ? artifactRow.sha256 : `sha256:${artifactRow.sha256}`;
343
+ if (!/^sha256:[0-9a-f]{64}$/.test(recorded) || recorded !== sha256) return undefined;
344
+ const stage = typeof artifactRow.stage === "string" ? artifactRow.stage : "unknown";
345
+ const latestStage = typeof rows.at(-1)?.stage === "string" ? rows.at(-1)?.stage : undefined;
346
+ const planningStuck = rows.some(row => row.event === "planning_stuck" && row.planning_stuck === true);
347
+ let nextAction: WorkflowRecoveryProjection["nextAction"];
348
+ if (planningStuck) {
349
+ nextAction = { actionClass: "awaiting-approval", detail: "planning-stuck" };
350
+ } else if (stage === "final") {
351
+ nextAction = { actionClass: "awaiting-approval" };
352
+ } else if (latestStage === "critic") {
353
+ nextAction =
354
+ input.lastReviewVerdictLane === "critic" && input.lastReviewVerdict === "OKAY"
355
+ ? { actionClass: "reconcile-intent" }
356
+ : { actionClass: "revise-plan" };
357
+ } else if (latestStage === "planner" || latestStage === "revision") {
358
+ // The manifest requires planner -> intent before Architect/Critic consensus.
359
+ // Resuming straight to review here would spend the expensive consensus lanes
360
+ // on a draft whose material intent was never reconciled.
361
+ nextAction = { actionClass: "reconcile-intent", detail: `${latestStage}-without-intent-receipt` };
362
+ } else {
363
+ nextAction = { actionClass: "run-plan-review" };
364
+ }
365
+ const projection: Omit<WorkflowRecoveryProjection, "zeroProgress"> = {
366
+ skill: "ralplan",
367
+ source: stage === "final" ? "ralplan-final" : "ralplan-run",
368
+ objective,
369
+ scope,
370
+ acceptanceCriteria: acceptance,
371
+ unresolved,
372
+ provenance: { planPath: artifactPath, runId: input.runId, stage, sha256 },
373
+ progress: {},
374
+ nextAction,
375
+ };
376
+ return withZeroProgress(projection);
377
+ }
378
+
379
+ export async function projectRalplanFinalRun(
380
+ input: RalplanFinalProjectionInput,
381
+ ): Promise<WorkflowRecoveryProjection | undefined> {
382
+ return await projectRalplanRunInternal(input, true);
383
+ }
384
+
385
+ export async function projectRalplanRun(
386
+ input: RalplanProjectionInput,
387
+ ): Promise<WorkflowRecoveryProjection | undefined> {
388
+ return await projectRalplanRunInternal(input, false);
389
+ }
390
+
391
+ function isSafeRunId(value: unknown): value is string {
392
+ return (
393
+ typeof value === "string" &&
394
+ value.trim().length > 0 &&
395
+ value === value.trim() &&
396
+ path.basename(value) === value &&
397
+ value !== "." &&
398
+ value !== ".."
399
+ );
400
+ }
401
+
402
+ /**
403
+ * Project the active Ralplan run recorded in durable mode state. When legacy
404
+ * state has no run id, fall back to complete runs ordered by index freshness,
405
+ * skipping unfinished or malformed candidates instead of letting them shadow
406
+ * the newest usable final contract.
407
+ */
408
+ export async function projectLatestRalplanRun(input: {
409
+ cwd: string;
410
+ sessionId: string;
411
+ }): Promise<WorkflowRecoveryProjection | undefined> {
412
+ const root = path.join(sessionPlansDir(input.cwd, input.sessionId), "ralplan");
413
+ let stateText: string | undefined;
414
+ try {
415
+ stateText = await fs.readFile(modeStatePath(input.cwd, input.sessionId, "ralplan"), "utf8");
416
+ } catch (error) {
417
+ if (!(typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT")) {
418
+ return undefined;
419
+ }
420
+ }
421
+ if (stateText !== undefined) {
422
+ let state: {
423
+ run_id?: unknown;
424
+ last_review_verdict?: unknown;
425
+ last_review_verdict_lane?: unknown;
426
+ };
427
+ try {
428
+ state = JSON.parse(stateText) as typeof state;
429
+ } catch {
430
+ return undefined;
431
+ }
432
+ if (state.run_id !== undefined && !isSafeRunId(state.run_id)) return undefined;
433
+ if (isSafeRunId(state.run_id)) {
434
+ return await projectRalplanRun({
435
+ ...input,
436
+ runId: state.run_id,
437
+ lastReviewVerdict: typeof state.last_review_verdict === "string" ? state.last_review_verdict : undefined,
438
+ lastReviewVerdictLane:
439
+ typeof state.last_review_verdict_lane === "string" ? state.last_review_verdict_lane : undefined,
440
+ });
441
+ }
442
+ }
443
+ try {
444
+ const entries = await fs.readdir(root, { withFileTypes: true });
445
+ const candidates = await Promise.all(
446
+ entries
447
+ .filter(entry => entry.isDirectory() && isSafeRunId(entry.name))
448
+ .map(async entry => ({
449
+ runId: entry.name,
450
+ mtimeMs:
451
+ (await fs.stat(path.join(root, entry.name, "index.jsonl")).catch(() => undefined))?.mtimeMs ?? -1,
452
+ })),
453
+ );
454
+ candidates.sort((left, right) => right.mtimeMs - left.mtimeMs || left.runId.localeCompare(right.runId));
455
+ for (const candidate of candidates) {
456
+ const projection = await projectRalplanRun({ ...input, runId: candidate.runId });
457
+ if (projection) return projection;
458
+ }
459
+ } catch {
460
+ return undefined;
461
+ }
462
+ return undefined;
463
+ }
464
+ /**
465
+ * Build a recovery projection from Ultragoal canonical durable state. The
466
+ * accepted contract is the aggregate objective plus the goal list; progress
467
+ * and next action are derived from `goals.json` status plus the newest ledger
468
+ * receipts, never from conversation memory.
469
+ */
470
+ export async function projectUltragoalRun(input: {
471
+ cwd: string;
472
+ sessionId: string;
473
+ }): Promise<WorkflowRecoveryProjection | undefined> {
474
+ const plan = await readUltragoalPlan(input.cwd, input.sessionId).catch(() => null);
475
+ if (!plan?.goals?.length) return undefined;
476
+ const ledger = await readUltragoalLedger(input.cwd, input.sessionId).catch(() => null);
477
+ if (!ledger) return undefined;
478
+ const paths = getUltragoalPaths(input.cwd, input.sessionId);
479
+ const schedulable = plan.goals.filter(goal => goal.status !== "complete" && goal.status !== "superseded");
480
+ const currentGoal = plan.goals.find(goal => goal.status === "active" || goal.status === "failed") ?? schedulable[0];
481
+ const reviewBlocked = plan.goals.find(goal => goal.status === "review_blocked");
482
+ const lastCheckpoint = [...ledger]
483
+ .reverse()
484
+ .find(event => event.event === "goal_checkpointed" && event.status === "complete");
485
+ // Newest joined cohort generation/sourceHash across all complete checkpoints.
486
+ let latestReviewGeneration: number | undefined;
487
+ let latestCohortSourceHash: string | undefined;
488
+ for (const event of ledger) {
489
+ if (event.event !== "goal_checkpointed" || event.status !== "complete") continue;
490
+ const cohort = readCohortFromLedgerEvent(event);
491
+ if (!cohort) continue;
492
+ if (latestReviewGeneration === undefined || cohort.reviewGeneration >= latestReviewGeneration) {
493
+ latestReviewGeneration = cohort.reviewGeneration;
494
+ latestCohortSourceHash = cohort.sourceHash;
495
+ }
496
+ }
497
+ const outstanding = schedulable.length;
498
+ const scope: WorkflowRecoveryScopeItem[] = [
499
+ {
500
+ kind: "accepted",
501
+ text: boundText(plan.gjcObjective, MAX_OBJECTIVE_CHARS) ?? "Ultragoal aggregate run",
502
+ },
503
+ ];
504
+ for (const goal of plan.goals.slice(0, MAX_SCOPE_ITEMS - 1)) {
505
+ const text = boundText(goal.title || goal.objective, MAX_ITEM_CHARS);
506
+ if (text) scope.push({ kind: "accepted", text: `goal ${goal.id}: ${text}` });
507
+ }
508
+ const acceptance: string[] = [];
509
+ for (const goal of plan.goals.slice(0, MAX_CRITERIA_ITEMS)) {
510
+ // Acceptance = per-goal completion evidence currently recorded durably.
511
+ const evidence = boundText(goal.evidence, MAX_ITEM_CHARS);
512
+ if (goal.status === "complete" && evidence) acceptance.push(`${goal.id} complete: ${evidence}`);
513
+ }
514
+ let nextAction: WorkflowRecoveryProjection["nextAction"] = { actionClass: "unknown" };
515
+ if (reviewBlocked) {
516
+ nextAction = {
517
+ actionClass: "resolve-review-blockers",
518
+ goalId: reviewBlocked.id,
519
+ detail: boundText(reviewBlocked.objective, MAX_ITEM_CHARS),
520
+ };
521
+ } else if (outstanding === 0) {
522
+ nextAction = { actionClass: "final-aggregate-checkpoint" };
523
+ } else if (currentGoal) {
524
+ nextAction = {
525
+ actionClass: currentGoal.status === "active" ? "continue-current-goal" : "start-next-goal",
526
+ goalId: currentGoal.id,
527
+ detail: boundText(currentGoal.objective, MAX_ITEM_CHARS),
528
+ };
529
+ }
530
+ const projection: Omit<WorkflowRecoveryProjection, "zeroProgress"> = {
531
+ skill: "ultragoal",
532
+ source: "ultragoal-plan",
533
+ objective: boundText(plan.gjcObjective, MAX_OBJECTIVE_CHARS) ?? "Ultragoal aggregate run",
534
+ scope,
535
+ acceptanceCriteria: acceptance,
536
+ unresolved: reviewBlocked
537
+ ? [`review blockers open on ${reviewBlocked.id}`]
538
+ : schedulable
539
+ .filter(goal => goal.status === "blocked" || goal.status === "failed")
540
+ .slice(0, MAX_UNRESOLVED_ITEMS)
541
+ .map(goal =>
542
+ `${goal.id} ${goal.status}: ${boundText(goal.evidence ?? "", MAX_ITEM_CHARS) ?? ""}`.trim(),
543
+ ),
544
+ provenance: { planPath: paths.goalsPath },
545
+ currentGoal: currentGoal
546
+ ? {
547
+ goalId: currentGoal.id,
548
+ status: currentGoal.status,
549
+ objective: boundText(currentGoal.objective, MAX_OBJECTIVE_CHARS) ?? "",
550
+ }
551
+ : undefined,
552
+ progress: {
553
+ totalGoals: plan.goals.length,
554
+ completedGoals: plan.goals.filter(goal => goal.status === "complete").length,
555
+ outstandingGoals: outstanding,
556
+ latestReviewGeneration,
557
+ latestCohortSourceHash,
558
+ latestLedgerEventId: lastCheckpoint?.eventId ?? [...ledger].at(-1)?.eventId,
559
+ },
560
+ nextAction,
561
+ };
562
+ return withZeroProgress(projection);
563
+ }
564
+
565
+ /** Attach a fresh zero-progress fingerprint to a built projection. */
566
+ function withZeroProgress(projection: Omit<WorkflowRecoveryProjection, "zeroProgress">): WorkflowRecoveryProjection {
567
+ const fingerprint = hashWorkflowRecoveryProjection(projection as WorkflowRecoveryProjection);
568
+ return {
569
+ ...projection,
570
+ zeroProgress: { fingerprint, unchangedObservations: 0, stalled: false },
571
+ };
572
+ }
573
+
574
+ function readCohortFromLedgerEvent(
575
+ event: UltragoalLedgerLike,
576
+ ): { reviewGeneration: number; sourceHash: string } | undefined {
577
+ const gate = event.qualityGateJson;
578
+ if (!gate || typeof gate !== "object" || Array.isArray(gate)) return undefined;
579
+ const iteration = (gate as { iteration?: unknown }).iteration;
580
+ if (!iteration || typeof iteration !== "object" || Array.isArray(iteration)) return undefined;
581
+ const cohort = (iteration as { reviewCohort?: unknown }).reviewCohort;
582
+ if (!cohort || typeof cohort !== "object" || Array.isArray(cohort)) return undefined;
583
+ const reviewGeneration = (cohort as { reviewGeneration?: unknown }).reviewGeneration;
584
+ const sourceHash = (cohort as { sourceHash?: unknown }).sourceHash;
585
+ if (typeof reviewGeneration !== "number" || typeof sourceHash !== "string") return undefined;
586
+ return { reviewGeneration, sourceHash };
587
+ }
588
+
589
+ interface UltragoalLedgerLike {
590
+ event?: string;
591
+ status?: string;
592
+ eventId?: string;
593
+ qualityGateJson?: unknown;
594
+ }
595
+
596
+ /** Stable digest over the projection's contract-relevant fields. */
597
+ export function hashWorkflowRecoveryProjection(projection: WorkflowRecoveryProjection): string {
598
+ const basis = {
599
+ skill: projection.skill,
600
+ source: projection.source,
601
+ objective: projection.objective,
602
+ scope: projection.scope,
603
+ acceptanceCriteria: projection.acceptanceCriteria,
604
+ unresolved: projection.unresolved,
605
+ provenance: projection.provenance,
606
+ currentGoal: projection.currentGoal,
607
+ progressBasis: {
608
+ totalGoals: projection.progress.totalGoals,
609
+ completedGoals: projection.progress.completedGoals,
610
+ outstandingGoals: projection.progress.outstandingGoals,
611
+ latestCohortSourceHash: projection.progress.latestCohortSourceHash,
612
+ },
613
+ nextAction: projection.nextAction,
614
+ };
615
+ return `sha256:${crypto.createHash("sha256").update(JSON.stringify(basis)).digest("hex")}`;
616
+ }
@@ -31,10 +31,10 @@ export const GJC_SKILL_KEYWORD_DEFINITIONS: readonly SkillKeywordDefinition[] =
31
31
  guidance: "Activate GJC ultragoal durable goal workflow",
32
32
  },
33
33
  {
34
- keyword: "$team",
35
- skill: "team",
34
+ keyword: "$autoresearch",
35
+ skill: "autoresearch",
36
36
  priority: 8,
37
- guidance: "Activate GJC team workflow",
37
+ guidance: "Activate GJC autoresearch research-mission workflow",
38
38
  },
39
39
  ] as const;
40
40
 
@@ -71,7 +71,7 @@ function formatBoolean(name: string, value: boolean | undefined): string {
71
71
  export function buildSanitizedEffectiveSkillConfigContext(input: EffectiveSkillConfigInput | undefined): string {
72
72
  if (!input || input.unavailableReason) {
73
73
  const reason = input?.unavailableReason ? sanitizeConfigValue(input.unavailableReason) : "not available";
74
- return `Sanitized effective skill config unavailable (${reason}); bundled GJC workflow activation remains available for deep-interview, ralplan, ultragoal, team.`;
74
+ return `Sanitized effective skill config unavailable (${reason}); bundled GJC workflow activation remains available for deep-interview, ralplan, ultragoal, autoresearch.`;
75
75
  }
76
76
 
77
77
  const settings = input.skillsSettings ?? {};
@@ -85,7 +85,7 @@ export function buildSanitizedEffectiveSkillConfigContext(input: EffectiveSkillC
85
85
  const userTrusted = resolveSkillScopeTrust(settings, "user");
86
86
 
87
87
  return [
88
- "Sanitized effective skill config for filesystem/custom skill discovery; bundled GJC workflow activation remains available for exactly deep-interview, ralplan, ultragoal, team.",
88
+ "Sanitized effective skill config for filesystem/custom skill discovery; bundled GJC workflow activation remains available for exactly deep-interview, ralplan, ultragoal, autoresearch.",
89
89
  `Skill discovery booleans: ${[
90
90
  formatBoolean("enabled", settings.enabled),
91
91
  formatBoolean("enableSkillCommands", settings.enableSkillCommands),