@moretea-labs/forge 1.7.1 → 1.8.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 (685) hide show
  1. package/AGENTS.md +58 -62
  2. package/CHANGELOG.md +39 -0
  3. package/CLAUDE.md +42 -62
  4. package/README.md +2 -2
  5. package/SKILL.md +0 -1
  6. package/THIRD_PARTY_NOTICES.md +5 -2
  7. package/adapters/browser/index.ts +2 -0
  8. package/adapters/chatgpt/browser-delivery-host.ts +71 -0
  9. package/adapters/chatgpt/browser-delivery-runtime.ts +783 -0
  10. package/adapters/chatgpt/controller-binding-store.ts +55 -0
  11. package/adapters/chatgpt/controller-host.ts +119 -0
  12. package/adapters/chatgpt/controller-round-host.ts +78 -0
  13. package/adapters/chatgpt/controller-round-settlement-store.ts +45 -0
  14. package/adapters/chatgpt/provider-delivery.ts +113 -0
  15. package/adapters/chatgpt/work-conversation-binding-store.ts +192 -0
  16. package/adapters/chatgpt/wsl-bridge-delivery-host.ts +63 -0
  17. package/adapters/computer/desktop-operator-contract.ts +7 -0
  18. package/adapters/computer/desktop-operator-discovery.ts +201 -0
  19. package/adapters/computer/desktop-operator-negotiation.ts +174 -0
  20. package/adapters/computer/desktop-operator-provider.ts +312 -0
  21. package/adapters/computer/index.ts +1 -0
  22. package/adapters/computer/interaction-target-authority.ts +944 -0
  23. package/adapters/controller-process/binding-store.ts +34 -0
  24. package/adapters/controller-process/controller-host.ts +26 -0
  25. package/adapters/mcp/access-mode.ts +132 -0
  26. package/adapters/mcp/audit.ts +32 -0
  27. package/adapters/mcp/auth.ts +407 -0
  28. package/adapters/mcp/controller-round-compatibility.ts +184 -0
  29. package/adapters/mcp/frozen-client-semantic-compatibility.ts +292 -0
  30. package/adapters/mcp/instructions.ts +36 -0
  31. package/adapters/mcp/legacy-context.ts +18 -0
  32. package/adapters/mcp/multi-repository.ts +437 -0
  33. package/adapters/mcp/oauth.ts +391 -0
  34. package/adapters/mcp/paths.ts +140 -0
  35. package/adapters/mcp/policy.ts +232 -0
  36. package/adapters/mcp/proxy-env.ts +62 -0
  37. package/adapters/mcp/readiness-projection.ts +24 -0
  38. package/adapters/mcp/redaction.ts +21 -0
  39. package/adapters/mcp/repo.ts +24 -0
  40. package/adapters/mcp/runtime-gateway/artifact-adapter.ts +52 -0
  41. package/adapters/mcp/runtime-gateway/context-adapter.ts +829 -0
  42. package/adapters/mcp/runtime-gateway/controller-authority-adapter.ts +191 -0
  43. package/adapters/mcp/runtime-gateway/engineering-preconditions.ts +147 -0
  44. package/adapters/mcp/runtime-gateway/execution-control-adapter.ts +28 -0
  45. package/adapters/mcp/runtime-gateway/execution-tools.ts +207 -0
  46. package/adapters/mcp/runtime-gateway/filesystem-adapter.ts +38 -0
  47. package/adapters/mcp/runtime-gateway/gateway-execution-adapter.ts +411 -0
  48. package/adapters/mcp/runtime-gateway/legacy-ios-tool-adapter.ts +105 -0
  49. package/adapters/mcp/runtime-gateway/maintenance-adapter.ts +44 -0
  50. package/adapters/mcp/runtime-gateway/model-adapter.ts +63 -0
  51. package/adapters/mcp/runtime-gateway/persisted-check-process.ts +3 -0
  52. package/adapters/mcp/runtime-gateway/plugin-adapter.ts +308 -0
  53. package/adapters/mcp/runtime-gateway/process-tools.ts +289 -0
  54. package/adapters/mcp/runtime-gateway/protected-computer-adapter.ts +76 -0
  55. package/adapters/mcp/runtime-gateway/recovery-adapter.ts +268 -0
  56. package/adapters/mcp/runtime-gateway/recovery-client-adapter.ts +70 -0
  57. package/adapters/mcp/runtime-gateway/recovery-tool-contract.ts +99 -0
  58. package/adapters/mcp/runtime-gateway/repository-compat-adapter.ts +110 -0
  59. package/adapters/mcp/runtime-gateway/result-adapter.ts +108 -0
  60. package/adapters/mcp/runtime-gateway/router.ts +728 -0
  61. package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1251 -0
  62. package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +634 -0
  63. package/adapters/mcp/runtime-gateway/runtime-tool-shared.ts +27 -0
  64. package/adapters/mcp/runtime-gateway/runtime-tools.ts +68 -0
  65. package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +186 -0
  66. package/adapters/mcp/runtime-gateway/shared-adapter.ts +25 -0
  67. package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +870 -0
  68. package/adapters/mcp/runtime-gateway/work-adapter.ts +3186 -0
  69. package/adapters/mcp/runtime-gateway/work-compat-adapter.ts +502 -0
  70. package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +1 -0
  71. package/adapters/mcp/server.ts +1048 -0
  72. package/adapters/mcp/setup.ts +791 -0
  73. package/adapters/mcp/tool-capability-cache.ts +87 -0
  74. package/adapters/mcp/tool-mapping/access-tools.ts +387 -0
  75. package/adapters/mcp/tool-mapping/legacy-tool-service.ts +5265 -0
  76. package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +150 -0
  77. package/adapters/mcp/tool-mapping/repository-tools.ts +1691 -0
  78. package/adapters/mcp/tool-mapping/tools.ts +9 -0
  79. package/adapters/mcp/tool-mapping/umbrella-tools.ts +117 -0
  80. package/adapters/mcp/toolset-names.ts +74 -0
  81. package/adapters/mcp/toolset-selection.ts +41 -0
  82. package/adapters/mcp/toolset.ts +336 -0
  83. package/adapters/mcp/transports/http.ts +1456 -0
  84. package/adapters/mcp/transports/session-registry.ts +513 -0
  85. package/adapters/mcp/transports/stdio.ts +8 -0
  86. package/adapters/mcp/tunnels/openai-secure-tunnel.ts +139 -0
  87. package/adapters/mcp/types.ts +41 -0
  88. package/assets/AGENTS.md +2 -16
  89. package/assets/CLAUDE.md +2 -16
  90. package/assets/hooks/AGENTS.md +3 -14
  91. package/assets/hooks/CLAUDE.md +3 -14
  92. package/assets/hooks/hook-input.sh +9 -2
  93. package/assets/hooks/lib/workflow-state.sh +28 -10
  94. package/assets/hooks/prompt-guard.sh +63 -37
  95. package/assets/hooks/security-sentinel.sh +2 -1
  96. package/assets/hooks/session-start-context.sh +7 -5
  97. package/assets/hooks/stop-orchestrator.sh +13 -3
  98. package/assets/initializer-question-pack.v4.json +11 -7
  99. package/assets/partials/05-workflow.partial.md +18 -73
  100. package/assets/partials/07-footer.partial.md +1 -1
  101. package/assets/partials-agents/02-operating-mode.partial.md +1 -2
  102. package/assets/partials-agents/04-task-protocol.partial.md +14 -59
  103. package/assets/partials-agents/06-quality-safety.partial.md +11 -16
  104. package/assets/partials-agents/08-deep-docs.partial.md +1 -2
  105. package/assets/plan-map.json +11 -11
  106. package/assets/plugin-registry.v1.json +9 -3
  107. package/assets/plugin-trust-policy.v1.json +6 -0
  108. package/assets/recovery/ForgeRecovery.ps1 +100 -0
  109. package/assets/recovery/forge-wsl-rescue +271 -0
  110. package/assets/reference-configs/development-protocol.md +11 -15
  111. package/assets/reference-configs/document-generation.md +1 -1
  112. package/assets/reference-configs/global-working-rules.md +9 -1
  113. package/assets/reference-configs/harness-overview.md +22 -112
  114. package/assets/reference-configs/hook-operations.md +1 -1
  115. package/assets/reference-configs/sprint-contracts.md +2 -2
  116. package/assets/skill-version.json +2 -2
  117. package/assets/templates/helpers/architecture-event.ts +3 -34
  118. package/assets/templates/helpers/architecture-queue.sh +61 -10
  119. package/assets/templates/helpers/archive-workflow.sh +90 -180
  120. package/assets/templates/helpers/capability-config.ts +8 -11
  121. package/assets/templates/helpers/capability-resolver.ts +37 -19
  122. package/assets/templates/helpers/capture-plan.sh +2 -2
  123. package/assets/templates/helpers/check-architecture-sync.sh +1 -9
  124. package/assets/templates/helpers/check-task-sync.sh +82 -11
  125. package/assets/templates/helpers/check-task-workflow.sh +1 -3
  126. package/assets/templates/helpers/context-contract-sync.sh +1 -12
  127. package/assets/templates/helpers/contract-worktree.sh +26 -24
  128. package/assets/templates/helpers/ensure-task-workflow.sh +6 -8
  129. package/assets/templates/helpers/inspect-project-state.ts +1 -1
  130. package/assets/templates/helpers/migrate-project-template.sh +1 -1
  131. package/assets/templates/helpers/migrate-workflow-docs.ts +53 -239
  132. package/assets/templates/helpers/new-plan.sh +2 -2
  133. package/assets/templates/helpers/plan-to-todo.sh +3 -31
  134. package/assets/templates/helpers/sprint-backlog.sh +1 -1
  135. package/assets/templates/helpers/switch-plan.sh +1 -1
  136. package/assets/templates/helpers/workflow-contract.ts +1 -0
  137. package/assets/workflow-contract.v1.json +63 -171
  138. package/assets/workflows/xiaohongshu/xiaohongshu.collect-metrics.v1.draft.json +118 -0
  139. package/assets/workflows/xiaohongshu/xiaohongshu.publish-image-note.v1.draft.json +316 -0
  140. package/assets/workflows/xiaohongshu/xiaohongshu.publish-long-text.v1.draft.json +324 -0
  141. package/docs/architecture/CURRENT.md +148 -13
  142. package/docs/architecture/EVOLUTION.md +13 -0
  143. package/docs/architecture/decisions/20260801-controller-home-sqlite-state.md +2 -1
  144. package/docs/architecture/decisions/20260831-independent-windows-wsl-host-rescue.md +59 -0
  145. package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +120 -0
  146. package/docs/architecture/decisions/20260904-repository-runtime-data-boundary.md +42 -0
  147. package/docs/architecture/decisions/20260907-assistant-learning-loop.md +35 -0
  148. package/docs/architecture/decisions/20260907-controller-round-transition-policy.md +296 -0
  149. package/docs/architecture/decisions/20260907-maintenance-diagnosis-bounded-read.md +39 -0
  150. package/docs/architecture/domains/verification.md +7 -0
  151. package/docs/architecture/domains/workflow-engine.md +1 -1
  152. package/docs/architecture/index.md +3 -3
  153. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +19 -8
  154. package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +124 -0
  155. package/docs/forge-chatgpt-mcp-setup.md +5 -3
  156. package/docs/forge-plugin-management.md +20 -4
  157. package/docs/forge-runtime-self-healing-loop.md +2 -0
  158. package/docs/operations/benchmark-cross-version-freeze.md +38 -0
  159. package/docs/operations/controller-browser-plugin.md +3 -1
  160. package/docs/operations/homebrew.md +2 -2
  161. package/docs/operations/platform-support.md +16 -8
  162. package/docs/operations/platform-support.zh-CN.md +2 -2
  163. package/docs/operations/standalone-disaster-recovery.md +26 -1
  164. package/docs/operations/uu-remote-rescue.md +41 -0
  165. package/docs/operations/windows-wsl-host-rescue.md +133 -0
  166. package/docs/public-usage-guide.md +1 -1
  167. package/docs/public-usage-guide.zh-CN.md +1 -1
  168. package/docs/tutorials/01-install-and-start.md +1 -1
  169. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  170. package/docs/tutorials/02-connect-chatgpt.md +4 -4
  171. package/docs/tutorials/02-connect-chatgpt.zh-CN.md +4 -4
  172. package/docs/wiki/Implementation.md +1 -1
  173. package/package.json +23 -14
  174. package/packages/kernel/controller/api/index.ts +2 -0
  175. package/packages/kernel/controller/application/controller-round-service.ts +29 -0
  176. package/packages/kernel/controller/application/controller-service.ts +30 -0
  177. package/packages/kernel/controller/domain/controller-round-transition-policy.ts +308 -0
  178. package/packages/kernel/controller/domain/controller-round.ts +107 -0
  179. package/packages/kernel/controller/domain/execution-quality.ts +133 -0
  180. package/packages/kernel/controller/domain/types.ts +56 -0
  181. package/packages/kernel/controller/index.ts +1 -0
  182. package/packages/kernel/controller/infrastructure/controller-binding-store.ts +84 -0
  183. package/packages/kernel/controller/infrastructure/controller-round-store.ts +1384 -0
  184. package/packages/kernel/controller/infrastructure/controller-session-store.ts +578 -0
  185. package/packages/kernel/controller/ports/controller-host.ts +16 -0
  186. package/packages/kernel/goal/api/index.ts +1 -0
  187. package/packages/kernel/goal/domain/types.ts +40 -0
  188. package/packages/kernel/goal/index.ts +1 -0
  189. package/packages/kernel/identity/api/index.ts +5 -0
  190. package/packages/kernel/identity/application/identity-service.ts +67 -0
  191. package/packages/kernel/identity/domain/scope.ts +63 -0
  192. package/packages/kernel/identity/domain/semantic-sync.ts +81 -0
  193. package/packages/kernel/identity/domain/types.ts +44 -0
  194. package/packages/kernel/identity/domain/workspace.ts +56 -0
  195. package/packages/kernel/identity/index.ts +1 -0
  196. package/packages/kernel/identity/infrastructure/identity-store.ts +75 -0
  197. package/packages/kernel/memory/api/index.ts +5 -0
  198. package/packages/kernel/memory/application/experience-service.ts +118 -0
  199. package/packages/kernel/memory/application/outcome-service.ts +77 -0
  200. package/packages/kernel/memory/domain/experience.ts +103 -0
  201. package/packages/kernel/memory/domain/operational-prior.ts +92 -0
  202. package/packages/kernel/memory/index.ts +1 -0
  203. package/packages/kernel/progression/api/index.ts +2 -0
  204. package/packages/kernel/progression/application/projector.ts +211 -0
  205. package/packages/kernel/progression/domain/types.ts +116 -0
  206. package/packages/kernel/progression/index.ts +1 -0
  207. package/packages/kernel/scheduler/api/index.ts +6 -0
  208. package/packages/kernel/scheduler/application/continuation-service.ts +212 -0
  209. package/packages/kernel/scheduler/application/eligibility.ts +252 -0
  210. package/packages/kernel/scheduler/application/schedule-service.ts +24 -0
  211. package/packages/kernel/scheduler/application/settlement.ts +197 -0
  212. package/packages/kernel/scheduler/domain/continuation.ts +38 -0
  213. package/packages/kernel/scheduler/domain/schedule.ts +149 -0
  214. package/packages/kernel/scheduler/index.ts +1 -0
  215. package/packages/kernel/scheduler/infrastructure/continuation-dispatch-store.ts +38 -0
  216. package/packages/kernel/scheduler/infrastructure/schedule-store.ts +785 -0
  217. package/packages/kernel/work/api/index.ts +15 -0
  218. package/packages/kernel/work/application/work-service.ts +148 -0
  219. package/packages/kernel/work/domain/admission-policy.ts +34 -0
  220. package/packages/kernel/work/domain/check-receipt.ts +43 -0
  221. package/packages/kernel/work/domain/context-closure-receipt.ts +32 -0
  222. package/packages/kernel/work/domain/engineering-contracts.ts +157 -0
  223. package/packages/kernel/work/domain/engineering-design.ts +281 -0
  224. package/packages/kernel/work/domain/engineering-profile.ts +242 -0
  225. package/packages/kernel/work/domain/execution-concurrency.ts +254 -0
  226. package/packages/kernel/work/domain/execution-snapshot.ts +33 -0
  227. package/packages/kernel/work/domain/implementation-review.ts +510 -0
  228. package/packages/kernel/work/domain/project-engineering-contract.ts +202 -0
  229. package/packages/kernel/work/domain/repository-completion-receipt.ts +73 -0
  230. package/packages/kernel/work/domain/state-machine.ts +259 -0
  231. package/packages/kernel/work/domain/types.ts +528 -0
  232. package/packages/kernel/work/index.ts +1 -0
  233. package/packages/kernel/work/infrastructure/work-contract-store.ts +1876 -0
  234. package/packages/kernel/work/ports/work-contract-store.ts +18 -0
  235. package/packages/plugin-runtime/browser/index.ts +3 -0
  236. package/packages/plugin-runtime/browser/provider-registry.ts +110 -0
  237. package/packages/plugin-runtime/browser/runtime-contract.ts +150 -0
  238. package/packages/plugin-runtime/browser/session-authority.ts +61 -0
  239. package/packages/plugin-runtime/computer/index.ts +6 -0
  240. package/packages/plugin-runtime/computer/provider-error.ts +26 -0
  241. package/packages/plugin-runtime/computer/provider-registration.ts +52 -0
  242. package/packages/plugin-runtime/computer/provider-registry.ts +63 -0
  243. package/packages/plugin-runtime/computer/provider.ts +19 -0
  244. package/packages/plugin-runtime/computer/target-authority.ts +190 -0
  245. package/packages/plugin-runtime/computer/target-persistence.ts +54 -0
  246. package/packages/plugin-runtime/external/index.ts +1 -0
  247. package/packages/plugin-runtime/external/unix-jsonl-transport.ts +457 -0
  248. package/packages/plugin-runtime/index.ts +3 -0
  249. package/packages/protocols/browser/index.ts +1 -0
  250. package/packages/protocols/browser/session.ts +98 -0
  251. package/packages/protocols/computer/contract.ts +199 -0
  252. package/packages/protocols/computer/index.ts +1 -0
  253. package/packages/protocols/failure/contract.ts +158 -0
  254. package/packages/protocols/failure/index.ts +1 -0
  255. package/packages/protocols/handoff/index.ts +1 -0
  256. package/packages/protocols/handoff/status.ts +22 -0
  257. package/packages/protocols/mcp/execution-context.ts +11 -0
  258. package/packages/protocols/mcp/index.ts +2 -0
  259. package/packages/protocols/mcp/tool-contract.ts +42 -0
  260. package/packages/workflow-runtime/api/index.ts +2 -0
  261. package/packages/workflow-runtime/application/interpreter.ts +342 -0
  262. package/packages/workflow-runtime/domain/workflow-asset.ts +223 -0
  263. package/packages/workflow-runtime/index.ts +1 -0
  264. package/scripts/assemble-template.ts +1 -1
  265. package/scripts/benchmark-computer-hotpath.ts +255 -0
  266. package/scripts/benchmark-controller-round2.ts +54 -2
  267. package/scripts/benchmark-ios-agent-device.ts +3 -6
  268. package/scripts/benchmark-mcp-transport.ts +3 -4
  269. package/scripts/benchmark-operational-memory-activation.ts +235 -0
  270. package/scripts/benchmark-operational-shadow.ts +28 -0
  271. package/scripts/bootstrap-runtime-maintenance-recovery.sh +4 -1
  272. package/scripts/canary-chatgpt-provider-delivery.ts +97 -0
  273. package/scripts/canary-controller-semantic-wait.ts +195 -0
  274. package/scripts/canary-global-hook.sh +2 -2
  275. package/scripts/check-mcp-compatibility.ts +173 -7
  276. package/scripts/check-platform-support.mjs +28 -2
  277. package/scripts/check-public-docs.sh +1 -1
  278. package/scripts/check-release-readiness.sh +1 -1
  279. package/scripts/check-repository-hygiene.mjs +92 -0
  280. package/scripts/check-runtime-architecture.mjs +1206 -95
  281. package/scripts/check-source-duplication.mjs +9 -1
  282. package/scripts/check-tarball-install-smoke.sh +98 -15
  283. package/scripts/controller-ngrok-rotation.sh +5 -4
  284. package/scripts/create-project-dirs.sh +10 -74
  285. package/scripts/forge-local-recovery-helper.mjs +204 -0
  286. package/scripts/forge-uu-remote-rescue-helper.d.mts +31 -0
  287. package/scripts/forge-uu-remote-rescue-helper.mjs +409 -0
  288. package/scripts/forge-windows-host-recovery-helper.mjs +136 -0
  289. package/scripts/hook-shim.sh +2 -2
  290. package/scripts/init-project.sh +12 -70
  291. package/scripts/install-desktop-operator-registration.ts +4 -2
  292. package/scripts/install-independent-windows-wsl-recovery.ts +125 -0
  293. package/scripts/install-standalone-recovery.ts +122 -10
  294. package/scripts/install-uu-remote-rescue-registration.ts +74 -0
  295. package/scripts/install-windows-host-recovery-registration.ts +22 -0
  296. package/scripts/lib/controller-home-env.sh +6 -3
  297. package/scripts/lib/project-init-lib.sh +39 -582
  298. package/scripts/migrate-chatgpt-automation-schedules.ts +2 -2
  299. package/scripts/migrate-windows-wsl-controller-home.ts +306 -0
  300. package/scripts/open-source-audit-allowlist.txt +12 -1
  301. package/scripts/portability-bindings.mjs +70 -0
  302. package/scripts/run-bounded-command.mjs +55 -0
  303. package/scripts/run-bun-test-file.ts +22 -6
  304. package/scripts/run-governed-gate.ts +4 -1
  305. package/scripts/smoke-mcp-http-runtime.ts +1 -1
  306. package/scripts/smoke-runtime-recovery.ts +34 -1
  307. package/scripts/smoke-schedule-engine.ts +1 -1
  308. package/scripts/stage-runtime-release.ts +6 -1
  309. package/scripts/test-governance.ts +51 -2
  310. package/src/cli/agent-jobs/job-manager.ts +10 -1
  311. package/src/cli/chatgpt-browser/bind-server.ts +2 -1
  312. package/src/cli/chatgpt-browser/binding.ts +20 -10
  313. package/src/cli/chatgpt-browser/bridge-extension.ts +75 -0
  314. package/src/cli/chatgpt-browser/bridge-provider.ts +376 -11
  315. package/src/cli/chatgpt-browser/engine.ts +10 -11
  316. package/src/cli/chatgpt-browser/native-provider.ts +24 -83
  317. package/src/cli/chatgpt-browser/oracle-provider.ts +5 -3
  318. package/src/cli/chatgpt-browser/types.ts +1 -0
  319. package/src/cli/commands/bootstrap-control.ts +334 -0
  320. package/src/cli/commands/brain-assistant.ts +38 -0
  321. package/src/cli/commands/brain.ts +193 -40
  322. package/src/cli/commands/chatgpt.ts +127 -0
  323. package/src/cli/commands/computer.ts +437 -0
  324. package/src/cli/commands/doctor.ts +18 -1
  325. package/src/cli/commands/init-hook.ts +71 -61
  326. package/src/cli/commands/init.ts +3 -1
  327. package/src/cli/commands/mcp.ts +16 -10
  328. package/src/cli/commands/plugin.ts +38 -12
  329. package/src/cli/commands/recovery.ts +493 -80
  330. package/src/cli/commands/runtime.ts +9 -4
  331. package/src/cli/commands/setup-profile.ts +89 -39
  332. package/src/cli/commands/status.ts +2 -1
  333. package/src/cli/controller/check-runner.ts +487 -98
  334. package/src/cli/controller/context/types.ts +7 -1
  335. package/src/cli/controller/context-pack.ts +23 -3
  336. package/src/cli/controller/issue-store.ts +1 -1
  337. package/src/cli/controller/systemd-user.ts +110 -0
  338. package/src/cli/controller/types.ts +10 -70
  339. package/src/cli/controller/worklog.ts +67 -13
  340. package/src/cli/editing/edit-operation-contract.ts +92 -0
  341. package/src/cli/editing/edit-session.ts +289 -13
  342. package/src/cli/github/plugin.ts +17 -12
  343. package/src/cli/hook/route-registry.ts +3 -5
  344. package/src/cli/hook/runtime.ts +49 -3
  345. package/src/cli/index.ts +3 -0
  346. package/src/cli/local-bridge/connector-freshness.ts +2 -2
  347. package/src/cli/local-bridge/console-automations.ts +2 -2
  348. package/src/cli/local-bridge/facade-api.ts +41 -14
  349. package/src/cli/local-bridge/job-store.ts +1 -1
  350. package/src/cli/local-bridge/server.ts +24 -27
  351. package/src/cli/mcp/access-mode.ts +2 -132
  352. package/src/cli/mcp/access-tools.ts +2 -387
  353. package/src/cli/mcp/audit.ts +2 -32
  354. package/src/cli/mcp/auth.ts +2 -404
  355. package/src/cli/mcp/instructions.ts +2 -34
  356. package/src/cli/mcp/legacy-context.ts +2 -16
  357. package/src/cli/mcp/legacy-tool-service.ts +2 -5308
  358. package/src/cli/mcp/multi-repository.ts +2 -436
  359. package/src/cli/mcp/oauth.ts +2 -375
  360. package/src/cli/mcp/openai-secure-tunnel.ts +2 -0
  361. package/src/cli/mcp/paths.ts +2 -140
  362. package/src/cli/mcp/policy.ts +2 -232
  363. package/src/cli/mcp/post-finalize-work-attribution.ts +2 -150
  364. package/src/cli/mcp/proxy-env.ts +2 -62
  365. package/src/cli/mcp/readiness-projection.ts +2 -24
  366. package/src/cli/mcp/redaction.ts +2 -21
  367. package/src/cli/mcp/repo.ts +2 -24
  368. package/src/cli/mcp/repository-tools.ts +2 -1585
  369. package/src/cli/mcp/server.ts +2 -951
  370. package/src/cli/mcp/setup.ts +2 -737
  371. package/src/cli/mcp/tool-capability-cache.ts +2 -87
  372. package/src/cli/mcp/tool-contract.ts +2 -14
  373. package/src/cli/mcp/tools.ts +2 -9
  374. package/src/cli/mcp/toolset-names.ts +2 -70
  375. package/src/cli/mcp/toolset-selection.ts +2 -41
  376. package/src/cli/mcp/toolset.ts +2 -336
  377. package/src/cli/mcp/transports/AGENTS.md +1 -10
  378. package/src/cli/mcp/transports/CLAUDE.md +1 -10
  379. package/src/cli/mcp/transports/http.ts +2 -1414
  380. package/src/cli/mcp/transports/session-registry.ts +2 -490
  381. package/src/cli/mcp/transports/stdio.ts +2 -8
  382. package/src/cli/mcp/types.ts +2 -41
  383. package/src/cli/mcp/umbrella-tools.ts +2 -117
  384. package/src/cli/repositories/command-executor.ts +64 -18
  385. package/src/cli/repositories/command-scope.ts +117 -1
  386. package/src/cli/repositories/controller-home.ts +119 -1
  387. package/src/cli/repositories/identity.ts +13 -0
  388. package/src/cli/repositories/registry.ts +68 -49
  389. package/src/cli/repositories/repository-snapshot.ts +53 -7
  390. package/src/cli/repositories/runtime-storage.ts +27 -2
  391. package/src/cli/repositories/safe-patch.ts +34 -38
  392. package/src/cli/repositories/selected-path-actions.ts +22 -2
  393. package/src/cli/repositories/structured-git.ts +105 -25
  394. package/src/cli/runtime/helper-runner.ts +7 -0
  395. package/src/cli/runtime-invocation.ts +8 -1
  396. package/src/cli/tools/codegraph.ts +44 -25
  397. package/src/core/adoption/manifest-templates.ts +1 -1
  398. package/src/core/adoption/plan.ts +3 -26
  399. package/src/core/adoption/workflow-contract-plan.ts +17 -9
  400. package/src/effects/process-runner.ts +8 -14
  401. package/src/runtime/context/assistant-context.ts +154 -0
  402. package/src/runtime/context/assistant-work-context.ts +147 -0
  403. package/src/runtime/context/codegraph-cache-boundary.ts +255 -0
  404. package/src/runtime/context/codegraph-read-provider.ts +55 -22
  405. package/src/runtime/context/codegraph-sidecar.cjs +8 -1
  406. package/src/runtime/context/context-closure.ts +268 -0
  407. package/src/runtime/context/context-plane.ts +211 -0
  408. package/src/runtime/context/project-engineering-contract.ts +60 -0
  409. package/src/runtime/control-plane/bootstrap/capability-provider.ts +74 -0
  410. package/src/runtime/control-plane/bootstrap/control-api.ts +57 -0
  411. package/src/runtime/control-plane/bootstrap/index.ts +5 -0
  412. package/src/runtime/control-plane/bootstrap/state-machine.ts +141 -0
  413. package/src/runtime/control-plane/bootstrap/store.ts +21 -0
  414. package/src/runtime/control-plane/bootstrap/types.ts +87 -0
  415. package/src/runtime/control-plane/cleanup-artifact-retention.ts +328 -0
  416. package/src/runtime/control-plane/codegraph-cache-retention.ts +164 -0
  417. package/src/runtime/control-plane/concurrency/work-execution-concurrency.ts +320 -0
  418. package/src/runtime/control-plane/execution/content-equivalent-commit-authority.ts +169 -0
  419. package/src/runtime/control-plane/execution/controller-authority-recovery.ts +350 -0
  420. package/src/runtime/control-plane/execution/direct-canonical-work-reconciliation.ts +85 -0
  421. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +417 -100
  422. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +103 -15
  423. package/src/runtime/control-plane/execution/implementation-review-content.ts +95 -0
  424. package/src/runtime/control-plane/execution/repository-work-attribution.ts +2 -2
  425. package/src/runtime/control-plane/execution/retained-work-resume.ts +336 -6
  426. package/src/runtime/control-plane/execution/validation.ts +28 -3
  427. package/src/runtime/control-plane/execution/work-authority-reconciler.ts +116 -0
  428. package/src/runtime/control-plane/execution/work-completion-authority.ts +44 -2
  429. package/src/runtime/control-plane/execution/work-evidence-policy.ts +34 -3
  430. package/src/runtime/control-plane/execution/work-execution-support.ts +42 -22
  431. package/src/runtime/control-plane/execution/work-finalization-service.ts +1103 -166
  432. package/src/runtime/control-plane/execution/work-handle-authority.ts +267 -16
  433. package/src/runtime/control-plane/execution/work-handle-store.ts +77 -0
  434. package/src/runtime/control-plane/execution/work-head-settlement.ts +75 -0
  435. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +145 -23
  436. package/src/runtime/control-plane/execution/work-operation-service.ts +49 -13
  437. package/src/runtime/control-plane/execution/work-preparation-service.ts +31 -15
  438. package/src/runtime/control-plane/execution/work-remote-delivery.ts +1 -1
  439. package/src/runtime/control-plane/execution/work-task-receipt.ts +20 -1
  440. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +407 -76
  441. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +88 -18
  442. package/src/runtime/control-plane/execution/work-verification-context.ts +1 -1
  443. package/src/runtime/control-plane/execution/work-verification-service.ts +258 -4
  444. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +200 -15
  445. package/src/runtime/control-plane/facade/capability-registry.ts +21 -10
  446. package/src/runtime/control-plane/facade/controller-round-relay.ts +3 -1118
  447. package/src/runtime/control-plane/facade/controller-session-store.ts +2 -488
  448. package/src/runtime/control-plane/facade/goal-workloop-access.ts +6 -3
  449. package/src/runtime/control-plane/facade/goal-workloop.ts +972 -137
  450. package/src/runtime/control-plane/facade/handoff-inbox-application.ts +158 -0
  451. package/src/runtime/control-plane/facade/handoff-inbox-store.ts +10 -0
  452. package/src/runtime/control-plane/facade/index.ts +4 -2
  453. package/src/runtime/control-plane/facade/operation-digest.ts +22 -58
  454. package/src/runtime/control-plane/facade/plan-contract-store.ts +1004 -38
  455. package/src/runtime/control-plane/facade/repository-work-admission.ts +19 -1
  456. package/src/runtime/control-plane/facade/requirement-authority.ts +218 -0
  457. package/src/runtime/control-plane/facade/requirement-board.ts +6 -1
  458. package/src/runtime/control-plane/facade/rh-work-operation-contract.ts +48 -0
  459. package/src/runtime/control-plane/facade/semantic-admission.ts +8 -16
  460. package/src/runtime/control-plane/facade/suggested-actions.ts +4 -1
  461. package/src/runtime/control-plane/facade/types.ts +189 -532
  462. package/src/runtime/control-plane/facade/work-admission-policy.ts +84 -35
  463. package/src/runtime/control-plane/facade/work-contract-store.ts +2 -1043
  464. package/src/runtime/control-plane/facade/work-implementation-review.ts +2 -0
  465. package/src/runtime/control-plane/facade/work-state-machine.ts +2 -250
  466. package/src/runtime/control-plane/generated-cache-retention.ts +239 -18
  467. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +12 -1
  468. package/src/runtime/control-plane/global-scheduler/maintenance.ts +76 -13
  469. package/src/runtime/control-plane/global-scheduler/periodic-cleanup-entry.ts +55 -0
  470. package/src/runtime/control-plane/global-scheduler/periodic-cleanup-process.ts +98 -0
  471. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +1 -1
  472. package/src/runtime/control-plane/global-scheduler/scheduler-worker-entry.ts +10 -0
  473. package/src/runtime/control-plane/global-scheduler/scheduler.ts +173 -81
  474. package/src/runtime/control-plane/global-scheduler/worker-launch.ts +18 -6
  475. package/src/runtime/control-plane/launcher/chatgpt-round-continuation.ts +397 -0
  476. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +2 -177
  477. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +301 -560
  478. package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +20 -4
  479. package/src/runtime/control-plane/launcher/thin-launcher.ts +50 -4
  480. package/src/runtime/control-plane/lifecycle-retention-metrics.ts +60 -0
  481. package/src/runtime/control-plane/persistence/experience-store.ts +169 -0
  482. package/src/runtime/control-plane/persistence/operational-prior-store.ts +348 -0
  483. package/src/runtime/control-plane/persistence/requirement-store.ts +30 -22
  484. package/src/runtime/control-plane/persistence/sqlite-store.ts +545 -76
  485. package/src/runtime/control-plane/persistence/workflow-content-store.ts +98 -0
  486. package/src/runtime/control-plane/persistence/workflow-registry-store.ts +186 -0
  487. package/src/runtime/control-plane/persistence/workflow-run-store.ts +123 -0
  488. package/src/runtime/control-plane/release-retention.ts +56 -16
  489. package/src/runtime/control-plane/repository-namespace-retention.ts +208 -0
  490. package/src/runtime/control-plane/routing/route-policy.ts +19 -0
  491. package/src/runtime/control-plane/runtime-cleanup.ts +574 -37
  492. package/src/runtime/control-plane/runtime-generation.ts +6 -0
  493. package/src/runtime/control-plane/workspace/semantic-sync-service.ts +465 -0
  494. package/src/runtime/control-plane/workspace/workspace-store.ts +80 -0
  495. package/src/runtime/diagnostics/incident-repair.ts +406 -0
  496. package/src/runtime/diagnostics/mcp-timing.ts +125 -6
  497. package/src/runtime/diagnostics/performance.ts +6 -14
  498. package/src/runtime/diagnostics/read-only-tool.ts +1 -1
  499. package/src/runtime/evidence/artifact-store.ts +65 -2
  500. package/src/runtime/evidence/operational-shadow.ts +38 -0
  501. package/src/runtime/evidence/process-check-receipt.ts +5 -43
  502. package/src/runtime/evidence/sensitive-output.ts +4 -4
  503. package/src/runtime/execution/jobs/receipt-recovery.ts +1 -1
  504. package/src/runtime/execution/jobs/store.ts +121 -2
  505. package/src/runtime/execution/managed-workspace.ts +55 -5
  506. package/src/runtime/execution/process-runtime/canary.ts +19 -5
  507. package/src/runtime/execution/process-runtime/check-classification.ts +48 -0
  508. package/src/runtime/execution/process-runtime/check-facade.ts +23 -9
  509. package/src/runtime/execution/process-runtime/check-receipt.ts +1 -1
  510. package/src/runtime/execution/process-runtime/check-result-retention.ts +132 -0
  511. package/src/runtime/execution/process-runtime/check-result.ts +73 -2
  512. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +42 -3
  513. package/src/runtime/execution/process-runtime/check-storage.ts +187 -0
  514. package/src/runtime/execution/process-runtime/command-facade.ts +38 -87
  515. package/src/runtime/execution/process-runtime/index.ts +1 -0
  516. package/src/runtime/execution/process-runtime/interactive-admission.ts +2 -1
  517. package/src/runtime/execution/process-runtime/lightweight-managed.ts +32 -1
  518. package/src/runtime/execution/process-runtime/persisted-check.ts +128 -21
  519. package/src/runtime/execution/process-runtime/resource-claims.ts +60 -3
  520. package/src/runtime/execution/process-runtime/runtime.ts +132 -15
  521. package/src/runtime/execution/process-runtime/types.ts +22 -0
  522. package/src/runtime/execution/workers/executor.ts +16 -13
  523. package/src/runtime/execution/workers/worker-entry.ts +1 -1
  524. package/src/runtime/gateway/mcp/execution-tools.ts +2 -207
  525. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +2 -61
  526. package/src/runtime/gateway/mcp/persisted-check-process.ts +2 -3
  527. package/src/runtime/gateway/mcp/process-tools.ts +2 -289
  528. package/src/runtime/gateway/mcp/router.ts +2 -1125
  529. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +2 -565
  530. package/src/runtime/gateway/mcp/runtime-tools.ts +2 -6651
  531. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +2 -1
  532. package/src/runtime/maintenance/cleanup.ts +38 -24
  533. package/src/runtime/platform/browser-product-discovery.ts +174 -0
  534. package/src/runtime/platform/computer-platform.ts +14 -0
  535. package/src/runtime/platform/executable-discovery.ts +54 -0
  536. package/src/runtime/platform/fetch-http-server.ts +90 -0
  537. package/src/runtime/platform/service-manager.ts +138 -0
  538. package/src/runtime/platform/windows-host-discovery.ts +118 -0
  539. package/src/runtime/platform/windows-recovery-deployment.ts +67 -0
  540. package/src/runtime/plugins/action-application.ts +135 -0
  541. package/src/runtime/plugins/app-store-connect-adapter.ts +9 -21
  542. package/src/runtime/plugins/browser-adapter.ts +140 -100
  543. package/src/runtime/plugins/browser-automation-service.ts +15 -36
  544. package/src/runtime/plugins/browser-macos-bridge.ts +69 -21
  545. package/src/runtime/plugins/browser-manifest-surface.ts +30 -6
  546. package/src/runtime/plugins/browser-native-session-service.ts +1 -1
  547. package/src/runtime/plugins/browser-provider-registry.ts +2 -109
  548. package/src/runtime/plugins/browser-registration.ts +16 -0
  549. package/src/runtime/plugins/browser-runtime-contract.ts +2 -154
  550. package/src/runtime/plugins/browser-runtime.ts +41 -9
  551. package/src/runtime/plugins/browser-session-authority.ts +101 -291
  552. package/src/runtime/plugins/browser-session-legacy-migration.ts +203 -0
  553. package/src/runtime/plugins/browser-session-store.ts +229 -36
  554. package/src/runtime/plugins/browser-session-types.ts +2 -98
  555. package/src/runtime/plugins/capability-authorization-grants.ts +17 -0
  556. package/src/runtime/plugins/catalog-trust.ts +82 -0
  557. package/src/runtime/plugins/computer-registration.ts +839 -0
  558. package/src/runtime/plugins/config-store.ts +119 -0
  559. package/src/runtime/plugins/desktop-operator-external-policy.ts +932 -0
  560. package/src/runtime/plugins/desktop-operator-registration.ts +111 -8
  561. package/src/runtime/plugins/errors.ts +8 -1
  562. package/src/runtime/plugins/external-adapter.ts +45 -322
  563. package/src/runtime/plugins/external-provider-policy.ts +48 -0
  564. package/src/runtime/plugins/external-registration.ts +9 -1
  565. package/src/runtime/plugins/external-unix-socket-probe.cjs +9 -3
  566. package/src/runtime/plugins/external-unix-socket.ts +64 -193
  567. package/src/runtime/plugins/first-party-external-registration.ts +163 -0
  568. package/src/runtime/plugins/first-party-registry.ts +3 -1
  569. package/src/runtime/plugins/github-adapter.ts +1 -2
  570. package/src/runtime/plugins/gmail-adapter.ts +6 -6
  571. package/src/runtime/plugins/google-calendar-adapter.ts +6 -6
  572. package/src/runtime/plugins/google-shared.ts +31 -46
  573. package/src/runtime/plugins/google-tasks-adapter.ts +6 -6
  574. package/src/runtime/plugins/ios-adapter.ts +1 -2
  575. package/src/runtime/plugins/local-system-adapter.ts +180 -0
  576. package/src/runtime/plugins/macos-capability-broker.ts +15 -105
  577. package/src/runtime/plugins/resend-adapter.ts +15 -24
  578. package/src/runtime/plugins/store.ts +493 -51
  579. package/src/runtime/plugins/types.ts +8 -1
  580. package/src/runtime/plugins/uu-remote-rescue-registration.ts +155 -0
  581. package/src/runtime/plugins/windows-host-recovery-install.ts +68 -0
  582. package/src/runtime/plugins/windows-host-recovery-registration.ts +85 -0
  583. package/src/runtime/plugins/xiaohongshu-publish.ts +86 -430
  584. package/src/runtime/projections/controller-context.ts +12 -6
  585. package/src/runtime/projections/git-status-sampler.ts +73 -22
  586. package/src/runtime/projections/materialized-view.ts +14 -8
  587. package/src/runtime/recovery/capability-action-application.ts +228 -0
  588. package/src/runtime/recovery/index.ts +3 -0
  589. package/src/runtime/recovery/lifecycle-inventory.ts +338 -0
  590. package/src/runtime/recovery/local-jobs-repair.ts +14 -14
  591. package/src/runtime/recovery/maintenance-executor.ts +358 -76
  592. package/src/runtime/recovery/quarantine-retention.ts +256 -0
  593. package/src/runtime/release/identity.ts +6 -0
  594. package/src/runtime/resources/leases/store.ts +1 -1
  595. package/src/runtime/root/active-execution-power-assertion.ts +139 -0
  596. package/src/runtime/root/assistant-learning-loop.ts +45 -0
  597. package/src/runtime/root/browser-session-composition.ts +59 -0
  598. package/src/runtime/root/computer-composition.ts +191 -0
  599. package/src/runtime/root/computer-target-composition.ts +16 -0
  600. package/src/runtime/root/computer-target-persistence.ts +39 -0
  601. package/src/runtime/root/controller-round-composition.ts +107 -0
  602. package/src/runtime/root/controller-services.ts +2 -0
  603. package/src/runtime/root/entry.ts +4 -3
  604. package/src/runtime/root/gateway-adapter.ts +3 -3
  605. package/src/runtime/root/local-bridge.ts +5 -2
  606. package/src/runtime/root/mcp-transport.ts +5 -5
  607. package/src/runtime/root/package-connector-service.ts +108 -56
  608. package/src/runtime/root/package-runtime-release.ts +86 -18
  609. package/src/runtime/root/package-runtime-service.ts +63 -86
  610. package/src/runtime/root/release-execution-canary.ts +71 -0
  611. package/src/runtime/root/release-loader.ts +68 -0
  612. package/src/runtime/root/release-manifest.ts +106 -2
  613. package/src/runtime/root/release-materialize.ts +103 -4
  614. package/src/runtime/root/release-store.ts +31 -2
  615. package/src/runtime/root/runtime.ts +240 -25
  616. package/src/runtime/root/scheduled-controller-composition.ts +51 -0
  617. package/src/runtime/root/scheduler.ts +12 -4
  618. package/src/runtime/root/service-runner.ts +1 -1
  619. package/src/runtime/root/service.ts +41 -16
  620. package/src/runtime/root/status.ts +1 -0
  621. package/src/runtime/root/types.ts +24 -1
  622. package/src/runtime/shared/local-bridge-surface.ts +1 -1
  623. package/src/runtime/shared/process-environment.ts +19 -4
  624. package/src/runtime/shared/storage-capacity.ts +68 -0
  625. package/src/runtime/standalone-recovery/controller-home-migration.ts +1116 -0
  626. package/src/runtime/standalone-recovery/core.ts +984 -230
  627. package/src/runtime/standalone-recovery/entry.ts +182 -51
  628. package/src/runtime/standalone-recovery/independent-host-rescue.ts +172 -0
  629. package/src/runtime/standalone-recovery/installer.ts +212 -104
  630. package/src/runtime/standalone-recovery/mcp-server.ts +243 -0
  631. package/src/runtime/standalone-recovery/mutation-identity-contract.ts +21 -0
  632. package/src/runtime/standalone-recovery/operation-lock.ts +160 -0
  633. package/src/runtime/standalone-recovery/performance.ts +135 -0
  634. package/src/runtime/standalone-recovery/release.ts +5 -0
  635. package/src/runtime/watchdog/workflow-watchdog.ts +1 -1
  636. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +1 -1
  637. package/src/runtime/workflow/schedules/engine.ts +191 -343
  638. package/src/runtime/workflow/schedules/pending-activation.ts +1 -1
  639. package/src/runtime/workflow/schedules/settlement.ts +2 -191
  640. package/src/runtime/workflow/schedules/store.ts +2 -607
  641. package/src/runtime/workflow/schedules/types.ts +2 -127
  642. package/src/runtime/workflow/schedules/work-continuation.ts +180 -15
  643. package/src/runtime/workflows/first-party/xiaohongshu.ts +122 -0
  644. package/src/runtime/workflows/runtime.ts +486 -0
  645. package/src/testing/test-governance.ts +90 -29
  646. package/assets/templates/helpers/archive-architecture-request.sh +0 -244
  647. package/docs/architecture/history/README.md +0 -7
  648. package/docs/architecture/history/chatgpt-handoff-facade.md +0 -397
  649. package/docs/architecture/history/global-hook-runtime.md +0 -166
  650. package/docs/architecture/history/ios-semantic-automation-provider-v2.md +0 -170
  651. package/docs/architecture/requests/archive/2026/20260528-154740-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
  652. package/docs/architecture/requests/archive/2026/20260528-154816-assets-hooks-assets-hooks-hook-input-sh.md +0 -59
  653. package/docs/architecture/requests/archive/2026/20260528-161851-ai-harness-workflow-contract-json-ai-harness-workflow-contract-json.md +0 -59
  654. package/docs/architecture/requests/archive/2026/20260528-161853-scripts-check-task-workflow-sh-scripts-check-task-workflow-sh.md +0 -59
  655. package/docs/architecture/requests/archive/2026/20260528-161911-assets-workflow-contract-v1-json-assets-workflow-contract-v1-json.md +0 -59
  656. package/docs/architecture/requests/archive/2026/20260528-222416-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
  657. package/docs/architecture/requests/archive/2026/20260528-222444-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
  658. package/docs/architecture/requests/archive/2026/20260528-222457-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
  659. package/docs/architecture/requests/archive/2026/20260528-222648-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
  660. package/docs/architecture/requests/archive/2026/20260528-222705-ai-hooks-ai-hooks-worktree-guard-sh.md +0 -59
  661. package/docs/architecture/requests/archive/2026/20260528-222726-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
  662. package/docs/architecture/requests/archive/2026/20260528-222727-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
  663. package/docs/architecture/requests/archive/2026/20260528-224736-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
  664. package/docs/architecture/requests/archive/2026/20260528-233259-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
  665. package/docs/architecture/requests/archive/2026/20260528-233335-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
  666. package/docs/architecture/requests/archive/2026/20260528-233456-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
  667. package/docs/architecture/requests/archive/2026/20260528-233457-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
  668. package/docs/architecture/requests/archive/2026/20260528-233637-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
  669. package/docs/architecture/requests/archive/2026/20260528-233638-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
  670. package/docs/architecture/requests/archive/2026/20260529-000322-root-package-json.md +0 -59
  671. package/docs/architecture/requests/archive/2026/20260529-004238-ai-hooks-ai-hooks-lib-workflow-state-sh.md +0 -59
  672. package/docs/architecture/requests/archive/2026/20260529-020654-ai-harness-policy-json-ai-harness-policy-json.md +0 -63
  673. package/docs/architecture/requests/archive/2026/20260529-094236-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -60
  674. package/docs/architecture/requests/archive/2026/20260529-094323-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -60
  675. package/docs/architecture/requests/archive/2026/20260529-094331-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -60
  676. package/docs/architecture/requests/archive/2026/20260529-094339-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -60
  677. package/docs/architecture/requests/archive/2026/20260529-094430-assets-hooks-assets-hooks-session-start-context-sh.md +0 -60
  678. package/docs/architecture/requests/archive/2026/20260529-094446-ai-hooks-ai-hooks-session-start-context-sh.md +0 -60
  679. package/docs/architecture/requests/archive/2026/20260613-002040-runtime-harness-hook-adapters.md +0 -68
  680. package/docs/architecture/requests/archive/2026/root.md +0 -68
  681. package/docs/architecture/requests/archive/2026/runtime-harness-hook-adapters.md +0 -76
  682. package/docs/architecture/requests/archive/2026/verification-evals-checks.md +0 -68
  683. package/docs/architecture/requests/archive/2026/workflow-engine-contract-assets.md +0 -69
  684. package/scripts/archive-architecture-request.sh +0 -4
  685. package/scripts/run-skill-evals.ts +0 -1022
@@ -0,0 +1,1384 @@
1
+ import { createHash, randomUUID } from 'crypto';
2
+ import { withControllerLock } from '../../../../src/cli/repositories/locks';
3
+ import { readRequirement } from '../../../../src/runtime/control-plane/persistence/requirement-store';
4
+ import {
5
+ listControlPlaneRecords,
6
+ readControlPlaneRecord,
7
+ readControlPlaneRecordWithinTransaction,
8
+ withControlPlaneTransaction,
9
+ writeControlPlaneRecord,
10
+ writeControlPlaneRecordWithinTransaction,
11
+ type ControlPlaneRecord,
12
+ } from '../../../../src/runtime/control-plane/persistence/sqlite-store';
13
+ import { workHasActiveExecution } from '../../../../src/runtime/execution/work-activity';
14
+ import { controllerSessionBlocksRecovery, getControllerSession } from './controller-session-store';
15
+ import { getHandoffItem, listHandoffItems } from '../../../../src/runtime/control-plane/facade/handoff-inbox-store';
16
+ import { getWorkContract, readWorkContractStore, isTerminalWorkContractStatus, type WorkContract } from '../../work/api/index';
17
+ import { isTerminalHandoffStatus } from '../../../protocols/handoff/index';
18
+ import type { ControllerSession, ControllerType } from '../domain/types';
19
+ import { deriveClosedRoundQualitySignals, type AssistantContextSnapshot, type AssistantContextUsage, type ClosedRoundObservation, type ExecutionQualityAdjustmentResult, type ExecutionQualityDecision, type ExecutionQualitySignal } from '../domain/execution-quality';
20
+ import {
21
+ CONTROLLER_ROUND_DISPOSITIONS,
22
+ CONTROLLER_RELAY_ABANDONED_RELEASE_ERROR,
23
+ type ControllerRoundDisposition,
24
+ type ControllerRoundRelayIdentity,
25
+ type ControllerRoundRelayRecord,
26
+ type ControllerRoundRelayStatus,
27
+ } from '../domain/controller-round';
28
+ import {
29
+ controllerRoundBlockerClass,
30
+ controllerRoundProviderEffectId,
31
+ decideControllerRoundTransition,
32
+ type ControllerRoundTransitionDecision,
33
+ type ControllerRoundTransitionEvent,
34
+ } from '../domain/controller-round-transition-policy';
35
+
36
+ export interface ControllerRoundRelayStoreOptions {
37
+ controllerHome: string;
38
+ repoId: string;
39
+ now?: () => string;
40
+ /** Trusted composition supplies advisory data; Kernel does not read project files. */
41
+ prepareAssistantContext?: (workId: string) => string | undefined;
42
+ }
43
+
44
+ export interface SubmitControllerRoundDispositionInput {
45
+ workId: string;
46
+ identity: ControllerRoundRelayIdentity;
47
+ disposition: ControllerRoundDisposition;
48
+ executionQualityDecisions?: ExecutionQualityDecision[];
49
+ executionQualityAdjustmentResults?: Array<Omit<ExecutionQualityAdjustmentResult, 'verifiedAt'>>;
50
+ assistantContextDigest?: string;
51
+ assistantContextUsage?: AssistantContextUsage[];
52
+ relayScopeId?: string;
53
+ requirementId?: string;
54
+ handoffId?: string;
55
+ stateFingerprint?: string;
56
+ reason?: string;
57
+ /** Opaque provider binding owned by a ControllerHost adapter. */
58
+ bindingId?: string;
59
+ maxRounds?: number;
60
+ maxRepeatedState?: number;
61
+ maxFailures?: number;
62
+ }
63
+
64
+ export interface BeginInitialControllerRoundDispatchInput {
65
+ workId: string;
66
+ identity: ControllerRoundRelayIdentity;
67
+ relayScopeId?: string;
68
+ requirementId?: string;
69
+ /** Opaque provider binding owned by a ControllerHost adapter. */
70
+ bindingId?: string;
71
+ maxRounds?: number;
72
+ maxRepeatedState?: number;
73
+ maxFailures?: number;
74
+ /** Required for any later schedule/manual/replan occurrence after a prior semantic state exists. */
75
+ occurrenceId?: string;
76
+ }
77
+
78
+ export interface RecoverControllerRoundRelayAuthorityInput {
79
+ workId: string;
80
+ requestedBy?: string;
81
+ identity: ControllerRoundRelayIdentity;
82
+ }
83
+
84
+ /** Legacy ControllerRound rows predate explicit controllerType and were ChatGPT-only. */
85
+ function relayControllerType(record: Pick<ControllerRoundRelayRecord, 'controllerType'>): ControllerType {
86
+ return record.controllerType ?? 'chatgpt';
87
+ }
88
+
89
+ const NAMESPACE = 'controller_round_relay';
90
+ const SCHEMA_VERSION = 1;
91
+ const DEFAULT_MAX_ROUNDS = 8;
92
+ const DEFAULT_MAX_REPEATED_STATE = 2;
93
+ const DEFAULT_MAX_FAILURES = 3;
94
+ const DEFAULT_UNCLOSED_ROUND_GRACE_MS = 10 * 60_000;
95
+ const MAX_UNCLOSED_ROUND_GRACE_MS = 60 * 60_000;
96
+ const DEFAULT_STALLED_RECOVERY_BACKOFF_MS = 60_000;
97
+ const MAX_STALLED_RECOVERY_BACKOFF_MS = 15 * 60_000;
98
+
99
+
100
+ function transitionDecisionOrThrow(decision: ControllerRoundTransitionDecision): { record: ControllerRoundRelayRecord; action?: string; changed: boolean } {
101
+ if (decision.kind === 'accept') return { record: decision.next, action: decision.action, changed: true };
102
+ if (decision.kind === 'accept_atomic') throw new Error('CONTROLLER_RELAY_ATOMIC_TRANSITION_REQUIRES_TRANSACTION');
103
+ if (decision.kind === 'no_op') return { record: decision.current, changed: false };
104
+ if (decision.kind === 'needs_evidence') throw new Error(decision.code);
105
+ throw new Error(decision.code);
106
+ }
107
+
108
+ function atomicTransitionDecisionOrThrow(decision: ControllerRoundTransitionDecision): Extract<ControllerRoundTransitionDecision, { kind: 'accept_atomic' }> {
109
+ if (decision.kind === 'accept_atomic') return decision;
110
+ if (decision.kind === 'needs_evidence' || decision.kind === 'reject') throw new Error(decision.code);
111
+ throw new Error('CONTROLLER_RELAY_ATOMIC_TRANSITION_REQUIRED');
112
+ }
113
+
114
+ function applyControllerRoundTransition(
115
+ options: ControllerRoundRelayStoreOptions,
116
+ current: ControlPlaneRecord<ControllerRoundRelayRecord> | undefined,
117
+ event: ControllerRoundTransitionEvent,
118
+ ): ControllerRoundRelayRecord {
119
+ const decided = transitionDecisionOrThrow(decideControllerRoundTransition(current?.value, event));
120
+ if (!decided.changed) return decided.record;
121
+ const key = current?.value.originWorkId ?? (event.type === 'occurrence_requested' ? event.originWorkId : undefined);
122
+ if (!key) throw new Error('CONTROLLER_RELAY_TRANSITION_KEY_REQUIRED');
123
+ writeControlPlaneRecord(options.controllerHome, {
124
+ namespace: NAMESPACE, scope: options.repoId, key, schemaVersion: SCHEMA_VERSION, value: decided.record,
125
+ action: decided.action ?? `controller_round_transition_${event.type}`, expectedRevision: current?.revision ?? null,
126
+ });
127
+ return decided.record;
128
+ }
129
+ function nowIso(options: ControllerRoundRelayStoreOptions): string {
130
+ return options.now?.() ?? new Date().toISOString();
131
+ }
132
+
133
+ function newControllerRoundAuthorityId(): string {
134
+ return `cra_${randomUUID().replace(/-/g, '')}`;
135
+ }
136
+
137
+ function bounded(value: string | undefined, max: number): string | undefined {
138
+ const normalized = value?.trim();
139
+ return normalized ? normalized.slice(0, max) : undefined;
140
+ }
141
+
142
+ function normalizeScopeId(value: string): string {
143
+ const normalized = value.trim();
144
+ if (!normalized || /[\r\n]/.test(normalized)) throw new Error('CONTROLLER_RELAY_SCOPE_INVALID');
145
+ return normalized.slice(0, 240);
146
+ }
147
+
148
+ function boundedInteger(value: number | undefined, fallback: number, min: number, max: number): number {
149
+ if (value === undefined || !Number.isFinite(value)) return fallback;
150
+ return Math.max(min, Math.min(Math.trunc(value), max));
151
+ }
152
+
153
+ function relayLock<T>(
154
+ options: ControllerRoundRelayStoreOptions,
155
+ relayScopeId: string,
156
+ actor: string,
157
+ operation: () => T,
158
+ ): T {
159
+ const lockId = createHash('sha256').update(relayScopeId).digest('hex').slice(0, 20);
160
+ return withControllerLock(
161
+ options.controllerHome,
162
+ { scope: 'task', repoId: options.repoId, taskId: `controller-round-relay-${lockId}` },
163
+ actor,
164
+ operation,
165
+ );
166
+ }
167
+
168
+ function readRelayRecord(
169
+ options: ControllerRoundRelayStoreOptions,
170
+ workId: string,
171
+ ): ControlPlaneRecord<ControllerRoundRelayRecord> | undefined {
172
+ return readControlPlaneRecord<ControllerRoundRelayRecord>(options.controllerHome, NAMESPACE, options.repoId, workId);
173
+ }
174
+
175
+ export function getControllerRoundRelay(
176
+ options: ControllerRoundRelayStoreOptions,
177
+ workId: string,
178
+ ): ControllerRoundRelayRecord | undefined {
179
+ return readRelayRecord(options, workId)?.value;
180
+ }
181
+
182
+ /**
183
+ * Resolve an existing Requirement-scoped ControllerRound authority for another
184
+ * Work that is mechanically part of the same durable Requirement/Work graph.
185
+ * This never mints, copies, or rebinds relay state; callers may only project the
186
+ * already-proven opaque authority into the target Work's ControllerSession.
187
+ */
188
+ export function resolveRequirementControllerRoundRelayForWork(
189
+ options: ControllerRoundRelayStoreOptions,
190
+ input: { workId: string; authorityId: string; relayScopeId: string },
191
+ ): ControllerRoundRelayRecord | undefined {
192
+ const workId = input.workId.trim();
193
+ const authorityId = input.authorityId.trim();
194
+ const relayScopeId = input.relayScopeId.trim();
195
+ if (!workId || !authorityId || !relayScopeId.startsWith('requirement:')) return undefined;
196
+ const candidate = latestRelayRecordsByScope(options).find((entry) =>
197
+ entry.relayScopeId === relayScopeId
198
+ && entry.authorityId?.trim() === authorityId
199
+ && Boolean(entry.requirementId)
200
+ && `requirement:${entry.requirementId}` === relayScopeId);
201
+ if (!candidate) return undefined;
202
+ return relevantWork(options, candidate).some((work) => work.workId === workId) ? candidate : undefined;
203
+ }
204
+
205
+ function relayHistory(options: ControllerRoundRelayStoreOptions, relayScopeId: string): ControllerRoundRelayRecord[] {
206
+ return listControlPlaneRecords<ControllerRoundRelayRecord>(options.controllerHome, {
207
+ namespace: NAMESPACE,
208
+ scope: options.repoId,
209
+ limit: 5_000,
210
+ })
211
+ .map((entry) => entry.value)
212
+ .filter((entry) => entry.relayScopeId === relayScopeId && entry.status !== 'handed_off')
213
+ .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
214
+ }
215
+
216
+ function latestRelayRecordsByScope(options: ControllerRoundRelayStoreOptions): ControllerRoundRelayRecord[] {
217
+ const latest = new Map<string, ControllerRoundRelayRecord>();
218
+ for (const entry of listControlPlaneRecords<ControllerRoundRelayRecord>(options.controllerHome, {
219
+ namespace: NAMESPACE,
220
+ scope: options.repoId,
221
+ limit: 5_000,
222
+ }).map((record) => record.value)) {
223
+ if (entry.status === 'handed_off') continue;
224
+ const current = latest.get(entry.relayScopeId);
225
+ if (!current || entry.updatedAt > current.updatedAt) latest.set(entry.relayScopeId, entry);
226
+ }
227
+ return [...latest.values()].sort((left, right) => left.updatedAt.localeCompare(right.updatedAt));
228
+ }
229
+
230
+ function requirementForRelay(options: ControllerRoundRelayStoreOptions, requirementId: string | undefined) {
231
+ return requirementId ? readRequirement({ controllerHome: options.controllerHome }, requirementId)?.value : undefined;
232
+ }
233
+
234
+ function relevantWork(options: ControllerRoundRelayStoreOptions, record: Pick<ControllerRoundRelayRecord, 'relayScopeId' | 'originWorkId' | 'requirementId'>): WorkContract[] {
235
+ const all = readWorkContractStore({ controllerHome: options.controllerHome, repoId: options.repoId }).contracts;
236
+ const linkedWorkIds = new Set([
237
+ record.originWorkId,
238
+ ...relayHistory(options, record.relayScopeId).map((entry) => entry.originWorkId),
239
+ ]);
240
+ if (record.requirementId) {
241
+ for (const work of all) {
242
+ if (work.requirementId === record.requirementId) linkedWorkIds.add(work.workId);
243
+ }
244
+ }
245
+ let expanded = true;
246
+ while (expanded) {
247
+ expanded = false;
248
+ for (const work of all) {
249
+ if (linkedWorkIds.has(work.workId)) {
250
+ if (work.parentWorkId && !linkedWorkIds.has(work.parentWorkId)) {
251
+ linkedWorkIds.add(work.parentWorkId);
252
+ expanded = true;
253
+ }
254
+ continue;
255
+ }
256
+ if (work.parentWorkId && linkedWorkIds.has(work.parentWorkId)) {
257
+ linkedWorkIds.add(work.workId);
258
+ expanded = true;
259
+ }
260
+ }
261
+ }
262
+ return all
263
+ .filter((work) => linkedWorkIds.has(work.workId))
264
+ .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
265
+ }
266
+
267
+ function semanticVerificationFacts(work: WorkContract): Array<{
268
+ checkId: string; verificationInputFingerprint: string; checkDefinitionDigest: string;
269
+ checkEnvironmentFingerprint: string; checkCacheKey: string; outcome: string; status: string;
270
+ }> {
271
+ const unique = new Map<string, {
272
+ checkId: string; verificationInputFingerprint: string; checkDefinitionDigest: string;
273
+ checkEnvironmentFingerprint: string; checkCacheKey: string; outcome: string; status: string;
274
+ }>();
275
+ for (const record of work.checkRefs.slice(-32)) {
276
+ const receipt = record.receipt;
277
+ if (!receipt || !record.verificationInputFingerprint || !receipt.checkDefinitionDigest
278
+ || !receipt.checkEnvironmentFingerprint || !receipt.checkCacheKey
279
+ || !['valid_pass', 'valid_fail'].includes(record.outcome)
280
+ || !['passed', 'failed'].includes(receipt.status)) continue;
281
+ const fact = {
282
+ checkId: record.checkId,
283
+ verificationInputFingerprint: record.verificationInputFingerprint,
284
+ checkDefinitionDigest: receipt.checkDefinitionDigest,
285
+ checkEnvironmentFingerprint: receipt.checkEnvironmentFingerprint,
286
+ checkCacheKey: receipt.checkCacheKey,
287
+ outcome: record.outcome,
288
+ status: receipt.status,
289
+ };
290
+ unique.set(JSON.stringify(fact), fact);
291
+ }
292
+ return [...unique.values()].sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
293
+ }
294
+
295
+ function mechanicalStateFingerprint(
296
+ options: ControllerRoundRelayStoreOptions,
297
+ work: WorkContract,
298
+ requirementId: string | undefined,
299
+ relayScopeId: string,
300
+ ): string {
301
+ const requirement = requirementForRelay(options, requirementId);
302
+ const works = relevantWork(options, { relayScopeId, originWorkId: work.workId, requirementId })
303
+ .map((entry) => ({
304
+ workId: entry.workId,
305
+ parentWorkId: entry.parentWorkId,
306
+ status: entry.status,
307
+ phase: entry.phase,
308
+ dispatchState: entry.dispatchState,
309
+ evidenceState: entry.evidenceState,
310
+ completionOutcome: entry.completionOutcome,
311
+ verificationFacts: semanticVerificationFacts(entry),
312
+ executionConcurrency: entry.executionConcurrency ? {
313
+ status: entry.executionConcurrency.status,
314
+ source: entry.executionConcurrency.source,
315
+ blockerCode: entry.executionConcurrency.blockerCode,
316
+ blockingWorkId: entry.executionConcurrency.blockingWorkId,
317
+ semanticScopeKeys: entry.executionConcurrency.semanticScopeKeys,
318
+ resourceKeys: entry.executionConcurrency.resourceKeys,
319
+ leaseRepoId: entry.executionConcurrency.leaseRepoId,
320
+ resourceIntents: entry.executionConcurrency.resourceIntents,
321
+ wakeTrigger: entry.executionConcurrency.wakeTrigger,
322
+ } : undefined,
323
+ }))
324
+ .sort((left, right) => left.workId.localeCompare(right.workId));
325
+ const linkedWorkIds = new Set(works.map((entry) => entry.workId));
326
+ const handoffs = listHandoffItems({ controllerHome: options.controllerHome, repoId: options.repoId, status: 'active', limit: 100 })
327
+ .filter((handoff) => !handoff.workId || linkedWorkIds.has(handoff.workId))
328
+ .map((handoff) => ({
329
+ id: handoff.id,
330
+ workId: handoff.workId,
331
+ status: handoff.status,
332
+ severity: handoff.severity,
333
+ title: handoff.title,
334
+ reason: handoff.reason,
335
+ creationReason: handoff.creationReason,
336
+ blockingDecision: handoff.blockingDecision,
337
+ }))
338
+ .sort((left, right) => left.id.localeCompare(right.id));
339
+ return createHash('sha256').update(JSON.stringify({
340
+ requirement: requirement ? {
341
+ requirementId: requirement.requirementId,
342
+ state: requirement.state,
343
+ revision: requirement.revision,
344
+ } : undefined,
345
+ works,
346
+ handoffs,
347
+ })).digest('hex');
348
+ }
349
+
350
+ /** Current semantic state identity for one existing ControllerRound lineage. Volatile persistence timestamps are intentionally excluded. */
351
+ export function readControllerRoundSemanticStateFingerprint(
352
+ options: ControllerRoundRelayStoreOptions,
353
+ workId: string,
354
+ ): string | undefined {
355
+ const work = getWorkContract(options, workId);
356
+ const current = readRelayRecord(options, workId)?.value;
357
+ if (!work || !current) return undefined;
358
+ return mechanicalStateFingerprint(options, work, current.requirementId, current.relayScopeId);
359
+ }
360
+
361
+ function resolveRequirementId(
362
+ options: ControllerRoundRelayStoreOptions,
363
+ work: WorkContract,
364
+ requestedRequirementId: string | undefined,
365
+ ): string | undefined {
366
+ const requested = bounded(requestedRequirementId, 160);
367
+ if (work.requirementId && requested && work.requirementId !== requested) {
368
+ throw new Error(`CONTROLLER_RELAY_REQUIREMENT_MISMATCH: Work ${work.workId} belongs to ${work.requirementId}`);
369
+ }
370
+ const requirementId = work.requirementId ?? requested;
371
+ if (requirementId && !requirementForRelay(options, requirementId)) {
372
+ throw new Error(`REQUIREMENT_NOT_FOUND: ${requirementId}`);
373
+ }
374
+ return requirementId;
375
+ }
376
+
377
+ function resolveRelayScope(
378
+ work: WorkContract,
379
+ requirementId: string | undefined,
380
+ requestedScopeId: string | undefined,
381
+ ): string {
382
+ const canonicalRequirementScope = requirementId ? `requirement:${requirementId}` : undefined;
383
+ if (canonicalRequirementScope) {
384
+ if (requestedScopeId && normalizeScopeId(requestedScopeId) !== canonicalRequirementScope) {
385
+ throw new Error(`CONTROLLER_RELAY_SCOPE_MISMATCH: expected ${canonicalRequirementScope}`);
386
+ }
387
+ return canonicalRequirementScope;
388
+ }
389
+ return requestedScopeId ? normalizeScopeId(requestedScopeId) : `goal:${work.workId}`;
390
+ }
391
+
392
+ function assertControllerOwner(
393
+ options: ControllerRoundRelayStoreOptions,
394
+ workId: string,
395
+ identity: ControllerRoundRelayIdentity,
396
+ ): ControllerSession & { claimGeneration: number } {
397
+ const owner = getControllerSession(options, workId);
398
+ if (!owner) throw new Error(`WORK_CONTROLLER_OWNER_REQUIRED: ${workId}`);
399
+ if (owner.controllerType !== identity.controllerType) throw new Error(`CONTROLLER_RELAY_CONTROLLER_TYPE_MISMATCH: ${workId}`);
400
+ if (owner.controllerId !== identity.controllerId) throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workId}`);
401
+ const ownerPrincipal = owner.principalId?.trim() || owner.controllerId;
402
+ if (ownerPrincipal !== identity.principalId.trim()) throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${workId}`);
403
+ if (owner.sessionId !== identity.sessionId.trim()) throw new Error(`WORK_CONTROLLER_SESSION_MISMATCH: ${workId}`);
404
+ const ownerInstanceId = owner.controllerInstanceId?.trim();
405
+ if (ownerInstanceId && ownerInstanceId !== identity.controllerInstanceId.trim()) {
406
+ throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${workId}`);
407
+ }
408
+ if (typeof owner.claimGeneration !== 'number' || owner.claimGeneration < 1) throw new Error(`CONTROLLER_RELAY_CLAIM_GENERATION_REQUIRED: ${workId}`);
409
+ return owner as ControllerSession & { claimGeneration: number };
410
+ }
411
+
412
+ function assertControllerRoundSuccessorLineage(
413
+ options: ControllerRoundRelayStoreOptions,
414
+ predecessor: WorkContract,
415
+ successorWorkId: string,
416
+ ): WorkContract {
417
+ const successor = getWorkContract(options, successorWorkId);
418
+ if (!successor) throw new Error(`CONTROLLER_RELAY_SUCCESSOR_WORK_NOT_FOUND: ${successorWorkId}`);
419
+ if (isTerminalWorkContractStatus(successor.status)) {
420
+ throw new Error(`CONTROLLER_RELAY_SUCCESSOR_WORK_TERMINAL: ${successor.workId}:${successor.status}`);
421
+ }
422
+ if (successor.predecessorWorkId !== predecessor.workId) {
423
+ throw new Error(`CONTROLLER_RELAY_SUCCESSOR_LINEAGE_MISMATCH: ${successor.workId}:predecessor=${successor.predecessorWorkId ?? 'none'}:expected=${predecessor.workId}`);
424
+ }
425
+ if (predecessor.requirementId && successor.requirementId !== predecessor.requirementId) {
426
+ throw new Error(`CONTROLLER_RELAY_SUCCESSOR_REQUIREMENT_MISMATCH: ${successor.workId}`);
427
+ }
428
+ if (predecessor.planId && successor.planId !== predecessor.planId) {
429
+ throw new Error(`CONTROLLER_RELAY_SUCCESSOR_PLAN_MISMATCH: ${successor.workId}`);
430
+ }
431
+ if (successor.workId === predecessor.workId) throw new Error('CONTROLLER_RELAY_SUCCESSOR_IDENTITY_REUSE_FORBIDDEN');
432
+ return successor;
433
+ }
434
+
435
+ /**
436
+ * Bind an already-admitted GoalWorkloop successor to the currently claimed
437
+ * ControllerRound. This stores only a mechanical handoff target: Goal/Plan
438
+ * semantics were decided before this call and are revalidated through Work
439
+ * lineage here. No successor Work is created by ControllerRound.
440
+ */
441
+ export function bindControllerRoundSuccessorWork(
442
+ options: ControllerRoundRelayStoreOptions,
443
+ input: { workId: string; successorWorkId: string; identity: ControllerRoundRelayIdentity; controllerAuthorityId?: string },
444
+ ): ControllerRoundRelayRecord {
445
+ const predecessor = getWorkContract(options, input.workId);
446
+ if (!predecessor) throw new Error(`WORK_NOT_FOUND: ${input.workId}`);
447
+ if (predecessor.status !== 'completed') {
448
+ throw new Error(`CONTROLLER_RELAY_SUCCESSOR_PREDECESSOR_NOT_COMPLETED: ${predecessor.workId}:${predecessor.status}`);
449
+ }
450
+ const initial = readRelayRecord(options, predecessor.workId);
451
+ if (!initial) throw new Error(`CONTROLLER_RELAY_ROUND_NOT_OPEN: ${predecessor.workId}`);
452
+ return relayLock(options, initial.value.relayScopeId, `controller-relay-bind-successor:${input.identity.controllerId}`, () => {
453
+ const current = readRelayRecord(options, predecessor.workId);
454
+ if (!current || current.value.status !== 'claimed') {
455
+ throw new Error(`CONTROLLER_RELAY_SUCCESSOR_BIND_REQUIRES_CLAIMED_ROUND: ${predecessor.workId}:${current?.value.status ?? 'missing'}`);
456
+ }
457
+ const expectedAuthorityId = current.value.authorityId?.trim() || '';
458
+ const requestedAuthorityId = input.controllerAuthorityId?.trim() || '';
459
+ if (expectedAuthorityId) {
460
+ if (!requestedAuthorityId) throw new Error(`CONTROLLER_RELAY_SUCCESSOR_BIND_AUTHORITY_REQUIRED: ${predecessor.workId}`);
461
+ if (requestedAuthorityId !== expectedAuthorityId) throw new Error(`CONTROLLER_RELAY_SUCCESSOR_BIND_AUTHORITY_MISMATCH: ${predecessor.workId}`);
462
+ const principalId = input.identity.principalId.trim();
463
+ if (!principalId || current.value.controllerId !== input.identity.controllerId || current.value.principalId !== principalId) {
464
+ throw new Error(`CONTROLLER_RELAY_SUCCESSOR_BIND_AUTHORITY_MISMATCH: ${predecessor.workId}`);
465
+ }
466
+ if (relayControllerType(current.value) !== input.identity.controllerType || current.value.claimGeneration < 1) {
467
+ throw new Error(`CONTROLLER_RELAY_SUCCESSOR_BIND_AUTHORITY_MISMATCH: ${predecessor.workId}`);
468
+ }
469
+ const liveOwner = getControllerSession(options, predecessor.workId);
470
+ if (liveOwner) {
471
+ const livePrincipal = liveOwner.principalId?.trim() || liveOwner.controllerId;
472
+ if (liveOwner.controllerId !== input.identity.controllerId
473
+ || liveOwner.controllerType !== input.identity.controllerType
474
+ || livePrincipal !== principalId) {
475
+ throw new Error(`CONTROLLER_RELAY_SUCCESSOR_BIND_AUTHORITY_MISMATCH: ${predecessor.workId}`);
476
+ }
477
+ }
478
+ } else {
479
+ // Legacy claimed rounds have no opaque per-round capability. Preserve only
480
+ // their exact live owner epoch; rollover is available after the round has
481
+ // been upgraded by the canonical launcher/recovery path.
482
+ const owner = assertControllerOwner(options, predecessor.workId, input.identity);
483
+ if (current.value.controllerId !== owner.controllerId
484
+ || current.value.principalId !== (owner.principalId?.trim() || owner.controllerId)
485
+ || current.value.claimGeneration !== owner.claimGeneration) {
486
+ throw new Error(`CONTROLLER_RELAY_SUCCESSOR_BIND_AUTHORITY_MISMATCH: ${predecessor.workId}`);
487
+ }
488
+ }
489
+ const successor = assertControllerRoundSuccessorLineage(options, predecessor, input.successorWorkId.trim());
490
+ return applyControllerRoundTransition(options, current, {
491
+ type: 'successor_bound', at: nowIso(options), successorWorkId: successor.workId,
492
+ });
493
+ });
494
+ }
495
+
496
+ function sameAssistantContextSnapshot(
497
+ left: AssistantContextSnapshot | undefined,
498
+ right: AssistantContextSnapshot | null | undefined,
499
+ ): boolean {
500
+ if (right === undefined) return true;
501
+ if (right === null) return left === undefined;
502
+ return JSON.stringify(left) === JSON.stringify(right);
503
+ }
504
+
505
+ function validatedAssistantContextUsage(
506
+ record: ControllerRoundRelayRecord,
507
+ input: SubmitControllerRoundDispositionInput,
508
+ ): AssistantContextUsage[] {
509
+ const usage = input.assistantContextUsage ?? [];
510
+ if (!Array.isArray(usage) || usage.length > 32) throw new Error('CONTROLLER_ASSISTANT_CONTEXT_USAGE_LIMIT');
511
+ const snapshot = record.assistantContextSnapshot;
512
+ const digest = input.assistantContextDigest?.trim();
513
+ if (usage.length > 0 && (!snapshot || !digest)) throw new Error('CONTROLLER_ASSISTANT_CONTEXT_SNAPSHOT_REQUIRED');
514
+ if (digest && (!snapshot || digest !== snapshot.digest)) throw new Error('CONTROLLER_ASSISTANT_CONTEXT_DIGEST_MISMATCH');
515
+ if (!usage.length) return [];
516
+ const expected = new Map(snapshot!.items.map((item) => [`${item.kind}:${item.itemId}`, item]));
517
+ const seen = new Set<string>();
518
+ for (const item of usage) {
519
+ const key = `${item.kind}:${item.itemId}`;
520
+ if (!expected.has(key) || seen.has(key) || !['used', 'rejected'].includes(item.decision)
521
+ || typeof item.reason !== 'string' || !item.reason.trim() || item.reason.length > 1_000) {
522
+ throw new Error('CONTROLLER_ASSISTANT_CONTEXT_USAGE_INVALID');
523
+ }
524
+ seen.add(key);
525
+ }
526
+ if (seen.size !== expected.size) throw new Error('CONTROLLER_ASSISTANT_CONTEXT_USAGE_INCOMPLETE');
527
+ return usage.map((item) => ({ ...item, reason: item.reason.trim() }));
528
+ }
529
+
530
+ function persistClaimedAssistantContextSnapshot(
531
+ options: ControllerRoundRelayStoreOptions,
532
+ record: ControllerRoundRelayRecord,
533
+ requested: AssistantContextSnapshot | null | undefined,
534
+ at: string,
535
+ ): ControllerRoundRelayRecord {
536
+ if (requested === undefined || sameAssistantContextSnapshot(record.assistantContextSnapshot, requested)) return record;
537
+ if (record.assistantContextSnapshot) {
538
+ throw new Error(`CONTROLLER_ASSISTANT_CONTEXT_CLAIM_MISMATCH: ${record.originWorkId}`);
539
+ }
540
+ if (requested === null) return record;
541
+ const current = readRelayRecord(options, record.originWorkId);
542
+ if (!current || current.value.relayScopeId !== record.relayScopeId || current.value.status !== 'claimed'
543
+ || current.value.claimGeneration !== record.claimGeneration) {
544
+ throw new Error(`CONTROLLER_ASSISTANT_CONTEXT_CLAIM_STALE: ${record.originWorkId}`);
545
+ }
546
+ if (current.value.assistantContextSnapshot) {
547
+ if (sameAssistantContextSnapshot(current.value.assistantContextSnapshot, requested)) return current.value;
548
+ throw new Error(`CONTROLLER_ASSISTANT_CONTEXT_CLAIM_MISMATCH: ${record.originWorkId}`);
549
+ }
550
+ const next: ControllerRoundRelayRecord = { ...current.value, assistantContextSnapshot: requested, updatedAt: at };
551
+ writeControlPlaneRecord(options.controllerHome, {
552
+ namespace: NAMESPACE, scope: options.repoId, key: record.originWorkId, schemaVersion: SCHEMA_VERSION, value: next,
553
+ action: 'controller_round_assistant_context_bound', expectedRevision: current.revision,
554
+ });
555
+ return next;
556
+ }
557
+
558
+ function pendingQualitySignals(record: ControllerRoundRelayRecord): ExecutionQualitySignal[] {
559
+ const handled = new Set(record.qualityDecisions?.map(decision => decision.fingerprint) ?? []);
560
+ return deriveClosedRoundQualitySignals(record.observationWindow ?? [], { repeatedStateCount: record.repeatedStateCount,
561
+ maxRepeatedState: record.maxRepeatedState, waiting: record.status === 'waiting' || record.status === 'waiting_for_user',
562
+ roundRef: `${record.relayScopeId}:${record.roundCount}` }).map(signal => ({ ...signal,
563
+ fingerprint: createHash('sha256').update(JSON.stringify([signal.code, [...signal.evidenceRefs].sort()])).digest('hex'),
564
+ })).filter(signal => !handled.has(signal.fingerprint));
565
+ }
566
+
567
+ function closedRoundObservation(work: WorkContract, roundRef: string, stateFingerprint: string, waiting: boolean, assistantContext: AssistantContextSnapshot | undefined, assistantContextUsage: AssistantContextUsage[]): ClosedRoundObservation {
568
+ const coverageGaps: string[] = [];
569
+ const verifications: ClosedRoundObservation['verifications'] = [];
570
+ for (const check of work.checkRefs.slice(-32)) {
571
+ const receipt = check.receipt;
572
+ // HEAD alone does not identify dirty source. Require the exact input fingerprint.
573
+ if (!receipt || !check.verificationInputFingerprint || !receipt.checkDefinitionDigest || !receipt.checkEnvironmentFingerprint) {
574
+ coverageGaps.push('check_identity_incomplete'); continue;
575
+ }
576
+ if (receipt.status !== 'passed' && receipt.status !== 'failed') continue;
577
+ verifications.push({ checkId: `${receipt.checkId}:${receipt.checkDefinitionDigest}`, sourceDigest: check.verificationInputFingerprint,
578
+ environment: receipt.checkEnvironmentFingerprint, outcome: receipt.status, evidenceRef: receipt.receiptId });
579
+ }
580
+ // Generic evidence references without content identity cannot prove absence of new knowledge.
581
+ if (work.evidenceRefs.length) coverageGaps.push('generic_evidence_content_identity_unavailable');
582
+ if (work.checkRefs.length > 32) coverageGaps.push('check_window_truncated');
583
+ if (!assistantContext) coverageGaps.push('assistant_context_snapshot_unavailable');
584
+ else if (assistantContext.items.length > 0 && assistantContextUsage.length === 0) coverageGaps.push('assistant_context_usage_unreported');
585
+ const accepted = work.semanticAcceptanceEvidence ?? [];
586
+ if (accepted.length > 32) coverageGaps.push('accepted_result_window_truncated');
587
+ return { roundRef, workId: work.workId, requirementId: work.requirementId, planId: work.planId,
588
+ designVersion: work.engineeringContext?.sourceIdentity.kind === 'revision' ? work.engineeringContext.sourceIdentity.revision : undefined,
589
+ rootCauses: (work.engineeringContext?.blockerDispositions ?? []).slice(-32).filter(disposition => disposition.classification === 'same_root_cause')
590
+ .map(disposition => ({ dispositionRef: disposition.receiptId, rootCauseId: disposition.blockerId,
591
+ designScope: JSON.stringify([...disposition.semanticScopeKeys].sort()), controllerConfirmed: true })),
592
+ stateFingerprint, waiting, coverageGaps: [...new Set(coverageGaps)], verifications, assistantContext, assistantContextUsage,
593
+ evidenceIdentities: work.checkRefs.slice(-32).flatMap(check => check.receipt ? [check.receipt.resultDigest] : []),
594
+ acceptedResultIdentities: accepted.slice(-32).map(result => createHash('sha256').update(JSON.stringify([result.criterion, [...result.evidenceIds].sort()])).digest('hex')) };
595
+ }
596
+
597
+ export function submitControllerRoundDisposition(
598
+ options: ControllerRoundRelayStoreOptions,
599
+ input: SubmitControllerRoundDispositionInput,
600
+ ): ControllerRoundRelayRecord {
601
+ if (!CONTROLLER_ROUND_DISPOSITIONS.includes(input.disposition)) throw new Error('CONTROLLER_RELAY_DISPOSITION_INVALID');
602
+ const work = getWorkContract(options, input.workId);
603
+ if (!work) throw new Error(`WORK_NOT_FOUND: ${input.workId}`);
604
+ const requirementId = resolveRequirementId(options, work, input.requirementId);
605
+ const relayScopeId = resolveRelayScope(work, requirementId, input.relayScopeId);
606
+ const terminal = isTerminalWorkContractStatus(work.status);
607
+
608
+ return relayLock(options, relayScopeId, `controller-relay-submit:${input.identity.controllerId}`, () => {
609
+ const existing = readRelayRecord(options, work.workId);
610
+ if (!existing) throw new Error(`CONTROLLER_RELAY_ROUND_NOT_OPEN: ${work.workId}`);
611
+ if (existing.value.relayScopeId !== relayScopeId) {
612
+ throw new Error(`CONTROLLER_RELAY_SCOPE_MISMATCH: Work ${work.workId} is already bound to ${existing.value.relayScopeId}`);
613
+ }
614
+ if (existing.value.status !== 'claimed') {
615
+ throw new Error(`CONTROLLER_RELAY_ROUND_NOT_CLAIMED: ${existing.value.status}`);
616
+ }
617
+ const terminalSuccessor = terminal
618
+ && work.status === 'completed'
619
+ && input.disposition === 'continue_immediately'
620
+ && existing.value.successorWorkId
621
+ ? assertControllerRoundSuccessorLineage(options, work, existing.value.successorWorkId)
622
+ : undefined;
623
+ const terminalRoundClosureAllowed = terminal
624
+ && work.status === 'completed'
625
+ && (input.disposition === 'goal_complete' || Boolean(terminalSuccessor));
626
+ if (terminal && !terminalRoundClosureAllowed) {
627
+ throw new Error(`CONTROLLER_RELAY_WORK_TERMINAL: ${work.status}`);
628
+ }
629
+
630
+ const liveOwner = getControllerSession(options, work.workId);
631
+ const authority = terminal
632
+ ? (() => {
633
+ // The physical Work may be completed while its old live lease still exists.
634
+ // goal_complete is semantic round closure, not a terminal Work reclaim: use
635
+ // the authenticated controller/principal plus the already-claimed relay as
636
+ // authority and allow MCP session / Runtime instance rotation. If a live
637
+ // owner remains, it must still agree on controller type and principal.
638
+ const principalId = input.identity.principalId.trim();
639
+ const controllerInstanceId = input.identity.controllerInstanceId.trim();
640
+ const sessionId = input.identity.sessionId.trim();
641
+ if (!principalId || !controllerInstanceId || !sessionId) throw new Error(`CONTROLLER_RELAY_TERMINAL_AUTHORITY_REQUIRED: ${work.workId}`);
642
+ const expectedControllerType = relayControllerType(existing.value);
643
+ if (input.identity.controllerType !== expectedControllerType) throw new Error(`CONTROLLER_RELAY_CONTROLLER_TYPE_MISMATCH: ${work.workId}`);
644
+ if (liveOwner) {
645
+ if (liveOwner.controllerType !== expectedControllerType) throw new Error(`CONTROLLER_RELAY_CONTROLLER_TYPE_MISMATCH: ${work.workId}`);
646
+ if (liveOwner.controllerId !== input.identity.controllerId) throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${work.workId}`);
647
+ if ((liveOwner.principalId?.trim() || liveOwner.controllerId) !== principalId) {
648
+ throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${work.workId}`);
649
+ }
650
+ }
651
+ if (existing.value.controllerId !== input.identity.controllerId) throw new Error(`CONTROLLER_RELAY_CLAIM_CONTROLLER_MISMATCH: ${work.workId}`);
652
+ if (existing.value.principalId !== principalId) throw new Error(`CONTROLLER_RELAY_CLAIM_PRINCIPAL_MISMATCH: ${work.workId}`);
653
+ if (existing.value.claimGeneration < 1) throw new Error(`CONTROLLER_RELAY_CLAIM_GENERATION_REQUIRED: ${work.workId}`);
654
+ return {
655
+ controllerId: existing.value.controllerId,
656
+ controllerType: expectedControllerType,
657
+ principalId: existing.value.principalId,
658
+ controllerInstanceId,
659
+ sessionId,
660
+ claimGeneration: existing.value.claimGeneration,
661
+ };
662
+ })()
663
+ : (() => {
664
+ const owner = assertControllerOwner(options, work.workId, input.identity);
665
+ return {
666
+ controllerId: owner.controllerId,
667
+ controllerType: owner.controllerType,
668
+ principalId: owner.principalId?.trim() || owner.controllerId,
669
+ controllerInstanceId: owner.controllerInstanceId?.trim() || '',
670
+ sessionId: owner.sessionId,
671
+ claimGeneration: owner.claimGeneration,
672
+ };
673
+ })();
674
+ if (existing.value.controllerId !== authority.controllerId) {
675
+ throw new Error(`CONTROLLER_RELAY_CLAIM_CONTROLLER_MISMATCH: ${work.workId}`);
676
+ }
677
+ if (existing.value.principalId !== authority.principalId) {
678
+ throw new Error(`CONTROLLER_RELAY_CLAIM_PRINCIPAL_MISMATCH: ${work.workId}`);
679
+ }
680
+ // A completed Work may have already released its live controller session before
681
+ // the prompt-required terminal disposition arrives. At that point the exact
682
+ // authenticated controller/principal + Work + relay scope remain the lineage
683
+ // fence; MCP session and canonical Runtime instance are allowed to rotate.
684
+ const terminalLineageMigration = terminal;
685
+ if (!terminalLineageMigration && existing.value.controllerInstanceId !== authority.controllerInstanceId) {
686
+ throw new Error(`CONTROLLER_RELAY_CLAIM_INSTANCE_MISMATCH: ${work.workId}`);
687
+ }
688
+ if (!terminalLineageMigration && existing.value.claimGeneration !== authority.claimGeneration) {
689
+ throw new Error(`CONTROLLER_RELAY_CLAIM_GENERATION_MISMATCH: ${work.workId}`);
690
+ }
691
+ const previous = relayHistory(options, relayScopeId)[0];
692
+ const assistantContextUsage = validatedAssistantContextUsage(existing.value, input);
693
+ const at = nowIso(options);
694
+ const qualityDecisions = input.executionQualityDecisions ?? [];
695
+ if (!Array.isArray(qualityDecisions) || qualityDecisions.length > 8) throw new Error('CONTROLLER_QUALITY_DECISION_LIMIT');
696
+ const pending = new Set(pendingQualitySignals(existing.value).map(signal => signal.fingerprint));
697
+ const normalizedQualityDecisions: ExecutionQualityDecision[] = [];
698
+ for (const decision of qualityDecisions) {
699
+ if (!pending.has(decision.fingerprint) || !['no_adjustment', 'adjustment'].includes(decision.action)
700
+ || typeof decision.reason !== 'string' || !decision.reason.trim() || decision.reason.length > 1000
701
+ || decision.action === 'adjustment' && (typeof decision.verificationCondition !== 'string' || !decision.verificationCondition.trim() || decision.verificationCondition.length > 1000)) {
702
+ throw new Error('CONTROLLER_QUALITY_DECISION_INVALID');
703
+ }
704
+ normalizedQualityDecisions.push({ ...decision, decidedAt: at });
705
+ pending.delete(decision.fingerprint);
706
+ }
707
+ const knownDecisions = [...(existing.value.qualityDecisions ?? []), ...normalizedQualityDecisions];
708
+ const submittedAdjustmentResults = input.executionQualityAdjustmentResults ?? [];
709
+ if (!Array.isArray(submittedAdjustmentResults) || submittedAdjustmentResults.length > 8) throw new Error('CONTROLLER_QUALITY_ADJUSTMENT_RESULT_LIMIT');
710
+ const knownResultFingerprints = new Set((existing.value.qualityAdjustmentResults ?? []).map(result => result.fingerprint));
711
+ const normalizedAdjustmentResults: ExecutionQualityAdjustmentResult[] = [];
712
+ for (const submitted of submittedAdjustmentResults) {
713
+ const decision = knownDecisions.find(candidate => candidate.fingerprint === submitted.fingerprint);
714
+ const refs = Array.isArray(submitted.evidenceRefs) ? submitted.evidenceRefs.map(ref => String(ref).trim()).filter(Boolean) : [];
715
+ if (!decision || decision.action !== 'adjustment' || !decision.decidedAt
716
+ || !['improved', 'not_improved', 'inconclusive'].includes(submitted.outcome)
717
+ || typeof submitted.reason !== 'string' || !submitted.reason.trim() || submitted.reason.length > 1000
718
+ || refs.length < 1 || refs.length > 16 || new Set(refs).size !== refs.length
719
+ || knownResultFingerprints.has(submitted.fingerprint)) {
720
+ throw new Error('CONTROLLER_QUALITY_ADJUSTMENT_RESULT_INVALID');
721
+ }
722
+ const decidedAt = Date.parse(decision.decidedAt);
723
+ if (!Number.isFinite(decidedAt) || refs.some(ref => {
724
+ const check = work.checkRefs.find(candidate => candidate.receipt?.receiptId === ref);
725
+ return !check || !check.receipt || !Number.isFinite(Date.parse(check.recordedAt)) || Date.parse(check.recordedAt) <= decidedAt;
726
+ })) throw new Error('CONTROLLER_QUALITY_ADJUSTMENT_VERIFICATION_EVIDENCE_INVALID');
727
+ normalizedAdjustmentResults.push({ fingerprint: submitted.fingerprint, outcome: submitted.outcome, evidenceRefs: refs, reason: submitted.reason.trim(), verifiedAt: at });
728
+ knownResultFingerprints.add(submitted.fingerprint);
729
+ }
730
+ const requestedMaxRounds = boundedInteger(input.maxRounds, DEFAULT_MAX_ROUNDS, 1, 32);
731
+ const requestedMaxRepeatedState = boundedInteger(input.maxRepeatedState, DEFAULT_MAX_REPEATED_STATE, 1, 8);
732
+ const requestedMaxFailures = boundedInteger(input.maxFailures, DEFAULT_MAX_FAILURES, 1, 8);
733
+ const stateFingerprint = bounded(input.stateFingerprint, 256) ?? mechanicalStateFingerprint(options, terminalSuccessor ?? work, requirementId, relayScopeId);
734
+ const bindingId = bounded(input.bindingId, 500) ?? existing.value.bindingId;
735
+ const handoffId = bounded(input.handoffId, 200);
736
+ if (input.disposition === 'wait_for_user') {
737
+ if (!handoffId) throw new Error('CONTROLLER_RELAY_WAIT_FOR_USER_HANDOFF_REQUIRED');
738
+ const handoff = getHandoffItem(options, handoffId);
739
+ if (!handoff) throw new Error(`HANDOFF_NOT_FOUND: ${handoffId}`);
740
+ if (isTerminalHandoffStatus(handoff.status)) throw new Error(`CONTROLLER_RELAY_HANDOFF_TERMINAL: ${handoff.status}`);
741
+ if (handoff.workId && handoff.workId !== work.workId) throw new Error(`CONTROLLER_RELAY_HANDOFF_WORK_MISMATCH: ${handoffId}`);
742
+ }
743
+ const observationWindow = [
744
+ ...(existing.value.observationWindow ?? []),
745
+ closedRoundObservation(work, `${relayScopeId}:${existing.value.roundCount}`, stateFingerprint, input.disposition === 'wait' || input.disposition === 'wait_for_user', existing.value.assistantContextSnapshot, assistantContextUsage),
746
+ ].slice(-8);
747
+ const accumulatedQualityDecisions = [...(existing.value.qualityDecisions ?? []), ...normalizedQualityDecisions].slice(-64);
748
+ const accumulatedQualityAdjustmentResults = [...(existing.value.qualityAdjustmentResults ?? []), ...normalizedAdjustmentResults].slice(-64);
749
+ return applyControllerRoundTransition(options, existing, {
750
+ type: 'semantic_disposition_submitted', at, disposition: input.disposition, stateFingerprint,
751
+ maxRounds: requestedMaxRounds, maxRepeatedState: requestedMaxRepeatedState, maxFailures: requestedMaxFailures,
752
+ controllerSession: authority,
753
+ qualityDecisions: accumulatedQualityDecisions, qualityAdjustmentResults: accumulatedQualityAdjustmentResults, observationWindow,
754
+ ...(handoffId ? { handoffId } : {}),
755
+ ...(bounded(input.reason, 1_000) ? { reason: bounded(input.reason, 1_000) } : {}),
756
+ ...(bindingId ? { bindingId } : {}),
757
+ });
758
+ });
759
+ }
760
+
761
+ export function beginInitialControllerRoundDispatch(
762
+ options: ControllerRoundRelayStoreOptions,
763
+ input: BeginInitialControllerRoundDispatchInput,
764
+ ): ControllerRoundRelayRecord {
765
+ const work = getWorkContract(options, input.workId);
766
+ if (!work) throw new Error(`WORK_NOT_FOUND: ${input.workId}`);
767
+ if (isTerminalWorkContractStatus(work.status)) throw new Error(`CONTROLLER_RELAY_WORK_TERMINAL: ${work.status}`);
768
+ const requirementId = resolveRequirementId(options, work, input.requirementId);
769
+ const requirement = requirementForRelay(options, requirementId);
770
+ if (requirement && !['planned', 'active'].includes(requirement.state)) {
771
+ throw new Error(`CONTROLLER_RELAY_REQUIREMENT_TERMINAL: ${requirement.state}`);
772
+ }
773
+ const relayScopeId = resolveRelayScope(work, requirementId, input.relayScopeId);
774
+
775
+ return relayLock(options, relayScopeId, `controller-relay-launch:${input.identity.controllerId}`, () => {
776
+ const existing = readRelayRecord(options, work.workId);
777
+ if (existing && existing.value.relayScopeId !== relayScopeId) {
778
+ throw new Error(`CONTROLLER_RELAY_SCOPE_MISMATCH: Work ${work.workId} is already bound to ${existing.value.relayScopeId}`);
779
+ }
780
+ const previous = relayHistory(options, relayScopeId)[0];
781
+ const abandonedReleasedRound = previous?.status === 'failed' && previous.failureClass === 'abandoned_release';
782
+ const stateFingerprint = mechanicalStateFingerprint(options, work, requirementId, relayScopeId);
783
+ const occurrenceId = bounded(input.occurrenceId, 500);
784
+ return applyControllerRoundTransition(options, existing, {
785
+ type: 'occurrence_requested', at: nowIso(options), repoId: options.repoId, relayScopeId, originWorkId: work.workId,
786
+ ...(requirementId ? { requirementId } : {}), identity: input.identity, stateFingerprint, proposedAuthorityId: newControllerRoundAuthorityId(),
787
+ maxRounds: boundedInteger(input.maxRounds, DEFAULT_MAX_ROUNDS, 1, 32), maxRepeatedState: boundedInteger(input.maxRepeatedState, DEFAULT_MAX_REPEATED_STATE, 1, 8),
788
+ maxFailures: boundedInteger(input.maxFailures, DEFAULT_MAX_FAILURES, 1, 8),
789
+ ...(bounded(input.bindingId, 500) ? { bindingId: bounded(input.bindingId, 500) } : {}), ...(occurrenceId ? { occurrenceId } : {}),
790
+ abandonedReleaseRecovery: Boolean(abandonedReleasedRound),
791
+ });
792
+ });
793
+ }
794
+
795
+ export function beginControllerRoundRelayAfterRelease(
796
+ options: ControllerRoundRelayStoreOptions,
797
+ input: { workId: string; releasedSession: ControllerSession },
798
+ ): ControllerRoundRelayRecord | undefined {
799
+ const initial = readRelayRecord(options, input.workId);
800
+ if (!initial || initial.value.status !== 'pending_release') return undefined;
801
+ return relayLock(options, initial.value.relayScopeId, `controller-relay-begin:${input.releasedSession.controllerId}`, () => {
802
+ const current = readRelayRecord(options, input.workId);
803
+ if (!current || current.value.status !== 'pending_release') return undefined;
804
+ const record = current.value;
805
+ const releasedPrincipal = input.releasedSession.principalId?.trim() || input.releasedSession.controllerId;
806
+ if (
807
+ record.claimGeneration !== input.releasedSession.claimGeneration
808
+ || record.controllerId !== input.releasedSession.controllerId
809
+ || record.principalId !== releasedPrincipal
810
+ ) {
811
+ throw new Error(`CONTROLLER_RELAY_RELEASE_FENCE_MISMATCH: ${input.workId}`);
812
+ }
813
+ const at = nowIso(options);
814
+ if (record.successorWorkId) {
815
+ const predecessor = getWorkContract(options, input.workId);
816
+ if (!predecessor || predecessor.status !== 'completed') {
817
+ throw new Error(`CONTROLLER_RELAY_SUCCESSOR_PREDECESSOR_NOT_COMPLETED: ${input.workId}:${predecessor?.status ?? 'missing'}`);
818
+ }
819
+ const successor = assertControllerRoundSuccessorLineage(options, predecessor, record.successorWorkId);
820
+ const successorStateFingerprint = mechanicalStateFingerprint(options, successor, record.requirementId, record.relayScopeId);
821
+ return withControlPlaneTransaction(options.controllerHome, (database) => {
822
+ const predecessorRelay = readControlPlaneRecordWithinTransaction<ControllerRoundRelayRecord>(
823
+ database, NAMESPACE, options.repoId, input.workId,
824
+ );
825
+ if (!predecessorRelay || predecessorRelay.value.status !== 'pending_release' || predecessorRelay.value.successorWorkId !== successor.workId) {
826
+ throw new Error(`CONTROLLER_RELAY_SUCCESSOR_HANDOFF_STALE: ${input.workId}`);
827
+ }
828
+ const existingSuccessorRelay = readControlPlaneRecordWithinTransaction<ControllerRoundRelayRecord>(
829
+ database, NAMESPACE, options.repoId, successor.workId,
830
+ );
831
+ if (existingSuccessorRelay) {
832
+ if (existingSuccessorRelay.value.relayScopeId === record.relayScopeId
833
+ && ['dispatching', 'dispatched', 'claimed'].includes(existingSuccessorRelay.value.status)) {
834
+ return existingSuccessorRelay.value;
835
+ }
836
+ throw new Error(`CONTROLLER_RELAY_SUCCESSOR_ALREADY_HAS_ROUND: ${successor.workId}`);
837
+ }
838
+ const decision = atomicTransitionDecisionOrThrow(decideControllerRoundTransition(predecessorRelay.value, {
839
+ type: 'successor_release_handoff', at, successorWorkId: successor.workId, successorStateFingerprint, proposedAuthorityId: newControllerRoundAuthorityId(),
840
+ }));
841
+ writeControlPlaneRecordWithinTransaction(database, {
842
+ namespace: NAMESPACE, scope: options.repoId, key: predecessor.workId, schemaVersion: SCHEMA_VERSION,
843
+ value: decision.next, action: decision.action, expectedRevision: predecessorRelay.revision,
844
+ });
845
+ writeControlPlaneRecordWithinTransaction(database, {
846
+ namespace: NAMESPACE, scope: options.repoId, key: decision.relatedWorkId, schemaVersion: SCHEMA_VERSION,
847
+ value: decision.relatedNext, action: decision.relatedAction, expectedRevision: null,
848
+ });
849
+ return decision.relatedNext;
850
+ });
851
+ }
852
+ return applyControllerRoundTransition(options, current, {
853
+ type: 'controller_release_observed', at, proposedAuthorityId: newControllerRoundAuthorityId(),
854
+ });
855
+ });
856
+ }
857
+
858
+ /**
859
+ * Mechanically close a claimed controller round only after its exact controller
860
+ * epoch has been released and durable state proves no controller lease remains.
861
+ * This never invents a semantic disposition; launcher_start must open a new round.
862
+ */
863
+ export function reconcileControllerRoundAfterAbandonedRelease(
864
+ options: ControllerRoundRelayStoreOptions,
865
+ input: { workId: string; releasedSession: ControllerSession },
866
+ ): ControllerRoundRelayRecord | undefined {
867
+ const initial = readRelayRecord(options, input.workId);
868
+ if (!initial || initial.value.status !== 'claimed') return undefined;
869
+ return relayLock(options, initial.value.relayScopeId, `controller-relay-abandoned-release:${input.releasedSession.controllerId}`, () => {
870
+ const current = readRelayRecord(options, input.workId);
871
+ if (!current || current.value.status !== 'claimed') return undefined;
872
+ const record = current.value;
873
+ const releasedPrincipal = input.releasedSession.principalId?.trim() || input.releasedSession.controllerId;
874
+ const releasedInstanceId = input.releasedSession.controllerInstanceId?.trim() || '';
875
+ if (
876
+ record.claimGeneration !== input.releasedSession.claimGeneration
877
+ || record.controllerId !== input.releasedSession.controllerId
878
+ || record.principalId !== releasedPrincipal
879
+ || record.controllerInstanceId !== releasedInstanceId
880
+ ) {
881
+ throw new Error(`CONTROLLER_RELAY_RELEASE_FENCE_MISMATCH: ${input.workId}`);
882
+ }
883
+ if (getControllerSession(options, input.workId)) {
884
+ throw new Error(`CONTROLLER_RELAY_ABANDONED_RELEASE_ACTIVE_CLAIM: ${input.workId}`);
885
+ }
886
+ const work = getWorkContract(options, input.workId);
887
+ // Completed Work still requires semantic goal_complete or a valid terminal
888
+ // successor. Failed/cancelled Work has no legal semantic continuation, so
889
+ // its exact released controller epoch may mechanically abandon the claimed
890
+ // round and free the relay scope for later recovery/replanning.
891
+ if (!work || work.status === 'completed') return undefined;
892
+
893
+ return applyControllerRoundTransition(options, current, {
894
+ type: 'abandoned_release_observed', at: nowIso(options), error: CONTROLLER_RELAY_ABANDONED_RELEASE_ERROR,
895
+ });
896
+ });
897
+ }
898
+
899
+ /**
900
+ * Mechanically retire any surviving ControllerRound once Work lifecycle authority
901
+ * is durably failed/cancelled and no Controller lease remains. This is cleanup,
902
+ * not a semantic disposition: completed Work is intentionally excluded.
903
+ */
904
+ export function reconcileControllerRoundAfterTerminalWork(
905
+ options: ControllerRoundRelayStoreOptions,
906
+ input: { workId: string; actor?: string },
907
+ ): ControllerRoundRelayRecord | undefined {
908
+ const initial = readRelayRecord(options, input.workId);
909
+ if (!initial) return undefined;
910
+ const work = getWorkContract(options, input.workId);
911
+ if (!work || !['failed', 'cancelled'].includes(work.status)) return undefined;
912
+ if (getControllerSession(options, input.workId)) {
913
+ throw new Error(`CONTROLLER_RELAY_TERMINAL_WORK_ACTIVE_CLAIM: ${input.workId}`);
914
+ }
915
+ if (initial.value.status === 'failed') return initial.value;
916
+ return relayLock(options, initial.value.relayScopeId, input.actor ?? `controller-relay-terminal-work:${input.workId}`, () => {
917
+ const current = readRelayRecord(options, input.workId);
918
+ if (!current) return undefined;
919
+ const currentWork = getWorkContract(options, input.workId);
920
+ if (!currentWork || !['failed', 'cancelled'].includes(currentWork.status)) return undefined;
921
+ if (getControllerSession(options, input.workId)) {
922
+ throw new Error(`CONTROLLER_RELAY_TERMINAL_WORK_ACTIVE_CLAIM: ${input.workId}`);
923
+ }
924
+ if (current.value.status === 'failed') return current.value;
925
+ return applyControllerRoundTransition(options, current, {
926
+ type: 'terminal_work_observed', at: nowIso(options), error: `CONTROLLER_RELAY_TERMINAL_WORK_RETIRED:${input.workId}`,
927
+ });
928
+ });
929
+ }
930
+
931
+ export function finishControllerRoundRelayDispatch(
932
+ options: ControllerRoundRelayStoreOptions,
933
+ input: { workId: string; ok: boolean; bindingId?: string; providerDispatchEffectId?: string; providerDispatchReceiptId?: string; error?: string; recovery?: boolean; outcomeUnknown?: boolean; waitForUser?: boolean; handoffId?: string; nowMs?: number },
934
+ ): ControllerRoundRelayRecord | undefined {
935
+ const initial = readRelayRecord(options, input.workId);
936
+ if (!initial || initial.value.status !== 'dispatching') return initial?.value;
937
+ return relayLock(options, initial.value.relayScopeId, `controller-relay-finish:${input.workId}`, () => {
938
+ const current = readRelayRecord(options, input.workId);
939
+ if (!current || current.value.status !== 'dispatching') return current?.value;
940
+ const at = typeof input.nowMs === 'number' ? new Date(input.nowMs).toISOString() : nowIso(options);
941
+ const handoffId = bounded(input.handoffId, 200);
942
+ if (input.waitForUser) {
943
+ if (!handoffId) throw new Error('CONTROLLER_RELAY_WAIT_FOR_USER_HANDOFF_REQUIRED');
944
+ const handoff = getHandoffItem(options, handoffId);
945
+ if (!handoff) throw new Error(`HANDOFF_NOT_FOUND: ${handoffId}`);
946
+ if (isTerminalHandoffStatus(handoff.status)) throw new Error(`CONTROLLER_RELAY_HANDOFF_TERMINAL: ${handoff.status}`);
947
+ if (handoff.workId && handoff.workId !== input.workId) throw new Error(`CONTROLLER_RELAY_HANDOFF_WORK_MISMATCH: ${handoffId}`);
948
+ }
949
+ const error = bounded(input.error, 2_000) ?? (input.outcomeUnknown
950
+ ? 'CONTROLLER_RELAY_PROVIDER_DISPATCH_OUTCOME_UNKNOWN'
951
+ : input.waitForUser
952
+ ? 'CONTROLLER_RELAY_WAIT_FOR_USER'
953
+ : input.recovery
954
+ ? 'CONTROLLER_RELAY_RECOVERY_FAILED'
955
+ : 'CONTROLLER_RELAY_DISPATCH_FAILED');
956
+ const providerDispatchEffectId = bounded(input.providerDispatchEffectId, 500)
957
+ ?? (current.value.authorityId ? controllerRoundProviderEffectId(current.value.relayScopeId, current.value.authorityId) : undefined);
958
+ let event: ControllerRoundTransitionEvent;
959
+ if (input.ok) {
960
+ if (!providerDispatchEffectId) throw new Error('CONTROLLER_RELAY_PROVIDER_EFFECT_ID_REQUIRED');
961
+ event = { type: 'provider_dispatch_succeeded', at, providerDispatchEffectId, ...(bounded(input.bindingId, 500) ? { bindingId: bounded(input.bindingId, 500) } : {}), ...(bounded(input.providerDispatchReceiptId, 500) ? { providerDispatchReceiptId: bounded(input.providerDispatchReceiptId, 500) } : {}) };
962
+ } else if (input.outcomeUnknown) {
963
+ const effectIdentity = providerDispatchEffectId;
964
+ if (!effectIdentity) throw new Error('CONTROLLER_RELAY_PROVIDER_EFFECT_ID_REQUIRED');
965
+ event = { type: 'provider_dispatch_outcome_unknown', at, error, providerDispatchEffectId: effectIdentity };
966
+ } else if (input.waitForUser) {
967
+ event = { type: 'provider_user_action_required', at, error, handoffId: handoffId! };
968
+ } else {
969
+ const nextFailureCount = current.value.consecutiveFailures + 1;
970
+ const recoveryDelayMs = Math.min(MAX_STALLED_RECOVERY_BACKOFF_MS, DEFAULT_STALLED_RECOVERY_BACKOFF_MS * 2 ** Math.max(0, nextFailureCount - 1));
971
+ event = { type: 'provider_dispatch_failed', at, error, recovery: input.recovery === true, ...(input.recovery ? { nextRecoveryAt: new Date(Date.parse(at) + recoveryDelayMs).toISOString() } : {}) };
972
+ }
973
+ return applyControllerRoundTransition(options, current, event);
974
+ });
975
+ }
976
+
977
+ export function acknowledgeControllerRoundClaim(
978
+ options: ControllerRoundRelayStoreOptions,
979
+ input: { workId: string; session: ControllerSession; assistantContextSnapshot?: AssistantContextSnapshot | null },
980
+ ): ControllerRoundRelayRecord | undefined {
981
+ const initial = readRelayRecord(options, input.workId);
982
+ if (!initial) return undefined;
983
+ const expectedControllerType = relayControllerType(initial.value);
984
+ if (input.session.controllerType !== expectedControllerType) throw new Error(`CONTROLLER_RELAY_CONTROLLER_TYPE_MISMATCH: ${input.workId}`);
985
+
986
+ return relayLock(options, initial.value.relayScopeId, `controller-relay-claim:${input.session.controllerId}`, () => {
987
+ const current = readRelayRecord(options, input.workId);
988
+ if (!current) return undefined;
989
+ if (current.value.status === 'claimed'
990
+ && current.value.controllerId === input.session.controllerId
991
+ && current.value.sessionId === input.session.sessionId
992
+ && current.value.claimGeneration === input.session.claimGeneration
993
+ && sameAssistantContextSnapshot(current.value.assistantContextSnapshot, input.assistantContextSnapshot)) return current.value;
994
+
995
+ const blocker = controllerRoundBlockerClass(current.value);
996
+ const policyClaimable = ['dispatching', 'dispatched', 'claimed'].includes(current.value.status)
997
+ || blocker === 'provider_dispatch_outcome_unknown'
998
+ || blocker === 'repeated_state';
999
+ if (!policyClaimable) return current.value;
1000
+
1001
+ const owner = getControllerSession(options, input.workId);
1002
+ const ownerPrincipal = owner?.principalId?.trim() || owner?.controllerId;
1003
+ const sessionPrincipal = input.session.principalId?.trim() || input.session.controllerId;
1004
+ if (
1005
+ !owner
1006
+ || owner.controllerType !== relayControllerType(current.value)
1007
+ || owner.controllerId !== input.session.controllerId
1008
+ || owner.sessionId !== input.session.sessionId
1009
+ || owner.claimGeneration !== input.session.claimGeneration
1010
+ || ownerPrincipal !== sessionPrincipal
1011
+ || (owner.controllerInstanceId?.trim() || '') !== (input.session.controllerInstanceId?.trim() || '')
1012
+ ) throw new Error(`CONTROLLER_RELAY_CLAIM_IDENTITY_MISMATCH: ${input.workId}`);
1013
+ // The dispatching relay records the provider/launcher identity until the
1014
+ // authenticated controller actually claims the round. The domain transition
1015
+ // policy owns that one-way identity handoff; duplicating an equality guard
1016
+ // here would turn provider dispatch metadata into a second claim authority.
1017
+ if (typeof owner.claimGeneration !== 'number' || owner.claimGeneration < 1) {
1018
+ throw new Error(`CONTROLLER_RELAY_CLAIM_GENERATION_REQUIRED: ${input.workId}`);
1019
+ }
1020
+ const controllerInstanceId = owner.controllerInstanceId?.trim();
1021
+ if (!controllerInstanceId) throw new Error(`CONTROLLER_RELAY_CLAIM_INSTANCE_REQUIRED: ${input.workId}`);
1022
+ const at = nowIso(options);
1023
+ const session = owner as ControllerSession & { claimGeneration: number };
1024
+
1025
+ if (blocker === 'repeated_state') {
1026
+ const work = getWorkContract(options, input.workId);
1027
+ if (!work || isTerminalWorkContractStatus(work.status)) return current.value;
1028
+ const stateFingerprint = mechanicalStateFingerprint(options, work, current.value.requirementId, current.value.relayScopeId);
1029
+ const transitioned = applyControllerRoundTransition(options, current, {
1030
+ type: 'semantic_state_changed', at, stateFingerprint, session, principalId: ownerPrincipal!, controllerInstanceId,
1031
+ });
1032
+ return persistClaimedAssistantContextSnapshot(options, transitioned, input.assistantContextSnapshot, at);
1033
+ }
1034
+
1035
+ const transitioned = applyControllerRoundTransition(options, current, {
1036
+ type: 'controller_claim_observed', at, session, principalId: ownerPrincipal!, controllerInstanceId,
1037
+ });
1038
+ return persistClaimedAssistantContextSnapshot(options, transitioned, input.assistantContextSnapshot, at);
1039
+ });
1040
+ }
1041
+
1042
+ /**
1043
+ * Explicitly rekey one exact relay-bound Work after the caller lost the opaque
1044
+ * per-round capability with its MCP/ChatGPT transport. This is not a semantic
1045
+ * continuation and never dispatches a provider: it preserves the current
1046
+ * relay lineage and budgets, rotates only the capability epoch, and leaves the
1047
+ * standard controller_claim path responsible for mechanically acknowledging
1048
+ * the recovered round.
1049
+ */
1050
+ export function recoverControllerRoundRelayAuthority(
1051
+ options: ControllerRoundRelayStoreOptions,
1052
+ input: RecoverControllerRoundRelayAuthorityInput,
1053
+ ): ControllerRoundRelayRecord {
1054
+ const workId = input.workId.trim();
1055
+ if (!workId) throw new Error('WORK_CONTROLLER_AUTHORITY_RECOVERY_WORK_REQUIRED');
1056
+ if (input.requestedBy !== 'user') {
1057
+ throw new Error('WORK_CONTROLLER_AUTHORITY_RECOVERY_USER_REQUIRED: relay authority rekey is an explicit user-directed recovery only.');
1058
+ }
1059
+ const work = getWorkContract(options, workId);
1060
+ if (!work) throw new Error(`WORK_NOT_FOUND: ${workId}`);
1061
+ if (isTerminalWorkContractStatus(work.status)) {
1062
+ throw new Error(`WORK_CONTROLLER_AUTHORITY_RECOVERY_TERMINAL: ${workId}:${work.status}`);
1063
+ }
1064
+ const initial = readRelayRecord(options, workId);
1065
+ if (!initial) throw new Error(`WORK_CONTROLLER_AUTHORITY_RECOVERY_RELAY_REQUIRED: ${workId}`);
1066
+ const expectedType = relayControllerType(initial.value);
1067
+ if (expectedType !== input.identity.controllerType) {
1068
+ throw new Error(`WORK_CONTROLLER_AUTHORITY_RECOVERY_CONTROLLER_TYPE_MISMATCH: ${workId}`);
1069
+ }
1070
+ if (initial.value.controllerId !== input.identity.controllerId || initial.value.principalId !== input.identity.principalId) {
1071
+ throw new Error(`WORK_CONTROLLER_AUTHORITY_RECOVERY_PRINCIPAL_MISMATCH: ${workId}`);
1072
+ }
1073
+ const recoverableStatuses: readonly ControllerRoundRelayStatus[] = ['pending_release', 'dispatching', 'dispatched', 'claimed', 'failed'];
1074
+ if (!recoverableStatuses.includes(initial.value.status)) {
1075
+ throw new Error(`WORK_CONTROLLER_AUTHORITY_RECOVERY_RELAY_STATE_INVALID: ${workId}:${initial.value.status}`);
1076
+ }
1077
+
1078
+ return relayLock(options, initial.value.relayScopeId, `controller-relay-explicit-recover:${workId}`, () => {
1079
+ const current = readRelayRecord(options, workId);
1080
+ if (!current) throw new Error(`WORK_CONTROLLER_AUTHORITY_RECOVERY_RELAY_REQUIRED: ${workId}`);
1081
+ const latest = relayHistory(options, current.value.relayScopeId)[0];
1082
+ if (!latest || latest.originWorkId !== workId || latest.updatedAt !== current.value.updatedAt) {
1083
+ throw new Error(`WORK_CONTROLLER_AUTHORITY_RECOVERY_RELAY_NOT_CURRENT: ${workId}`);
1084
+ }
1085
+ if (!recoverableStatuses.includes(current.value.status)) {
1086
+ throw new Error(`WORK_CONTROLLER_AUTHORITY_RECOVERY_RELAY_STATE_INVALID: ${workId}:${current.value.status}`);
1087
+ }
1088
+ if (relayControllerType(current.value) !== input.identity.controllerType
1089
+ || current.value.controllerId !== input.identity.controllerId
1090
+ || current.value.principalId !== input.identity.principalId) {
1091
+ throw new Error(`WORK_CONTROLLER_AUTHORITY_RECOVERY_PRINCIPAL_MISMATCH: ${workId}`);
1092
+ }
1093
+ const currentWork = getWorkContract(options, workId);
1094
+ if (!currentWork) throw new Error(`WORK_NOT_FOUND: ${workId}`);
1095
+ if (isTerminalWorkContractStatus(currentWork.status)) {
1096
+ throw new Error(`WORK_CONTROLLER_AUTHORITY_RECOVERY_TERMINAL: ${workId}:${currentWork.status}`);
1097
+ }
1098
+ const activeWorks = relevantWork(options, current.value).filter((entry) => !isTerminalWorkContractStatus(entry.status));
1099
+ if (activeWorks.some((entry) => workHasActiveExecution(options.controllerHome, options.repoId, entry.workId))) {
1100
+ throw new Error(`WORK_CONTROLLER_AUTHORITY_RECOVERY_ACTIVE_EXECUTION: ${workId}`);
1101
+ }
1102
+ if (activeWorks.some((entry) => Boolean(getControllerSession(options, entry.workId)))) {
1103
+ throw new Error(`WORK_CONTROLLER_AUTHORITY_RECOVERY_ACTIVE_CLAIM: ${workId}`);
1104
+ }
1105
+
1106
+ return applyControllerRoundTransition(options, current, {
1107
+ type: 'authority_recovery_requested', at: nowIso(options), proposedAuthorityId: newControllerRoundAuthorityId(),
1108
+ keepsConfirmedDispatch: current.value.status === 'dispatched',
1109
+ });
1110
+ });
1111
+ }
1112
+
1113
+ export interface RearmControllerRoundAfterProviderRecoveryInput {
1114
+ workId: string;
1115
+ relayScopeId: string;
1116
+ authorityId: string;
1117
+ expectedUpdatedAt: string;
1118
+ evidenceId: string;
1119
+ }
1120
+
1121
+ export interface BindLegacyControllerRoundOccurrenceInput {
1122
+ workId: string;
1123
+ relayScopeId: string;
1124
+ occurrenceId: string;
1125
+ authorityId: string;
1126
+ expectedUpdatedAt: string;
1127
+ identity: Pick<ControllerRoundRelayIdentity, 'controllerId' | 'controllerType' | 'principalId'>;
1128
+ }
1129
+
1130
+ /** Exact evidence-gated provider/environment recovery for one exhausted same-round dispatch responsibility. */
1131
+ export function rearmControllerRoundAfterProviderRecovery(
1132
+ options: ControllerRoundRelayStoreOptions,
1133
+ input: RearmControllerRoundAfterProviderRecoveryInput,
1134
+ ): ControllerRoundRelayRecord {
1135
+ const workId = input.workId.trim();
1136
+ const initial = readRelayRecord(options, workId);
1137
+ if (!initial) throw new Error(`CONTROLLER_RELAY_PROVIDER_RECOVERY_RELAY_REQUIRED: ${workId}`);
1138
+ if (initial.value.relayScopeId !== input.relayScopeId.trim()) throw new Error(`CONTROLLER_RELAY_PROVIDER_RECOVERY_SCOPE_MISMATCH: ${workId}`);
1139
+ return relayLock(options, initial.value.relayScopeId, `controller-relay-provider-recovered:${workId}`, () => {
1140
+ const current = readRelayRecord(options, workId);
1141
+ if (!current) throw new Error(`CONTROLLER_RELAY_PROVIDER_RECOVERY_RELAY_REQUIRED: ${workId}`);
1142
+ if (current.value.updatedAt !== input.expectedUpdatedAt.trim()) throw new Error(`CONTROLLER_RELAY_PROVIDER_RECOVERY_STALE: ${workId}`);
1143
+ if (current.value.relayScopeId !== input.relayScopeId.trim()) throw new Error(`CONTROLLER_RELAY_PROVIDER_RECOVERY_SCOPE_MISMATCH: ${workId}`);
1144
+ if ((current.value.authorityId?.trim() || '') !== input.authorityId.trim()) throw new Error(`CONTROLLER_RELAY_PROVIDER_RECOVERY_AUTHORITY_MISMATCH: ${workId}`);
1145
+ if (controllerRoundBlockerClass(current.value) !== 'consecutive_failures') throw new Error(`CONTROLLER_RELAY_PROVIDER_RECOVERY_BLOCKER_MISMATCH: ${workId}`);
1146
+ const work = getWorkContract(options, workId);
1147
+ if (!work || isTerminalWorkContractStatus(work.status)) throw new Error(`CONTROLLER_RELAY_PROVIDER_RECOVERY_WORK_TERMINAL: ${workId}:${work?.status ?? 'missing'}`);
1148
+ const requirement = requirementForRelay(options, current.value.requirementId);
1149
+ if (requirement && !['planned', 'active'].includes(requirement.state)) throw new Error(`CONTROLLER_RELAY_PROVIDER_RECOVERY_REQUIREMENT_TERMINAL: ${requirement.state}`);
1150
+ const activeWorks = relevantWork(options, current.value).filter((entry) => !isTerminalWorkContractStatus(entry.status));
1151
+ if (activeWorks.some((entry) => workHasActiveExecution(options.controllerHome, options.repoId, entry.workId))) throw new Error(`CONTROLLER_RELAY_PROVIDER_RECOVERY_ACTIVE_EXECUTION: ${workId}`);
1152
+ if (activeWorks.some((entry) => Boolean(getControllerSession(options, entry.workId)))) throw new Error(`CONTROLLER_RELAY_PROVIDER_RECOVERY_ACTIVE_CLAIM: ${workId}`);
1153
+ const evidenceId = bounded(input.evidenceId, 500);
1154
+ if (!evidenceId) throw new Error('CONTROLLER_RELAY_PROVIDER_RECOVERY_EVIDENCE_REQUIRED');
1155
+ return applyControllerRoundTransition(options, current, { type: 'provider_environment_recovered', at: nowIso(options), evidenceId });
1156
+ });
1157
+ }
1158
+
1159
+ /**
1160
+ * One-time migration of a pre-occurrence-era dispatch responsibility onto the
1161
+ * first explicit post-recovery Scheduler occurrence. This does not create a
1162
+ * round or provider effect: exact round authority, CAS and controller lineage
1163
+ * fence the single occurrenceId write before normal strict occurrence matching
1164
+ * resumes.
1165
+ */
1166
+ export function bindLegacyControllerRoundOccurrence(
1167
+ options: ControllerRoundRelayStoreOptions,
1168
+ input: BindLegacyControllerRoundOccurrenceInput,
1169
+ ): ControllerRoundRelayRecord {
1170
+ const workId = input.workId.trim();
1171
+ const relayScopeId = input.relayScopeId.trim();
1172
+ const occurrenceId = bounded(input.occurrenceId, 500);
1173
+ if (!occurrenceId) throw new Error('CONTROLLER_RELAY_OCCURRENCE_ID_REQUIRED');
1174
+ const initial = readRelayRecord(options, workId);
1175
+ if (!initial) throw new Error(`CONTROLLER_RELAY_LEGACY_OCCURRENCE_RELAY_REQUIRED: ${workId}`);
1176
+ if (initial.value.relayScopeId !== relayScopeId) throw new Error(`CONTROLLER_RELAY_LEGACY_OCCURRENCE_SCOPE_MISMATCH: ${workId}`);
1177
+ return relayLock(options, initial.value.relayScopeId, `controller-relay-bind-legacy-occurrence:${workId}`, () => {
1178
+ const current = readRelayRecord(options, workId);
1179
+ if (!current) throw new Error(`CONTROLLER_RELAY_LEGACY_OCCURRENCE_RELAY_REQUIRED: ${workId}`);
1180
+ if (current.value.updatedAt !== input.expectedUpdatedAt.trim()) throw new Error(`CONTROLLER_RELAY_LEGACY_OCCURRENCE_STALE: ${workId}`);
1181
+ if (current.value.relayScopeId !== relayScopeId) throw new Error(`CONTROLLER_RELAY_LEGACY_OCCURRENCE_SCOPE_MISMATCH: ${workId}`);
1182
+ if (current.value.originWorkId !== workId) throw new Error(`CONTROLLER_RELAY_LEGACY_OCCURRENCE_WORK_MISMATCH: ${workId}`);
1183
+ if ((current.value.authorityId?.trim() || '') !== input.authorityId.trim()) throw new Error(`CONTROLLER_RELAY_LEGACY_OCCURRENCE_AUTHORITY_MISMATCH: ${workId}`);
1184
+ const expectedPrincipalId = input.identity.principalId.trim() || input.identity.controllerId.trim();
1185
+ const currentPrincipalId = current.value.principalId?.trim() || current.value.controllerId;
1186
+ if (current.value.controllerId !== input.identity.controllerId.trim()
1187
+ || relayControllerType(current.value) !== input.identity.controllerType
1188
+ || currentPrincipalId !== expectedPrincipalId) {
1189
+ throw new Error(`CONTROLLER_RELAY_LEGACY_OCCURRENCE_CONTROLLER_MISMATCH: ${workId}`);
1190
+ }
1191
+ const work = getWorkContract(options, workId);
1192
+ if (!work || isTerminalWorkContractStatus(work.status)) throw new Error(`CONTROLLER_RELAY_LEGACY_OCCURRENCE_WORK_TERMINAL: ${workId}:${work?.status ?? 'missing'}`);
1193
+ const requirement = requirementForRelay(options, current.value.requirementId);
1194
+ if (requirement && !['planned', 'active'].includes(requirement.state)) throw new Error(`CONTROLLER_RELAY_LEGACY_OCCURRENCE_REQUIREMENT_TERMINAL: ${requirement.state}`);
1195
+ return applyControllerRoundTransition(options, current, { type: 'legacy_occurrence_bound', at: nowIso(options), occurrenceId });
1196
+ });
1197
+ }
1198
+
1199
+ export function claimStalledControllerRoundRelays(
1200
+ options: ControllerRoundRelayStoreOptions,
1201
+ input: { nowMs?: number; graceMs?: number; limit?: number; controllerTypes?: readonly ControllerType[] } = {},
1202
+ ): ControllerRoundRelayRecord[] {
1203
+ const nowMs = input.nowMs ?? Date.now();
1204
+ const graceMs = Math.max(60_000, Math.min(input.graceMs ?? DEFAULT_UNCLOSED_ROUND_GRACE_MS, MAX_UNCLOSED_ROUND_GRACE_MS));
1205
+ const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 2), 16));
1206
+ const claimed: ControllerRoundRelayRecord[] = [];
1207
+
1208
+ for (const candidate of latestRelayRecordsByScope(options)) {
1209
+ if (claimed.length >= limit) break;
1210
+ const repeatedStateBlocked = candidate.status === 'blocked' && candidate.blockedReason?.startsWith('repeated_state:') === true;
1211
+ if (!['pending_release', 'dispatching', 'dispatched', 'claimed'].includes(candidate.status) && !repeatedStateBlocked) continue;
1212
+ if (input.controllerTypes && !input.controllerTypes.includes(relayControllerType(candidate))) continue;
1213
+ const scheduledRecoveryAtMs = candidate.nextRecoveryAt ? Date.parse(candidate.nextRecoveryAt) : Number.NaN;
1214
+ if (Number.isFinite(scheduledRecoveryAtMs)) {
1215
+ if (nowMs < scheduledRecoveryAtMs) continue;
1216
+ } else {
1217
+ const roundOpenedAtMs = Date.parse(candidate.claimedAt ?? candidate.dispatchedAt ?? candidate.updatedAt);
1218
+ if (!Number.isFinite(roundOpenedAtMs) || nowMs - roundOpenedAtMs < graceMs) continue;
1219
+ }
1220
+ const requirement = requirementForRelay(options, candidate.requirementId);
1221
+ if (requirement && !['planned', 'active'].includes(requirement.state)) continue;
1222
+ const candidateWorks = relevantWork(options, candidate);
1223
+ const activeCandidateWorks = candidateWorks.filter((work) => !isTerminalWorkContractStatus(work.status));
1224
+ if (activeCandidateWorks.length === 0) continue;
1225
+ if (activeCandidateWorks.some((work) => workHasActiveExecution(options.controllerHome, options.repoId, work.workId) || controllerSessionBlocksRecovery(options, work.workId, { nowMs, graceMs }))) continue;
1226
+ if (repeatedStateBlocked) {
1227
+ const fingerprintWork = activeCandidateWorks[0] ?? getWorkContract(options, candidate.originWorkId);
1228
+ const currentFingerprint = fingerprintWork
1229
+ ? mechanicalStateFingerprint(options, fingerprintWork, candidate.requirementId, candidate.relayScopeId)
1230
+ : candidate.stateFingerprint;
1231
+ if (currentFingerprint === candidate.stateFingerprint) continue;
1232
+ }
1233
+
1234
+ const next = relayLock(options, candidate.relayScopeId, `controller-relay-recover:${candidate.originWorkId}`, () => {
1235
+ const latest = relayHistory(options, candidate.relayScopeId)[0];
1236
+ if (!latest || latest.originWorkId !== candidate.originWorkId || latest.updatedAt !== candidate.updatedAt || latest.status !== candidate.status) return undefined;
1237
+ const latestRepeatedStateBlocked = latest.status === 'blocked' && latest.blockedReason?.startsWith('repeated_state:') === true;
1238
+ if (latest.status === 'blocked' && !latestRepeatedStateBlocked) return undefined;
1239
+ if (input.controllerTypes && !input.controllerTypes.includes(relayControllerType(latest))) return undefined;
1240
+ const latestRecoveryAtMs = latest.nextRecoveryAt ? Date.parse(latest.nextRecoveryAt) : Number.NaN;
1241
+ if (Number.isFinite(latestRecoveryAtMs)) {
1242
+ if (nowMs < latestRecoveryAtMs) return undefined;
1243
+ } else {
1244
+ const latestRoundOpenedAtMs = Date.parse(latest.claimedAt ?? latest.dispatchedAt ?? latest.updatedAt);
1245
+ if (!Number.isFinite(latestRoundOpenedAtMs) || nowMs - latestRoundOpenedAtMs < graceMs) return undefined;
1246
+ }
1247
+ const latestRequirement = requirementForRelay(options, latest.requirementId);
1248
+ if (latestRequirement && !['planned', 'active'].includes(latestRequirement.state)) return undefined;
1249
+ const works = relevantWork(options, latest);
1250
+ const activeWorks = works.filter((work) => !isTerminalWorkContractStatus(work.status));
1251
+ if (activeWorks.length === 0) return undefined;
1252
+ if (activeWorks.some((work) => workHasActiveExecution(options.controllerHome, options.repoId, work.workId) || controllerSessionBlocksRecovery(options, work.workId, { nowMs, graceMs }))) return undefined;
1253
+
1254
+ const currentRecord = readRelayRecord(options, latest.originWorkId);
1255
+ if (!currentRecord || currentRecord.value.updatedAt !== latest.updatedAt || currentRecord.value.status !== latest.status) return undefined;
1256
+ const fingerprintWork = activeWorks[0] ?? getWorkContract(options, latest.originWorkId);
1257
+ const stateFingerprint = fingerprintWork
1258
+ ? mechanicalStateFingerprint(options, fingerprintWork, latest.requirementId, latest.relayScopeId)
1259
+ : latest.stateFingerprint;
1260
+ const at = new Date(nowMs).toISOString();
1261
+ const lastError = latestRepeatedStateBlocked
1262
+ ? undefined
1263
+ : latest.nextRecoveryAt
1264
+ ? latest.lastError
1265
+ : latest.status === 'pending_release'
1266
+ ? 'CONTROLLER_RELAY_RELEASE_TRANSITION_INCOMPLETE'
1267
+ : latest.status === 'dispatching'
1268
+ ? 'CONTROLLER_RELAY_DISPATCH_TRANSITION_INCOMPLETE'
1269
+ : latest.status === 'claimed' ? 'CONTROLLER_RELAY_CLAIMED_ROUND_UNCLOSED' : 'CONTROLLER_RELAY_ROUND_UNCLOSED';
1270
+ const recovered = applyControllerRoundTransition(options, currentRecord, {
1271
+ type: 'stalled_round_observed', at, stateFingerprint, proposedAuthorityId: newControllerRoundAuthorityId(),
1272
+ ...(lastError ? { lastError } : {}),
1273
+ });
1274
+ return recovered.status === 'dispatching' ? recovered : undefined;
1275
+ });
1276
+ if (next) claimed.push(next);
1277
+ }
1278
+ return claimed;
1279
+ }
1280
+
1281
+ const CONTROLLER_ROUND_ORIGIN_OBJECTIVE_MAX_CHARS = 4_000;
1282
+ const CONTROLLER_ROUND_RELATED_OBJECTIVE_MAX_CHARS = 500;
1283
+ const CONTROLLER_ROUND_ORIGIN_ACCEPTANCE_MAX_ITEMS = 16;
1284
+ const CONTROLLER_ROUND_ACCEPTANCE_CRITERION_MAX_CHARS = 1_000;
1285
+
1286
+ export interface ControllerRoundContextSnapshot {
1287
+ repoId: string;
1288
+ relayScopeId: string;
1289
+ originWorkId: string;
1290
+ requirement?: {
1291
+ requirementId: string;
1292
+ state: string;
1293
+ outcomeStatement: string;
1294
+ };
1295
+ works: Array<{
1296
+ workId: string;
1297
+ status: string;
1298
+ phase: string;
1299
+ updatedAt: string;
1300
+ objective: string;
1301
+ acceptanceCriteria: string[];
1302
+ }>;
1303
+ handoffs: Array<{
1304
+ id: string;
1305
+ status: string;
1306
+ workId?: string;
1307
+ title: string;
1308
+ reason: string;
1309
+ }>;
1310
+ round: {
1311
+ count: number;
1312
+ maxRounds: number;
1313
+ repeatedStateCount: number;
1314
+ maxRepeatedState: number;
1315
+ consecutiveFailures: number;
1316
+ maxFailures: number;
1317
+ };
1318
+ recoveryReason?: string;
1319
+ assistantContext?: string;
1320
+ executionQualitySignals?: ExecutionQualitySignal[];
1321
+ }
1322
+
1323
+ /** Provider-neutral launch context. ControllerHost adapters decide how to render it. */
1324
+ export function readControllerRoundContextSnapshot(
1325
+ options: ControllerRoundRelayStoreOptions,
1326
+ record: ControllerRoundRelayRecord,
1327
+ ): ControllerRoundContextSnapshot {
1328
+ const requirement = requirementForRelay(options, record.requirementId);
1329
+ const relevantWorks = relevantWork(options, record);
1330
+ const works = relevantWorks.slice(0, 8);
1331
+ const workIds = new Set(relevantWorks.map((work) => work.workId));
1332
+ const handoffs = listHandoffItems({ controllerHome: options.controllerHome, repoId: options.repoId, status: 'active', limit: 100 })
1333
+ .filter((handoff) => !handoff.workId || workIds.has(handoff.workId))
1334
+ .slice(0, 8);
1335
+ return {
1336
+ repoId: record.repoId,
1337
+ relayScopeId: record.relayScopeId,
1338
+ originWorkId: record.originWorkId,
1339
+ assistantContext: options.prepareAssistantContext?.(record.originWorkId),
1340
+ executionQualitySignals: pendingQualitySignals(record),
1341
+ ...(requirement ? {
1342
+ requirement: {
1343
+ requirementId: requirement.requirementId,
1344
+ state: requirement.state,
1345
+ outcomeStatement: requirement.outcomeStatement.slice(0, 800),
1346
+ },
1347
+ } : {}),
1348
+ works: works.map((work) => {
1349
+ const origin = work.workId === record.originWorkId;
1350
+ return {
1351
+ workId: work.workId,
1352
+ status: work.status,
1353
+ phase: work.phase,
1354
+ updatedAt: work.updatedAt,
1355
+ objective: work.objective.slice(0, origin
1356
+ ? CONTROLLER_ROUND_ORIGIN_OBJECTIVE_MAX_CHARS
1357
+ : CONTROLLER_ROUND_RELATED_OBJECTIVE_MAX_CHARS),
1358
+ acceptanceCriteria: origin
1359
+ ? work.acceptanceCriteria
1360
+ .slice(0, CONTROLLER_ROUND_ORIGIN_ACCEPTANCE_MAX_ITEMS)
1361
+ .map((criterion) => criterion.slice(0, CONTROLLER_ROUND_ACCEPTANCE_CRITERION_MAX_CHARS))
1362
+ : [],
1363
+ };
1364
+ }),
1365
+ handoffs: handoffs.map((handoff) => ({
1366
+ id: handoff.id,
1367
+ status: handoff.status,
1368
+ ...(handoff.workId ? { workId: handoff.workId } : {}),
1369
+ title: handoff.title,
1370
+ reason: handoff.reason.slice(0, 300),
1371
+ })),
1372
+ round: {
1373
+ count: record.roundCount,
1374
+ maxRounds: record.maxRounds,
1375
+ repeatedStateCount: record.repeatedStateCount,
1376
+ maxRepeatedState: record.maxRepeatedState,
1377
+ consecutiveFailures: record.consecutiveFailures,
1378
+ maxFailures: record.maxFailures,
1379
+ },
1380
+ ...(['CONTROLLER_RELAY_ROUND_UNCLOSED', 'CONTROLLER_RELAY_CLAIMED_ROUND_UNCLOSED'].includes(record.lastError ?? '')
1381
+ ? { recoveryReason: record.lastError }
1382
+ : {}),
1383
+ };
1384
+ }