@moretea-labs/forge 1.8.0 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (695) hide show
  1. package/AGENTS.md +73 -7
  2. package/CHANGELOG.md +34 -0
  3. package/CLAUDE.md +17 -2
  4. package/CONTRIBUTING.md +3 -2
  5. package/README.en.md +24 -3
  6. package/README.md +10 -6
  7. package/README.zh-CN.md +5 -3
  8. package/THIRD_PARTY_NOTICES.md +1 -2
  9. package/adapters/chatgpt/browser-delivery-host.ts +7 -1
  10. package/adapters/chatgpt/browser-delivery-runtime.ts +531 -56
  11. package/adapters/chatgpt/controller-binding-store.ts +101 -22
  12. package/adapters/chatgpt/controller-round-host.ts +15 -25
  13. package/adapters/chatgpt/controller-round-settlement-store.ts +58 -4
  14. package/adapters/chatgpt/provider-delivery.ts +229 -2
  15. package/adapters/chatgpt/work-conversation-binding-store.ts +99 -54
  16. package/adapters/computer/chatgpt-extension-target.ts +708 -0
  17. package/adapters/computer/chrome-extension/background.js +541 -0
  18. package/adapters/computer/chrome-extension/content.js +402 -0
  19. package/adapters/computer/chrome-extension/core.js +24 -0
  20. package/adapters/computer/chrome-extension/manifest.json +32 -0
  21. package/adapters/computer/desktop-operator-discovery.ts +11 -31
  22. package/adapters/computer/desktop-operator-provider.ts +18 -6
  23. package/adapters/computer/interaction-target-authority.ts +178 -24
  24. package/adapters/mcp/access-mode.ts +18 -39
  25. package/adapters/mcp/auth.ts +2 -7
  26. package/adapters/mcp/instructions.ts +6 -5
  27. package/adapters/mcp/multi-repository.ts +10 -47
  28. package/adapters/mcp/oauth.ts +73 -0
  29. package/adapters/mcp/paths.ts +4 -1
  30. package/adapters/mcp/runtime-gateway/bounded-result-runtime.ts +62 -0
  31. package/adapters/mcp/runtime-gateway/cognition-settlement.ts +201 -0
  32. package/adapters/mcp/runtime-gateway/context-adapter.ts +490 -113
  33. package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +83 -0
  34. package/adapters/mcp/runtime-gateway/controller-authority-adapter.ts +20 -5
  35. package/adapters/mcp/runtime-gateway/core-capability-adapter.ts +258 -0
  36. package/adapters/mcp/runtime-gateway/engineering-preconditions.ts +10 -27
  37. package/adapters/mcp/runtime-gateway/engineering-tool-contract.ts +15 -0
  38. package/adapters/mcp/runtime-gateway/execution-tools.ts +14 -54
  39. package/adapters/mcp/runtime-gateway/gateway-execution-adapter.ts +2 -1
  40. package/adapters/mcp/runtime-gateway/plugin-adapter.ts +132 -47
  41. package/adapters/mcp/runtime-gateway/process-tools.ts +173 -23
  42. package/adapters/mcp/runtime-gateway/protected-computer-adapter.ts +16 -4
  43. package/adapters/mcp/runtime-gateway/recovery-adapter.ts +69 -52
  44. package/adapters/mcp/runtime-gateway/repository-compat-adapter.ts +4 -35
  45. package/adapters/mcp/runtime-gateway/result-adapter.ts +10 -4
  46. package/adapters/mcp/runtime-gateway/router.ts +19 -507
  47. package/adapters/mcp/runtime-gateway/routing-policy.ts +506 -0
  48. package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +15 -66
  49. package/adapters/mcp/runtime-gateway/runtime-readiness-observation.ts +218 -0
  50. package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +123 -178
  51. package/adapters/mcp/runtime-gateway/runtime-tools.ts +11 -5
  52. package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +37 -1
  53. package/adapters/mcp/runtime-gateway/shared-adapter.ts +20 -0
  54. package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +416 -270
  55. package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +140 -0
  56. package/adapters/mcp/runtime-gateway/work-adapter.ts +262 -3123
  57. package/adapters/mcp/runtime-gateway/work-controller-operations.ts +257 -0
  58. package/adapters/mcp/runtime-gateway/work-detail-projection.ts +60 -0
  59. package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +229 -0
  60. package/adapters/mcp/runtime-gateway/work-learning-operations.ts +145 -0
  61. package/adapters/mcp/runtime-gateway/work-list-projection.ts +38 -0
  62. package/adapters/mcp/runtime-gateway/work-plan-operations.ts +382 -0
  63. package/adapters/mcp/runtime-gateway/work-repair-adapter.ts +308 -0
  64. package/adapters/mcp/runtime-gateway/work-requirement-operations.ts +148 -0
  65. package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +320 -0
  66. package/adapters/mcp/runtime-gateway/work-workflow-operations.ts +113 -0
  67. package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +180 -0
  68. package/adapters/mcp/server.ts +91 -26
  69. package/adapters/mcp/setup.ts +5 -13
  70. package/adapters/mcp/tool-mapping/access-tools.ts +0 -6
  71. package/adapters/mcp/tool-mapping/legacy-tool-service.ts +703 -3092
  72. package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +5 -10
  73. package/adapters/mcp/tool-mapping/repository-tools.ts +170 -213
  74. package/adapters/mcp/toolset-names.ts +11 -37
  75. package/adapters/mcp/toolset.ts +19 -92
  76. package/{src/cli → adapters}/mcp/transports/AGENTS.md +5 -5
  77. package/{src/cli → adapters}/mcp/transports/CLAUDE.md +5 -5
  78. package/adapters/mcp/transports/http-observation.ts +377 -0
  79. package/adapters/mcp/transports/http-post-lifecycle.ts +19 -0
  80. package/adapters/mcp/transports/http.ts +200 -683
  81. package/adapters/mcp/transports/oauth-http.ts +322 -0
  82. package/adapters/mcp/transports/session-registry.ts +6 -0
  83. package/adapters/mcp/tunnels/openai-secure-tunnel.ts +89 -5
  84. package/adapters/mcp/types.ts +0 -6
  85. package/assets/hooks/lib/session-state.sh +0 -38
  86. package/assets/hooks/post-bash.sh +1 -1
  87. package/assets/hooks/post-tool-observer.sh +0 -5
  88. package/assets/hooks/prompt-guard.sh +0 -55
  89. package/assets/partials/04-project-structure.partial.md +1 -1
  90. package/assets/partials/07-footer.partial.md +1 -1
  91. package/assets/partials-agents/02-operating-mode.partial.md +1 -1
  92. package/assets/partials-agents/08-deep-docs.partial.md +1 -1
  93. package/assets/plugin-registry.v1.json +6 -4
  94. package/assets/reference-configs/agentic-development-flow.md +26 -1
  95. package/assets/reference-configs/external-tooling.md +0 -76
  96. package/assets/reference-configs/global-working-rules.md +19 -0
  97. package/assets/reference-configs/release-deploy.md +1 -1
  98. package/assets/templates/helpers/architecture-queue.sh +1 -1
  99. package/assets/templates/helpers/check-agent-tooling.sh +1 -327
  100. package/assets/templates/helpers/check-task-workflow.sh +1 -1
  101. package/assets/templates/helpers/ensure-task-workflow.sh +0 -14
  102. package/assets/templates/helpers/migrate-project-template.sh +2 -2
  103. package/assets/workflow-contract.v1.json +0 -2
  104. package/docs/README.md +5 -2
  105. package/docs/architecture/CURRENT.md +205 -51
  106. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +14 -5
  107. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +6 -3
  108. package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +42 -0
  109. package/docs/architecture/decisions/20260907-assistant-learning-loop.md +2 -0
  110. package/docs/architecture/decisions/20260907-controller-round-transition-policy.md +19 -4
  111. package/docs/architecture/decisions/20260916-workflow-supervisor-authority.md +249 -0
  112. package/docs/architecture/decisions/20260917-cognitive-memory-foundation.md +82 -0
  113. package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +234 -0
  114. package/docs/architecture/decisions/20260919-autonomous-work-liveness.md +62 -0
  115. package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +74 -0
  116. package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +342 -0
  117. package/docs/architecture/decisions/20261005-forge-v3-desktop-client-boundary.md +77 -0
  118. package/docs/architecture/decisions/20261009-exact-code-context-discovery.md +19 -0
  119. package/docs/architecture/decisions/20261009-generated-xcode-cache-retention.md +37 -0
  120. package/docs/architecture/decisions/20261011-autonomous-continuation-convergence.md +597 -0
  121. package/docs/architecture/decisions/20261017-edit-session-finalization-reconciliation.md +17 -0
  122. package/docs/architecture/decisions/20261019-retire-host-skills.md +16 -0
  123. package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +1 -1
  124. package/docs/architecture/domains/public-surface.md +3 -5
  125. package/docs/architecture/domains/verification.md +6 -5
  126. package/docs/architecture/domains/workflow-engine.md +6 -0
  127. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +3 -3
  128. package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +4 -4
  129. package/docs/architecture/reviews/20261015-code-search-runtime-acceptance.md +75 -0
  130. package/docs/forge-plugin-management.md +16 -4
  131. package/docs/operations/controller-browser-plugin.md +23 -0
  132. package/docs/operations/controller-performance-and-502.md +17 -0
  133. package/docs/operations/controller-reliability-runbook.md +2 -2
  134. package/docs/operations/features.md +6 -7
  135. package/docs/operations/features.zh-CN.md +1 -1
  136. package/docs/operations/github-repository.md +4 -4
  137. package/docs/operations/github-semantic-release-projection.md +20 -0
  138. package/docs/operations/homebrew.md +2 -2
  139. package/docs/operations/mcp-tool-exposure.md +13 -52
  140. package/docs/operations/releasing.md +2 -0
  141. package/docs/operations/runtime-performance-diagnostics.md +8 -0
  142. package/docs/operations/standalone-disaster-recovery.md +15 -7
  143. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  144. package/docs/tutorials/03-first-repository-task.md +2 -0
  145. package/docs/wiki/Computer-and-Plugins.md +78 -0
  146. package/docs/wiki/Computer-and-Plugins.zh-CN.md +88 -0
  147. package/docs/wiki/Home.md +16 -2
  148. package/docs/wiki/Implementation.md +25 -13
  149. package/docs/wiki/Installation.md +28 -32
  150. package/docs/wiki/Integrations.md +4 -0
  151. package/docs/wiki/Quick-Start.md +35 -22
  152. package/docs/wiki/Releases-and-Upgrades.md +15 -25
  153. package/docs/wiki/_Sidebar.md +5 -1
  154. package/package.json +25 -25
  155. package/packages/kernel/cognition/api/index.ts +6 -0
  156. package/packages/kernel/cognition/application/activation-service.ts +451 -0
  157. package/packages/kernel/cognition/application/consolidation-service.ts +185 -0
  158. package/packages/kernel/cognition/application/legacy-experience-adapter.ts +38 -0
  159. package/packages/kernel/cognition/application/memory-service.ts +64 -0
  160. package/packages/kernel/cognition/domain/learning.ts +106 -0
  161. package/packages/kernel/cognition/domain/memory.ts +254 -0
  162. package/packages/kernel/controller/api/index.ts +1 -0
  163. package/packages/kernel/controller/application/continuation-service.ts +281 -0
  164. package/packages/kernel/controller/application/controller-round-service.ts +12 -0
  165. package/packages/kernel/controller/domain/controller-round-transition-policy.ts +242 -21
  166. package/packages/kernel/controller/domain/controller-round.ts +4 -1
  167. package/packages/kernel/controller/domain/execution-quality.ts +3 -1
  168. package/packages/kernel/controller/infrastructure/controller-round-store.ts +689 -103
  169. package/packages/kernel/controller/infrastructure/controller-session-store.ts +348 -97
  170. package/packages/kernel/controller/ports/controller-host.ts +2 -0
  171. package/packages/kernel/identity/api/index.ts +10 -0
  172. package/packages/kernel/identity/domain/activity-projection.ts +43 -0
  173. package/packages/kernel/identity/domain/capability-broker.ts +60 -0
  174. package/packages/kernel/identity/domain/grant.ts +72 -0
  175. package/packages/kernel/identity/domain/owned-resource.ts +116 -0
  176. package/packages/kernel/identity/domain/user-request.ts +93 -0
  177. package/packages/kernel/identity/infrastructure/activity-projection-store.ts +87 -0
  178. package/packages/kernel/identity/infrastructure/capability-broker.ts +81 -0
  179. package/packages/kernel/identity/infrastructure/grant-store.ts +284 -0
  180. package/packages/kernel/identity/infrastructure/owned-resource-store.ts +259 -0
  181. package/packages/kernel/identity/infrastructure/user-request-store.ts +182 -0
  182. package/packages/kernel/scheduler/api/index.ts +0 -2
  183. package/packages/kernel/scheduler/application/eligibility.ts +4 -1
  184. package/packages/kernel/scheduler/application/schedule-service.ts +3 -0
  185. package/packages/kernel/scheduler/application/settlement.ts +0 -15
  186. package/packages/kernel/scheduler/domain/schedule.ts +87 -0
  187. package/packages/kernel/scheduler/infrastructure/schedule-store.ts +166 -143
  188. package/packages/kernel/work/api/index.ts +3 -2
  189. package/packages/kernel/work/application/work-service.ts +18 -42
  190. package/packages/kernel/work/domain/current-task-lineage.ts +104 -0
  191. package/packages/kernel/work/domain/engineering-design.ts +10 -5
  192. package/packages/kernel/work/domain/engineering-profile.ts +8 -12
  193. package/packages/kernel/work/domain/evaluation-promotion-receipt.ts +186 -0
  194. package/packages/kernel/work/domain/execution-concurrency.ts +19 -26
  195. package/packages/kernel/work/domain/execution-snapshot.ts +0 -31
  196. package/packages/kernel/work/domain/implementation-review.ts +14 -331
  197. package/packages/kernel/work/domain/objective-graph.ts +119 -0
  198. package/packages/kernel/work/domain/project-engineering-contract.ts +23 -1
  199. package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
  200. package/packages/kernel/work/domain/state-machine.ts +34 -65
  201. package/packages/kernel/work/domain/types.ts +57 -51
  202. package/packages/kernel/work/infrastructure/work-contract-store.ts +847 -729
  203. package/packages/kernel/work/ports/work-contract-store.ts +5 -2
  204. package/packages/plugin-runtime/browser/provider-registry.ts +12 -36
  205. package/packages/plugin-runtime/browser/session-authority.ts +2 -1
  206. package/packages/plugin-runtime/computer/chatgpt-identity.ts +24 -0
  207. package/packages/plugin-runtime/computer/conversation-target.ts +153 -0
  208. package/packages/plugin-runtime/computer/index.ts +2 -0
  209. package/packages/plugin-runtime/computer/target-authority.ts +26 -1
  210. package/packages/plugin-runtime/external/unix-jsonl-transport.ts +142 -18
  211. package/packages/protocols/computer/contract.ts +27 -1
  212. package/scripts/activate-source-baseline.ts +7 -9
  213. package/scripts/assemble-template.ts +11 -72
  214. package/scripts/canary-controller-semantic-wait.ts +12 -24
  215. package/scripts/check-mcp-compatibility.ts +94 -201
  216. package/scripts/check-package-identity.mjs +0 -5
  217. package/scripts/check-runtime-architecture.mjs +921 -239
  218. package/scripts/forge-local-recovery-helper.mjs +93 -15
  219. package/scripts/init-project.sh +0 -46
  220. package/scripts/install-ios-hid-toolchain.sh +60 -0
  221. package/scripts/install-standalone-recovery.ts +1 -1
  222. package/scripts/lib/project-init-lib.sh +6 -3
  223. package/scripts/migrate-windows-wsl-controller-home.ts +2 -0
  224. package/scripts/open-source-audit-allowlist.txt +1 -3
  225. package/scripts/run-bounded-command.mjs +5 -0
  226. package/scripts/run-governed-gate.ts +2 -1
  227. package/scripts/run-mobile-monitor-bridge.ts +0 -1
  228. package/scripts/smoke-control-plane-failure-domain.ts +94 -0
  229. package/scripts/smoke-ios-physical-device.ts +38 -10
  230. package/scripts/smoke-mcp-http-runtime.ts +12 -8
  231. package/scripts/smoke-mcp-tool-surface.ts +27 -41
  232. package/scripts/smoke-runtime-recovery.ts +12 -43
  233. package/scripts/smoke-schedule-engine.ts +23 -15
  234. package/scripts/smoke-unattended-transport-recovery.ts +223 -0
  235. package/scripts/smoke-user-request-continuation-restart.ts +232 -0
  236. package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +95 -0
  237. package/scripts/smoke-workflow-supervisor-process-restart.ts +276 -0
  238. package/scripts/smoke-workflow-supervisor-restart-recovery.ts +150 -0
  239. package/scripts/smoke-workflow-supervisor.ts +103 -0
  240. package/scripts/sync-generated-authority.mjs +0 -1
  241. package/scripts/verify-forge-runtime.sh +2 -1
  242. package/src/cli/agent-jobs/executor-health.ts +12 -35
  243. package/src/cli/agent-jobs/integration.ts +20 -51
  244. package/src/cli/agent-jobs/job-manager.ts +7 -3
  245. package/src/cli/chatgpt-browser/file-policy.ts +2 -2
  246. package/src/cli/commands/brain-assistant.ts +27 -13
  247. package/src/cli/commands/brain.ts +13 -15
  248. package/src/cli/commands/chatgpt.ts +1 -163
  249. package/src/cli/commands/doctor.ts +1 -143
  250. package/src/cli/commands/global-runtime.ts +3 -196
  251. package/src/cli/commands/init-hook.ts +6 -23
  252. package/src/cli/commands/init.ts +3 -252
  253. package/src/cli/commands/mcp.ts +0 -5
  254. package/src/cli/commands/prompt-guard-decision.ts +0 -2
  255. package/src/cli/commands/recovery.ts +137 -135
  256. package/src/cli/commands/repository.ts +17 -1
  257. package/src/cli/commands/runtime.ts +3 -3
  258. package/src/cli/commands/setup-execution.ts +41 -0
  259. package/src/cli/commands/setup-profile.ts +8 -0
  260. package/src/cli/controller/check-runner.ts +13 -2
  261. package/src/cli/controller/completion-backlog.ts +3 -3
  262. package/src/cli/controller/completion-orchestrator.ts +5 -44
  263. package/src/cli/controller/completion-target.ts +3 -72
  264. package/src/cli/controller/composite-operations.ts +1 -1
  265. package/src/cli/controller/context/instruction-context.ts +1 -1
  266. package/src/cli/controller/context/known-paths.ts +2 -2
  267. package/src/cli/controller/context/query-planning.ts +11 -4
  268. package/src/cli/controller/context/rule-resolution.ts +2 -2
  269. package/src/cli/controller/context/source-materializer.ts +83 -102
  270. package/src/cli/controller/context/structural-provider.ts +84 -0
  271. package/src/cli/controller/context/structural.ts +6 -6
  272. package/src/cli/controller/context/types.ts +8 -9
  273. package/src/cli/controller/context-pack.ts +107 -84
  274. package/src/cli/controller/execution-completion.ts +4 -7
  275. package/src/cli/controller/execution-policy.ts +36 -153
  276. package/src/cli/controller/governance.ts +2 -3
  277. package/src/cli/controller/issue-store.ts +4 -99
  278. package/src/cli/controller/launch-agents.ts +59 -18
  279. package/src/cli/controller/operational-plan.ts +25 -52
  280. package/src/cli/controller/runtime-config.ts +5 -1
  281. package/src/cli/controller/stuck-state-migration.ts +2 -3
  282. package/src/cli/controller/systemd-user.ts +42 -10
  283. package/src/cli/editing/edit-session.ts +18 -12
  284. package/src/cli/github/github.ts +4 -4
  285. package/src/cli/github/semantic-projection.ts +174 -0
  286. package/src/cli/hook/prompt-intents.ts +0 -9
  287. package/src/cli/index.ts +1 -43
  288. package/src/cli/local-bridge/connector-freshness.ts +1 -12
  289. package/src/cli/local-bridge/console-automations.ts +19 -14
  290. package/src/cli/local-bridge/console-view-models.ts +4 -13
  291. package/src/cli/local-bridge/facade-api.ts +64 -193
  292. package/src/cli/local-bridge/server.ts +24 -147
  293. package/src/cli/local-bridge/types.ts +0 -1
  294. package/src/cli/mcp/oauth-client-retirement.ts +74 -0
  295. package/src/cli/repositories/command-classifier.ts +0 -9
  296. package/src/cli/repositories/command-executor.ts +29 -23
  297. package/src/cli/repositories/command-scope.ts +37 -18
  298. package/src/cli/repositories/controller-home.ts +48 -0
  299. package/src/cli/repositories/ephemeral-workspace.ts +20 -30
  300. package/src/cli/repositories/git-command-executor.ts +2 -2
  301. package/src/cli/repositories/locks.ts +20 -0
  302. package/src/cli/repositories/registry.ts +79 -12
  303. package/src/cli/repositories/safe-patch.ts +2 -12
  304. package/src/cli/repositories/structured-git.ts +101 -5
  305. package/src/cli/repository/inspector.ts +185 -343
  306. package/src/cli/repository/session-cache.ts +92 -6
  307. package/src/effects/process-runner.ts +17 -1
  308. package/src/runtime/context/assistant-context.ts +20 -7
  309. package/src/runtime/context/assistant-work-context.ts +89 -27
  310. package/src/runtime/context/automatic-learning.ts +565 -0
  311. package/src/runtime/context/cognitive-attention.ts +173 -0
  312. package/src/runtime/context/cognitive-skill-canary.ts +158 -0
  313. package/src/runtime/context/context-closure.ts +46 -9
  314. package/src/runtime/context/context-pack-process.ts +103 -0
  315. package/src/runtime/context/context-plane.ts +1 -9
  316. package/src/runtime/context/direct-controller-learning.ts +298 -0
  317. package/src/runtime/context/generic-lsp-provider.ts +1 -1
  318. package/src/runtime/context/semantic-navigation-contract.ts +16 -0
  319. package/src/runtime/context/semantic-navigation.ts +7 -35
  320. package/src/runtime/context/typescript-navigation-process.ts +145 -0
  321. package/src/runtime/context/typescript-navigation.ts +89 -3
  322. package/src/runtime/control-plane/cleanup-artifact-retention.ts +215 -12
  323. package/src/runtime/control-plane/concurrency/work-execution-concurrency.ts +9 -47
  324. package/src/runtime/control-plane/execution/controller-authority-recovery.ts +34 -4
  325. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +29 -20
  326. package/src/runtime/control-plane/execution/execution-identity.ts +23 -3
  327. package/src/runtime/control-plane/execution/repository-work-attribution.ts +7 -30
  328. package/src/runtime/control-plane/execution/retained-work-resume.ts +206 -199
  329. package/src/runtime/control-plane/execution/session-store.ts +31 -1
  330. package/src/runtime/control-plane/execution/validation.ts +6 -8
  331. package/src/runtime/control-plane/execution/work-authority-reconciler.ts +13 -42
  332. package/src/runtime/control-plane/execution/work-check-evidence.ts +137 -0
  333. package/src/runtime/control-plane/execution/work-completion-authority.ts +30 -85
  334. package/src/runtime/control-plane/execution/work-delivery-service.ts +616 -0
  335. package/src/runtime/control-plane/execution/work-evidence-policy.ts +12 -50
  336. package/src/runtime/control-plane/execution/work-execution-support.ts +50 -149
  337. package/src/runtime/control-plane/execution/work-handle-authority.ts +197 -110
  338. package/src/runtime/control-plane/execution/work-handle-store.ts +72 -44
  339. package/src/runtime/control-plane/execution/work-head-settlement.ts +53 -20
  340. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +52 -29
  341. package/src/runtime/control-plane/execution/work-operation-service.ts +60 -34
  342. package/src/runtime/control-plane/execution/work-preparation-service.ts +231 -267
  343. package/src/runtime/control-plane/execution/work-process-evidence.ts +2 -2
  344. package/src/runtime/control-plane/execution/work-remote-delivery.ts +2 -3
  345. package/src/runtime/control-plane/execution/work-revision-diff.ts +39 -0
  346. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +201 -199
  347. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +33 -85
  348. package/src/runtime/control-plane/execution/work-verification-service.ts +406 -50
  349. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +6 -2
  350. package/src/runtime/control-plane/facade/capability-registry.ts +364 -37
  351. package/src/runtime/control-plane/facade/check-normalization.ts +3 -9
  352. package/src/runtime/control-plane/facade/codex-delegation.ts +10 -11
  353. package/src/runtime/control-plane/facade/controller-connection.ts +135 -0
  354. package/src/runtime/control-plane/facade/goal-workloop-access.ts +1 -20
  355. package/src/runtime/control-plane/facade/goal-workloop.ts +281 -1753
  356. package/src/runtime/control-plane/facade/handoff-inbox-application.ts +12 -25
  357. package/src/runtime/control-plane/facade/handoff-inbox-store.ts +256 -52
  358. package/src/runtime/control-plane/facade/index.ts +0 -1
  359. package/src/runtime/control-plane/facade/operation-digest.ts +12 -15
  360. package/src/runtime/control-plane/facade/plan-contract-store.ts +451 -991
  361. package/src/runtime/control-plane/facade/policy-gate.ts +0 -23
  362. package/src/runtime/control-plane/facade/repository-work-admission.ts +12 -22
  363. package/src/runtime/control-plane/facade/requirement-authority.ts +102 -199
  364. package/src/runtime/control-plane/facade/requirement-board.ts +3 -3
  365. package/src/runtime/control-plane/facade/rh-work-operation-contract.ts +51 -14
  366. package/src/runtime/control-plane/facade/self-healing-loop.ts +18 -53
  367. package/src/runtime/control-plane/facade/suggested-actions.ts +15 -18
  368. package/src/runtime/control-plane/facade/types.ts +49 -108
  369. package/src/runtime/control-plane/facade/work-continuation.ts +9 -9
  370. package/src/runtime/control-plane/generated-cache-retention.ts +206 -59
  371. package/src/runtime/control-plane/global-scheduler/autonomous-continuation.ts +248 -0
  372. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +1 -1
  373. package/src/runtime/control-plane/global-scheduler/maintenance.ts +256 -117
  374. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -12
  375. package/src/runtime/control-plane/global-scheduler/scheduler.ts +66 -40
  376. package/src/runtime/control-plane/global-scheduler/wake-signal.ts +42 -2
  377. package/src/runtime/control-plane/governance/access-policy.ts +19 -2
  378. package/src/runtime/control-plane/governance/authorization.ts +20 -3
  379. package/src/runtime/control-plane/governance/external-effects.ts +1 -1
  380. package/src/runtime/control-plane/launcher/chatgpt-round-continuation.ts +63 -300
  381. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +29 -300
  382. package/src/runtime/control-plane/launcher/local-conversation-controller.ts +238 -0
  383. package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +5 -2
  384. package/src/runtime/control-plane/launcher/thin-launcher.ts +96 -240
  385. package/src/runtime/control-plane/persistence/cognition-store.ts +820 -0
  386. package/src/runtime/control-plane/persistence/experience-store.ts +162 -29
  387. package/src/runtime/control-plane/persistence/requirement-store.ts +216 -74
  388. package/src/runtime/control-plane/persistence/sqlite-store.ts +113 -6
  389. package/src/runtime/control-plane/release-retention.ts +135 -47
  390. package/src/runtime/control-plane/repository-namespace-retention.ts +2 -2
  391. package/src/runtime/control-plane/runtime-cleanup.ts +295 -107
  392. package/src/runtime/control-plane/workspace/project-onboarding.ts +80 -0
  393. package/src/runtime/control-plane/workspace/semantic-sync-service.ts +19 -5
  394. package/src/runtime/control-plane/workspace/workspace-store.ts +169 -0
  395. package/src/runtime/diagnostics/incident-repair.ts +313 -146
  396. package/src/runtime/diagnostics/mcp-timing.ts +114 -0
  397. package/src/runtime/evidence/artifact-store.ts +158 -40
  398. package/src/runtime/evidence/evidence-store.ts +112 -8
  399. package/src/runtime/evidence/output-handle-index.ts +225 -0
  400. package/src/runtime/evidence/result-store.ts +158 -44
  401. package/src/runtime/evidence/sensitive-output.ts +22 -2
  402. package/src/runtime/execution/jobs/child-reference.ts +11 -0
  403. package/src/runtime/execution/jobs/receipt-recovery.ts +1 -11
  404. package/src/runtime/execution/jobs/store.ts +3 -4
  405. package/src/runtime/execution/managed-workspace.ts +57 -0
  406. package/src/runtime/execution/process-runtime/check-facade.ts +1 -1
  407. package/src/runtime/execution/process-runtime/check-result-retention.ts +2 -2
  408. package/src/runtime/execution/process-runtime/check-result.ts +105 -1
  409. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +18 -8
  410. package/src/runtime/execution/process-runtime/command-facade.ts +82 -30
  411. package/src/runtime/execution/process-runtime/gc.ts +15 -2
  412. package/src/runtime/execution/process-runtime/handle-index.ts +176 -0
  413. package/src/runtime/execution/process-runtime/host-command-lane.ts +139 -0
  414. package/src/runtime/execution/process-runtime/index.ts +3 -0
  415. package/src/runtime/execution/process-runtime/lightweight-managed.ts +251 -72
  416. package/src/runtime/execution/process-runtime/persisted-check.ts +3 -3
  417. package/src/runtime/execution/process-runtime/process-runner-entry.ts +47 -5
  418. package/src/runtime/execution/process-runtime/process-scope.ts +160 -0
  419. package/src/runtime/execution/process-runtime/resource-claims.ts +65 -61
  420. package/src/runtime/execution/process-runtime/runtime.ts +55 -18
  421. package/src/runtime/execution/process-runtime/store.ts +30 -2
  422. package/src/runtime/execution/process-runtime/types.ts +6 -1
  423. package/src/runtime/execution/thin-harness/execution-router.ts +16 -16
  424. package/src/runtime/execution/thin-harness/fast-executor.ts +2 -2
  425. package/src/runtime/execution/thin-harness/search-worker.ts +2 -2
  426. package/src/runtime/execution/workers/executor.ts +3 -8
  427. package/src/runtime/maintenance/cleanup.ts +41 -5
  428. package/src/runtime/platform/service-inventory.ts +130 -0
  429. package/src/runtime/platform/service-manager.ts +8 -0
  430. package/src/runtime/plugins/action-application.ts +97 -29
  431. package/src/runtime/plugins/app-store-connect-adapter.ts +290 -18
  432. package/src/runtime/plugins/browser-adapter.ts +1124 -333
  433. package/src/runtime/plugins/browser-automation-service.ts +23 -3
  434. package/src/runtime/plugins/browser-macos-bridge.ts +431 -137
  435. package/src/runtime/plugins/browser-manifest-surface.ts +47 -12
  436. package/src/runtime/plugins/browser-native-session-service.ts +63 -13
  437. package/src/runtime/plugins/browser-registration.ts +18 -0
  438. package/src/runtime/plugins/browser-runtime.ts +40 -36
  439. package/src/runtime/plugins/browser-session-authority.ts +2 -2
  440. package/src/runtime/plugins/browser-session-store.ts +46 -76
  441. package/src/runtime/plugins/capability-authorization-grants.ts +258 -62
  442. package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +228 -0
  443. package/src/runtime/plugins/computer-registration.ts +298 -20
  444. package/src/runtime/plugins/desktop-operator-external-policy.ts +36 -4
  445. package/src/runtime/plugins/desktop-operator-registration.ts +12 -4
  446. package/src/runtime/plugins/external-adapter.ts +42 -6
  447. package/src/runtime/plugins/external-registration.ts +21 -1
  448. package/src/runtime/plugins/github-adapter.ts +30 -3
  449. package/src/runtime/plugins/gmail-adapter.ts +189 -3
  450. package/src/runtime/plugins/google-calendar-adapter.ts +2 -2
  451. package/src/runtime/plugins/google-shared.ts +24 -12
  452. package/src/runtime/plugins/google-tasks-adapter.ts +2 -2
  453. package/src/runtime/plugins/ios/remote-xpc-hid.ts +70 -30
  454. package/src/runtime/plugins/ios-adapter.ts +46 -41
  455. package/src/runtime/plugins/ios-agent-device.ts +54 -4
  456. package/src/runtime/plugins/ios-device-ownership.ts +103 -0
  457. package/src/runtime/plugins/ios-physical-device.ts +497 -79
  458. package/src/runtime/plugins/lightweight-action.ts +208 -2
  459. package/src/runtime/plugins/local-system-adapter.ts +12 -100
  460. package/src/runtime/plugins/managed-process-adapter.ts +12 -5
  461. package/src/runtime/plugins/plugin-action-sidecar.ts +17 -13
  462. package/src/runtime/plugins/plugin-management-adapter.ts +1 -1
  463. package/src/runtime/plugins/store.ts +682 -435
  464. package/src/runtime/plugins/types.ts +24 -0
  465. package/src/runtime/plugins/xiaohongshu-publish.ts +16 -0
  466. package/src/runtime/projections/git-status-sampler.ts +2 -19
  467. package/src/runtime/projections/invalidation.ts +7 -3
  468. package/src/runtime/projections/materialized-view.ts +80 -36
  469. package/src/runtime/recovery/actions.ts +4 -4
  470. package/src/runtime/recovery/capability-action-application.ts +46 -45
  471. package/src/runtime/recovery/lifecycle-inventory.ts +30 -40
  472. package/src/runtime/recovery/maintenance-executor.ts +374 -123
  473. package/src/runtime/recovery/probe.ts +2 -2
  474. package/src/runtime/recovery/store.ts +29 -11
  475. package/src/runtime/release/release-coordinator.ts +312 -0
  476. package/src/runtime/release/release-session.ts +716 -0
  477. package/src/runtime/release/release-state-migration.ts +25 -0
  478. package/src/runtime/resources/leases/store.ts +56 -25
  479. package/src/runtime/root/computer-composition.ts +191 -6
  480. package/src/runtime/root/controller-progression-composition.ts +254 -0
  481. package/src/runtime/root/controller-round-composition.ts +20 -1
  482. package/src/runtime/root/deployment-topology.ts +106 -0
  483. package/src/runtime/root/entry.ts +4 -3
  484. package/src/runtime/root/known-good-recovery.ts +184 -0
  485. package/src/runtime/root/local-bridge.ts +0 -1
  486. package/src/runtime/root/mcp-transport.ts +111 -18
  487. package/src/runtime/root/ownership.ts +261 -9
  488. package/src/runtime/root/package-connector-service.ts +227 -29
  489. package/src/runtime/root/package-runtime-release.ts +117 -30
  490. package/src/runtime/root/package-runtime-service.ts +24 -24
  491. package/src/runtime/root/release-execution-canary.ts +57 -12
  492. package/src/runtime/root/release-loader.ts +90 -8
  493. package/src/runtime/root/release-manifest.ts +80 -40
  494. package/src/runtime/root/release-materialize.ts +372 -106
  495. package/src/runtime/root/release-store.ts +395 -36
  496. package/src/runtime/root/runtime-lane.ts +191 -0
  497. package/src/runtime/root/runtime.ts +160 -29
  498. package/src/runtime/root/scheduled-controller-composition.ts +29 -3
  499. package/src/runtime/root/service-runner.ts +2 -2
  500. package/src/runtime/root/service.ts +126 -44
  501. package/src/runtime/root/status.ts +48 -0
  502. package/src/runtime/root/types.ts +32 -9
  503. package/src/runtime/root/workflow-supervisor-composition.ts +681 -0
  504. package/src/runtime/root/workflow-supervisor-runtime.ts +564 -0
  505. package/src/runtime/root/write-fence.ts +50 -2
  506. package/src/runtime/safe-tooling/google-credential-store.ts +67 -22
  507. package/src/runtime/safe-tooling/google-oauth-broker.ts +25 -7
  508. package/src/runtime/safe-tooling/ios-development.ts +22 -8
  509. package/src/runtime/safe-tooling/job-summary.ts +1 -1
  510. package/src/runtime/safe-tooling/workspace-auth.ts +5 -2
  511. package/src/runtime/shared/atomic-text-file.ts +21 -0
  512. package/src/runtime/shared/bounded-child-supervisor.ts +9 -1
  513. package/src/runtime/shared/json-files.ts +8 -3
  514. package/src/runtime/shared/process-environment.ts +20 -9
  515. package/src/runtime/shared/response-budget.ts +1 -3
  516. package/src/runtime/standalone-recovery/controller-home-migration.ts +16 -24
  517. package/src/runtime/standalone-recovery/core.ts +3792 -417
  518. package/src/runtime/standalone-recovery/entry.ts +507 -45
  519. package/src/runtime/standalone-recovery/installer.ts +262 -148
  520. package/src/runtime/standalone-recovery/mcp-server.ts +29 -193
  521. package/src/runtime/standalone-recovery/operation-lock.ts +12 -2
  522. package/src/runtime/standalone-recovery/performance.ts +54 -16
  523. package/src/runtime/standalone-recovery/release.ts +53 -5
  524. package/src/runtime/standalone-recovery/service-labels.ts +5 -2
  525. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +5 -2
  526. package/src/runtime/workflow/schedules/browser-probe.ts +17 -6
  527. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +11 -3
  528. package/src/runtime/workflow/schedules/engine.ts +166 -178
  529. package/src/runtime/workflow/schedules/pending-activation.ts +11 -4
  530. package/src/runtime/workflow/schedules/work-continuation.ts +95 -11
  531. package/src/runtime/workflows/runtime.ts +15 -19
  532. package/src/testing/test-governance.ts +5 -1
  533. package/supervisor/chatgpt-conversation.ts +23 -0
  534. package/supervisor/client.ts +218 -0
  535. package/supervisor/control-plane.ts +963 -0
  536. package/supervisor/entry.ts +15 -0
  537. package/supervisor/forge-validators.ts +147 -0
  538. package/supervisor/index.ts +5 -0
  539. package/supervisor/native-browser-adapter.ts +1151 -0
  540. package/supervisor/native-messaging/host.ts +58 -0
  541. package/supervisor/native-messaging/manifest.ts +26 -0
  542. package/supervisor/native-messaging/protocol.ts +27 -0
  543. package/supervisor/paths.ts +18 -0
  544. package/supervisor/protocol.ts +250 -0
  545. package/supervisor/server.ts +412 -0
  546. package/supervisor/store.ts +1870 -0
  547. package/supervisor/tsconfig.json +8 -0
  548. package/supervisor/types.ts +261 -0
  549. package/SKILL.md +0 -427
  550. package/adapters/chatgpt/controller-host.ts +0 -119
  551. package/adapters/mcp/controller-round-compatibility.ts +0 -184
  552. package/adapters/mcp/frozen-client-semantic-compatibility.ts +0 -292
  553. package/adapters/mcp/runtime-gateway/work-compat-adapter.ts +0 -502
  554. package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +0 -1
  555. package/adapters/mcp/toolset-selection.ts +0 -41
  556. package/agents/openai.yaml +0 -4
  557. package/assets/skill-hooks.json +0 -41
  558. package/assets/skill-version.json +0 -189
  559. package/assets/skills/claude-review/SKILL.md +0 -231
  560. package/assets/skills/codex-review/SKILL.md +0 -103
  561. package/assets/templates/helpers/check-skill-version.ts +0 -193
  562. package/packages/kernel/progression/api/index.ts +0 -2
  563. package/packages/kernel/progression/application/projector.ts +0 -211
  564. package/packages/kernel/progression/domain/types.ts +0 -116
  565. package/packages/kernel/progression/index.ts +0 -1
  566. package/packages/kernel/scheduler/application/continuation-service.ts +0 -212
  567. package/packages/kernel/scheduler/domain/continuation.ts +0 -38
  568. package/packages/kernel/scheduler/infrastructure/continuation-dispatch-store.ts +0 -38
  569. package/packages/kernel/work/domain/admission-policy.ts +0 -34
  570. package/scripts/benchmark-background-check-overlap.ts +0 -228
  571. package/scripts/benchmark-computer-hotpath.ts +0 -255
  572. package/scripts/benchmark-controller-hotpath.ts +0 -212
  573. package/scripts/benchmark-controller-round2.ts +0 -191
  574. package/scripts/benchmark-ios-agent-device.ts +0 -511
  575. package/scripts/benchmark-mcp-host.sh +0 -117
  576. package/scripts/benchmark-mcp-transport.ts +0 -169
  577. package/scripts/benchmark-operational-memory-activation.ts +0 -235
  578. package/scripts/benchmark-operational-shadow.ts +0 -28
  579. package/scripts/benchmark-quality-harness.ts +0 -146
  580. package/scripts/benchmark-route-session-concurrency.ts +0 -915
  581. package/scripts/benchmark-thin-harness-gateway-ab.ts +0 -632
  582. package/scripts/benchmark-thin-harness-mcp-e2e.ts +0 -498
  583. package/scripts/benchmark-thin-harness.ts +0 -251
  584. package/scripts/benchmark-typescript-navigation.ts +0 -186
  585. package/scripts/benchmark-work-efficiency.ts +0 -334
  586. package/scripts/build-controller-ui.ts +0 -28
  587. package/scripts/check-npm-release.sh +0 -32
  588. package/scripts/check-public-release-surface.sh +0 -19
  589. package/scripts/check-release-published.sh +0 -57
  590. package/scripts/check-release-readiness.sh +0 -37
  591. package/scripts/check-release-version.mjs +0 -74
  592. package/scripts/check-skill-version.ts +0 -5
  593. package/scripts/check-tarball-install-smoke.sh +0 -150
  594. package/scripts/ensure-codegraph.sh +0 -16
  595. package/scripts/package-source-archive.sh +0 -147
  596. package/scripts/public-release-files.txt +0 -39
  597. package/scripts/public-release.ts +0 -174
  598. package/scripts/publish-release-tarball.sh +0 -27
  599. package/scripts/route-nl-vs-ts-eval.ts +0 -679
  600. package/scripts/run-skill-hook.ts +0 -244
  601. package/scripts/stage-runtime-release.ts +0 -44
  602. package/scripts/sync-codex-installed-copies.sh +0 -182
  603. package/src/cli/commands/tools.ts +0 -167
  604. package/src/cli/controller/progress.ts +0 -656
  605. package/src/cli/controller/work-mode.ts +0 -181
  606. package/src/cli/local-bridge/console-assets.ts +0 -17
  607. package/src/cli/local-bridge/dashboard.ts +0 -17
  608. package/src/cli/local-bridge/frontend/src/App.tsx +0 -10
  609. package/src/cli/local-bridge/frontend/src/components/Button.tsx +0 -1
  610. package/src/cli/local-bridge/frontend/src/components/DetailPane.tsx +0 -3
  611. package/src/cli/local-bridge/frontend/src/components/SearchField.tsx +0 -1
  612. package/src/cli/local-bridge/frontend/src/components/SectionHeader.tsx +0 -1
  613. package/src/cli/local-bridge/frontend/src/components/Segmented.tsx +0 -1
  614. package/src/cli/local-bridge/frontend/src/components/StatusText.tsx +0 -3
  615. package/src/cli/local-bridge/frontend/src/design/icons.tsx +0 -11
  616. package/src/cli/local-bridge/frontend/src/design/tokens.css +0 -8
  617. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +0 -153
  618. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +0 -37
  619. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +0 -183
  620. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +0 -2
  621. package/src/cli/local-bridge/frontend/src/features/system/SystemView.tsx +0 -2
  622. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +0 -4
  623. package/src/cli/local-bridge/frontend/src/lib/api.ts +0 -30
  624. package/src/cli/local-bridge/frontend/src/lib/format.ts +0 -4
  625. package/src/cli/local-bridge/frontend/src/lib/routes.ts +0 -7
  626. package/src/cli/local-bridge/frontend/src/main.tsx +0 -1
  627. package/src/cli/local-bridge/frontend/src/shell/AppShell.tsx +0 -1
  628. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +0 -2
  629. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +0 -4
  630. package/src/cli/local-bridge/frontend/src/styles.css +0 -28
  631. package/src/cli/local-bridge/frontend/src/types.ts +0 -15
  632. package/src/cli/local-bridge/frontend/src/vite-env.d.ts +0 -1
  633. package/src/cli/local-bridge/frontend/vite.config.ts +0 -27
  634. package/src/cli/local-bridge/ui-dist/app.css +0 -1
  635. package/src/cli/local-bridge/ui-dist/app.js +0 -11228
  636. package/src/cli/mcp/access-mode.ts +0 -2
  637. package/src/cli/mcp/access-tools.ts +0 -2
  638. package/src/cli/mcp/audit.ts +0 -2
  639. package/src/cli/mcp/auth.ts +0 -2
  640. package/src/cli/mcp/instructions.ts +0 -2
  641. package/src/cli/mcp/legacy-context.ts +0 -2
  642. package/src/cli/mcp/legacy-tool-service.ts +0 -2
  643. package/src/cli/mcp/multi-repository.ts +0 -2
  644. package/src/cli/mcp/oauth.ts +0 -2
  645. package/src/cli/mcp/openai-secure-tunnel.ts +0 -2
  646. package/src/cli/mcp/paths.ts +0 -2
  647. package/src/cli/mcp/policy.ts +0 -2
  648. package/src/cli/mcp/post-finalize-work-attribution.ts +0 -2
  649. package/src/cli/mcp/proxy-env.ts +0 -2
  650. package/src/cli/mcp/readiness-projection.ts +0 -2
  651. package/src/cli/mcp/redaction.ts +0 -2
  652. package/src/cli/mcp/repo.ts +0 -2
  653. package/src/cli/mcp/repository-tools.ts +0 -2
  654. package/src/cli/mcp/server.ts +0 -2
  655. package/src/cli/mcp/setup.ts +0 -2
  656. package/src/cli/mcp/tool-capability-cache.ts +0 -2
  657. package/src/cli/mcp/tool-contract.ts +0 -2
  658. package/src/cli/mcp/tools.ts +0 -2
  659. package/src/cli/mcp/toolset-names.ts +0 -2
  660. package/src/cli/mcp/toolset-selection.ts +0 -2
  661. package/src/cli/mcp/toolset.ts +0 -2
  662. package/src/cli/mcp/transports/http.ts +0 -2
  663. package/src/cli/mcp/transports/session-registry.ts +0 -2
  664. package/src/cli/mcp/transports/stdio.ts +0 -2
  665. package/src/cli/mcp/types.ts +0 -2
  666. package/src/cli/mcp/umbrella-tools.ts +0 -2
  667. package/src/cli/tools/codegraph.ts +0 -695
  668. package/src/core/adoption/helper-wrapper-plan.ts +0 -159
  669. package/src/runtime/context/codegraph-cache-boundary.ts +0 -255
  670. package/src/runtime/context/codegraph-read-provider.ts +0 -611
  671. package/src/runtime/context/codegraph-sidecar.cjs +0 -248
  672. package/src/runtime/control-plane/codegraph-cache-retention.ts +0 -164
  673. package/src/runtime/control-plane/execution/content-equivalent-commit-authority.ts +0 -169
  674. package/src/runtime/control-plane/execution/direct-canonical-work-reconciliation.ts +0 -85
  675. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +0 -655
  676. package/src/runtime/control-plane/execution/work-finalization-service.ts +0 -2850
  677. package/src/runtime/control-plane/execution/work-path-scope.ts +0 -37
  678. package/src/runtime/control-plane/execution/work-task-receipt.ts +0 -468
  679. package/src/runtime/control-plane/facade/controller-round-relay.ts +0 -3
  680. package/src/runtime/control-plane/facade/work-admission-policy.ts +0 -142
  681. package/src/runtime/control-plane/facade/work-implementation-review.ts +0 -2
  682. package/src/runtime/control-plane/facade/work-state-machine.ts +0 -2
  683. package/src/runtime/control-plane/facade/work-task-projection.ts +0 -66
  684. package/src/runtime/control-plane/routing/index.ts +0 -1
  685. package/src/runtime/control-plane/routing/route-policy.ts +0 -401
  686. package/src/runtime/control-plane/routing/workspace-admission.ts +0 -70
  687. package/src/runtime/gateway/mcp/execution-tools.ts +0 -2
  688. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +0 -2
  689. package/src/runtime/gateway/mcp/persisted-check-process.ts +0 -2
  690. package/src/runtime/gateway/mcp/process-tools.ts +0 -2
  691. package/src/runtime/gateway/mcp/router.ts +0 -2
  692. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +0 -2
  693. package/src/runtime/gateway/mcp/runtime-tools.ts +0 -2
  694. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +0 -2
  695. package/src/runtime/plugins/browser-provider-registry.ts +0 -2
@@ -1,7 +1,7 @@
1
1
  import { createHash } from 'crypto';
2
2
  import { mkdirSync } from 'fs';
3
3
  import { join } from 'path';
4
- import { repositoryControllerRoot } from '../../../cli/repositories/controller-home';
4
+ import { scopedOperationRoot } from '../../../cli/repositories/controller-home';
5
5
  import { withControllerLock } from '../../../cli/repositories/locks';
6
6
  import { readJsonFile, sanitizeFileComponent, writeJsonAtomic } from '../../shared/json-files';
7
7
  import {
@@ -15,23 +15,13 @@ import {
15
15
  } from './semantic-admission';
16
16
  import {
17
17
  listControlPlaneRecords,
18
- listAllControlPlaneRecordsWithinTransaction,
18
+ readControlPlaneRecord,
19
19
  readControlPlaneRecordWithinTransaction,
20
20
  withControlPlaneTransaction,
21
21
  writeControlPlaneRecordWithinTransaction,
22
22
  } from '../persistence/sqlite-store';
23
23
  import { readRequirement } from '../persistence/requirement-store';
24
- import {
25
- getWorkContract,
26
- canonicalizeWorkContractForAuthority,
27
- isDirectEditWorkCompletionReceipt,
28
- isRepositoryCompletionReceipt,
29
- rebindPlanBoundWorkContract,
30
- refreshPlanBoundWorkRevision,
31
- retirePlanBoundWorkContract,
32
- isTerminalWorkContractStatus,
33
- type WorkContract,
34
- } from '../../../../packages/kernel/work/api/index';
24
+ import type { ScopeRef } from '../../../../packages/kernel/identity/api/index';
35
25
  import {
36
26
  isTerminalPlanContractStatus,
37
27
  type EvidenceRef,
@@ -42,12 +32,13 @@ import {
42
32
  type PlanRevisionDraft,
43
33
  type PlanRevisionRecord,
44
34
  type PlanStep,
45
- type PlanStepDeliveryCarry,
46
35
  } from './types';
47
36
 
48
37
  export interface PlanContractStoreLocation {
49
38
  controllerHome?: string;
50
39
  repoId?: string;
40
+ /** Portable semantic scope key (for example the ForgeInstance semantic scope). Takes precedence over repoId. */
41
+ scopeKey?: string;
51
42
  root?: string;
52
43
  }
53
44
 
@@ -59,7 +50,8 @@ export interface PlanContractStoreOptions extends PlanContractStoreLocation {
59
50
 
60
51
  export interface CreatePlanContractInput {
61
52
  planId: string;
62
- repoId: string;
53
+ /** Optional node-local placement provenance; absent for a ForgeInstance-scoped Plan. */
54
+ repoId?: string;
63
55
  requirementId?: string;
64
56
  scopeKey: string;
65
57
  sourceRevision: string;
@@ -90,7 +82,8 @@ export type AdmitPlanContractResult = PlanAdmissionResolution;
90
82
  export interface PlanContractSummary {
91
83
  planId: string;
92
84
  revision: number;
93
- repoId: string;
85
+ /** Node-local placement provenance; absent for a ForgeInstance-scoped Plan. */
86
+ repoId?: string;
94
87
  requirementId?: string;
95
88
  scopeKey: string;
96
89
  sourceRevision: string;
@@ -109,6 +102,18 @@ function bounded(values: readonly string[] | undefined, limit: number, maxLength
109
102
  return (values ?? []).map((value) => String(value).trim()).filter(Boolean).slice(0, limit).map((value) => value.slice(0, maxLength));
110
103
  }
111
104
 
105
+ function normalizePlanSemanticItems(items: readonly { id: string; objective: string; dependencies?: string[] }[] | undefined) {
106
+ const normalized = (items ?? []).slice(0, 100).map((item) => ({
107
+ id: String(item.id).trim().slice(0, 200),
108
+ objective: String(item.objective).trim().slice(0, 2_000),
109
+ dependencies: [...new Set((item.dependencies ?? []).map(String).map((value) => value.trim()).filter(Boolean))].slice(0, 100),
110
+ }));
111
+ if (normalized.some((item) => !item.id || !item.objective) || new Set(normalized.map((item) => item.id)).size !== normalized.length) {
112
+ throw new Error('PLAN_SEMANTIC_ITEMS_INVALID');
113
+ }
114
+ return normalized;
115
+ }
116
+
112
117
  function normalizeScopeKey(value: string): string {
113
118
  return normalizePlanScopeKey(value) || 'unknown';
114
119
  }
@@ -118,23 +123,128 @@ export function currentPlanRevision(plan: PlanContract): number {
118
123
  return Number.isInteger(revision) && revision > 0 ? revision : 1;
119
124
  }
120
125
 
126
+ export function currentPlanSemanticRevision(plan: PlanContract): number {
127
+ const revision = Number(plan.semanticRevision);
128
+ return Number.isInteger(revision) && revision > 0 ? revision : 1;
129
+ }
130
+
131
+ export interface PlanSemanticView {
132
+ planId: string;
133
+ revision: number;
134
+ semanticScope: ScopeRef;
135
+ requirementId?: string;
136
+ /** One-way lineage fact: this Plan is historical and its obligations continue in the named successor. */
137
+ supersededBy?: string;
138
+ requirementBasisRevision?: number;
139
+ sourceBasisRevision: string;
140
+ goal: string;
141
+ nonGoals: string[];
142
+ assumptions: string[];
143
+ resolvedDecisions: string[];
144
+ stopConditions: string[];
145
+ replanConditions: string[];
146
+ integrationStrategy?: string;
147
+ items: Array<{ id: string; objective: string; dependencies: string[] }>;
148
+ createdAt: string;
149
+ updatedAt: string;
150
+ }
151
+
152
+ export interface PlanSemanticRevisionRecord extends PlanSemanticView {
153
+ schemaVersion: 1;
154
+ recordedAt: string;
155
+ }
156
+
157
+ export interface RevisePlanSemanticInput {
158
+ expectedRevision: number;
159
+ /** Optional one-way successor relation. Once set it cannot be cleared or changed. */
160
+ supersededBy?: string;
161
+ requirementBasisRevision?: number;
162
+ sourceBasisRevision?: string;
163
+ goal?: string;
164
+ nonGoals?: string[];
165
+ assumptions?: string[];
166
+ resolvedDecisions?: string[];
167
+ stopConditions?: string[];
168
+ replanConditions?: string[];
169
+ integrationStrategy?: string | null;
170
+ items?: Array<{ id: string; objective: string; dependencies?: string[] }>;
171
+ }
172
+
173
+ export interface CreatePlanSemanticInput {
174
+ planId: string;
175
+ /** Optional node-local placement provenance; absent for a ForgeInstance-scoped Plan. */
176
+ repoId?: string;
177
+ requirementId?: string;
178
+ requirementBasisRevision?: number;
179
+ scopeKey?: string;
180
+ sourceBasisRevision?: string;
181
+ goal: string;
182
+ nonGoals?: string[];
183
+ assumptions?: string[];
184
+ resolvedDecisions?: string[];
185
+ stopConditions?: string[];
186
+ replanConditions?: string[];
187
+ integrationStrategy?: string;
188
+ items?: Array<{ id: string; objective: string; dependencies?: string[] }>;
189
+ }
190
+
191
+ function legacyPlanSemanticContext(plan: PlanContract) {
192
+ return {
193
+ requirementBasisRevision: undefined,
194
+ sourceBasisRevision: plan.sourceRevision,
195
+ goal: plan.goal,
196
+ nonGoals: [...plan.nonGoals],
197
+ assumptions: [...plan.assumptions],
198
+ resolvedDecisions: [...plan.resolvedDecisions],
199
+ stopConditions: [...plan.stopConditions],
200
+ replanConditions: [...plan.replanConditions],
201
+ integrationStrategy: plan.integrationStrategy,
202
+ items: plan.steps.map((step) => ({ id: step.id, objective: step.objective, dependencies: [...step.dependencies] })),
203
+ };
204
+ }
205
+
206
+ export function planSemanticView(plan: PlanContract): PlanSemanticView {
207
+ const semantic = plan.semanticContext ?? legacyPlanSemanticContext(plan);
208
+ const semanticScope: ScopeRef = plan.requirementId?.trim()
209
+ ? { schemaVersion: 1, kind: 'requirement', id: plan.requirementId.trim() }
210
+ : { schemaVersion: 1, kind: 'plan', id: plan.planId };
211
+ return {
212
+ planId: plan.planId,
213
+ revision: currentPlanSemanticRevision(plan),
214
+ semanticScope,
215
+ requirementId: plan.requirementId,
216
+ ...(plan.supersededBy?.trim() ? { supersededBy: plan.supersededBy.trim() } : {}),
217
+ requirementBasisRevision: semantic.requirementBasisRevision,
218
+ sourceBasisRevision: semantic.sourceBasisRevision,
219
+ goal: semantic.goal,
220
+ nonGoals: [...semantic.nonGoals],
221
+ assumptions: [...semantic.assumptions],
222
+ resolvedDecisions: [...semantic.resolvedDecisions],
223
+ stopConditions: [...semantic.stopConditions],
224
+ replanConditions: [...semantic.replanConditions],
225
+ integrationStrategy: semantic.integrationStrategy,
226
+ items: semantic.items.map((item) => ({ id: item.id, objective: item.objective, dependencies: [...item.dependencies] })),
227
+ createdAt: plan.createdAt,
228
+ updatedAt: plan.semanticUpdatedAt ?? plan.createdAt,
229
+ };
230
+ }
231
+
121
232
  interface PlanRevisionRecordStore {
122
233
  schemaVersion: 1;
123
234
  updatedAt: string;
124
235
  revisions: PlanRevisionRecord[];
125
236
  }
126
237
 
238
+ interface PlanSemanticRevisionStore {
239
+ schemaVersion: 1;
240
+ updatedAt: string;
241
+ revisions: PlanSemanticRevisionRecord[];
242
+ }
243
+
127
244
  function planRevisionRecordKey(planId: string, revision: number): string {
128
245
  return `${sanitizeFileComponent(planId)}:r${revision}`;
129
246
  }
130
247
 
131
- function projectDependencyReadySteps(steps: readonly PlanStep[]): PlanStep[] {
132
- const completed = new Set(steps.filter((step) => step.status === 'completed').map((step) => step.id));
133
- return steps.map((step) => step.status === 'pending'
134
- && step.dependencies.every((dependencyId) => completed.has(dependencyId))
135
- ? { ...step, status: 'ready' as const }
136
- : step);
137
- }
138
248
 
139
249
  function normalizeStep(step: CreatePlanContractInput['steps'][number]): PlanStep {
140
250
  return {
@@ -206,81 +316,8 @@ export function listUnresolvedPlanObligations(plan: PlanContract): PlanObligatio
206
316
  return listPlanObligations(plan, 'unresolved');
207
317
  }
208
318
 
209
- function retireResolvedPredecessorObligationDispositions(
210
- plan: PlanContract,
211
- allPlans: readonly PlanContract[],
212
- ): PlanContract {
213
- const dispositions = plan.obligationDispositions ?? [];
214
- if (dispositions.length === 0 || (plan.supersedes?.length ?? 0) === 0) return plan;
215
- const resolvedByPredecessor = new Map<string, Set<string>>();
216
- for (const predecessorId of plan.supersedes ?? []) {
217
- const predecessor = allPlans.find((candidate) => candidate.planId === predecessorId);
218
- if (!predecessor) continue;
219
- resolvedByPredecessor.set(
220
- predecessorId,
221
- new Set(listPlanObligations(predecessor, 'completed_steps').map((obligation) => obligation.obligationId)),
222
- );
223
- }
224
- const retained = dispositions.filter((disposition) =>
225
- !resolvedByPredecessor.get(disposition.predecessorPlanId)?.has(disposition.obligationId));
226
- return retained.length === dispositions.length ? plan : { ...plan, obligationDispositions: retained };
227
- }
228
319
 
229
- function successorObligationRefs(plan: PlanContract): Set<string> {
230
- const refs = new Set<string>(['goal']);
231
- for (const step of plan.steps) {
232
- refs.add(`step:${step.id}`);
233
- step.acceptanceCriteria.forEach((_criterion, index) => refs.add(`step:${step.id}:acceptance:${index}`));
234
- }
235
- plan.nonGoals.forEach((_value, index) => refs.add(`non_goal:${index}`));
236
- plan.resolvedDecisions.forEach((_value, index) => refs.add(`resolved_decision:${index}`));
237
- plan.stopConditions.forEach((_value, index) => refs.add(`stop_condition:${index}`));
238
- plan.replanConditions.forEach((_value, index) => refs.add(`replan_condition:${index}`));
239
- if (plan.integrationStrategy?.trim()) refs.add('integration_strategy');
240
- return refs;
241
- }
242
320
 
243
- function obligationContinuityErrors(plan: PlanContract, allPlans: readonly PlanContract[]): string[] {
244
- const errors: string[] = [];
245
- const dispositions = plan.obligationDispositions ?? [];
246
- const successorRefs = successorObligationRefs(plan);
247
- const predecessorObligations = new Map<string, Set<string>>();
248
- for (const predecessorId of plan.supersedes ?? []) {
249
- const predecessor = allPlans.find((candidate) => candidate.planId === predecessorId);
250
- if (!predecessor) {
251
- errors.push(`superseded predecessor not found: ${predecessorId}`);
252
- continue;
253
- }
254
- predecessorObligations.set(predecessor.planId, new Set(listUnresolvedPlanObligations(predecessor).map((entry) => entry.obligationId)));
255
- }
256
-
257
- const seen = new Set<string>();
258
- for (const disposition of dispositions) {
259
- const key = `${disposition.predecessorPlanId}:${disposition.obligationId}`;
260
- if (seen.has(key)) errors.push(`duplicate obligation disposition ${key}`);
261
- seen.add(key);
262
- const known = predecessorObligations.get(disposition.predecessorPlanId);
263
- if ((plan.supersedes ?? []).includes(disposition.predecessorPlanId) && !known?.has(disposition.obligationId)) {
264
- errors.push(`unknown predecessor obligation ${key}`);
265
- }
266
- if ((disposition.disposition === 'keep' || disposition.disposition === 'change') && disposition.successorRefs.length === 0) {
267
- errors.push(`obligation ${key} ${disposition.disposition} requires successor_refs`);
268
- }
269
- for (const ref of disposition.successorRefs) {
270
- if (!successorRefs.has(ref)) errors.push(`obligation ${key} references unknown successor_ref ${ref}`);
271
- }
272
- if (disposition.disposition !== 'keep' && !disposition.rationale?.trim()) {
273
- errors.push(`obligation ${key} ${disposition.disposition} requires rationale`);
274
- }
275
- }
276
- for (const [predecessorId, obligations] of predecessorObligations) {
277
- const covered = new Set(dispositions.filter((entry) => entry.predecessorPlanId === predecessorId).map((entry) => entry.obligationId));
278
- for (const obligationId of obligations) {
279
- if (!covered.has(obligationId)) errors.push(`uncovered predecessor obligation ${predecessorId}:${obligationId}`);
280
- }
281
- }
282
- return [...new Set(errors)];
283
- }
284
321
 
285
322
  function updatePlanContract(
286
323
  options: PlanContractStoreOptions,
@@ -297,16 +334,17 @@ function updatePlanContract(
297
334
  writePlanContractStore(options, { schemaVersion: 1, updatedAt: next.updatedAt, contracts });
298
335
  return next;
299
336
  };
300
- if (!options.controllerHome || !options.repoId) return apply();
337
+ if (!options.controllerHome || !planContractStoreScopeKey(options)) return apply();
301
338
  return withControllerLock(
302
339
  options.controllerHome,
303
- { scope: 'task', repoId: options.repoId, taskId: `plan-${sanitizeFileComponent(planId)}` },
340
+ { scope: 'task', repoId: requirePlanContractStoreScopeKey(options), taskId: `plan-${sanitizeFileComponent(planId)}` },
304
341
  'plan-contract-store',
305
342
  apply,
306
343
  15_000,
307
344
  );
308
345
  }
309
346
 
347
+
310
348
  function assertRequirementReference(options: PlanContractStoreOptions, requirementId: string | undefined): void {
311
349
  const normalized = requirementId?.trim();
312
350
  if (!normalized || !options.controllerHome) return;
@@ -322,10 +360,10 @@ export function planContractRoot(location: PlanContractStoreLocation): string {
322
360
  mkdirSync(location.root, { recursive: true });
323
361
  return location.root;
324
362
  }
325
- if (!location.controllerHome || !location.repoId) {
326
- throw new Error('plan contract store requires either root or controllerHome + repoId');
363
+ if (!location.controllerHome || !planContractStoreScopeKey(location)) {
364
+ throw new Error('plan contract store requires either root or controllerHome + repoId/scopeKey');
327
365
  }
328
- const root = join(repositoryControllerRoot(location.controllerHome, location.repoId), 'plan-contracts');
366
+ const root = join(scopedOperationRoot(location.controllerHome, requirePlanContractStoreScopeKey(location)), 'plan-contracts');
329
367
  mkdirSync(root, { recursive: true });
330
368
  return root;
331
369
  }
@@ -334,10 +372,18 @@ export function planContractStorePath(location: PlanContractStoreLocation): stri
334
372
  return join(planContractRoot(location), 'index.json');
335
373
  }
336
374
 
375
+
376
+
377
+
378
+
337
379
  export function planRevisionStorePath(location: PlanContractStoreLocation): string {
338
380
  return join(planContractRoot(location), 'revisions.json');
339
381
  }
340
382
 
383
+ function planSemanticRevisionStorePath(location: PlanContractStoreLocation): string {
384
+ return join(planContractRoot(location), 'semantic-revisions.json');
385
+ }
386
+
341
387
  export function listPlanRevisionRecords(
342
388
  options: PlanContractStoreOptions,
343
389
  planId?: string,
@@ -350,7 +396,7 @@ export function listPlanRevisionRecords(
350
396
  .sort((left, right) => right.revision - left.revision);
351
397
  }
352
398
  return listControlPlaneRecords<PlanRevisionRecord>(options.controllerHome, {
353
- namespace: 'plan_revision', scope: options.repoId, limit: 5_000,
399
+ namespace: 'plan_revision', scope: requirePlanContractStoreScopeKey(options), limit: 5_000,
354
400
  }).map((record) => record.value)
355
401
  .filter((record) => !normalizedPlanId || record.planId === normalizedPlanId)
356
402
  .sort((left, right) => right.revision - left.revision);
@@ -365,6 +411,62 @@ function appendJsonPlanRevisionRecord(options: PlanContractStoreOptions, record:
365
411
  writeJsonAtomic(path, { schemaVersion: 1, updatedAt: record.recordedAt, revisions: [record, ...store.revisions] });
366
412
  }
367
413
 
414
+ /** Exact historical lookup; revision-list limits are not version-existence gates. */
415
+ export function getPlanSemanticRevisionRecord(
416
+ options: PlanContractStoreOptions,
417
+ planId: string,
418
+ revision: number,
419
+ ): PlanSemanticRevisionRecord | undefined {
420
+ if (!Number.isInteger(revision) || revision < 1) return undefined;
421
+ const normalizedPlanId = sanitizeFileComponent(planId);
422
+ const record = sqliteBacked(options)
423
+ ? readControlPlaneRecord<PlanSemanticRevisionRecord>(
424
+ options.controllerHome, 'plan_semantic_revision', requirePlanContractStoreScopeKey(options),
425
+ planRevisionRecordKey(normalizedPlanId, revision),
426
+ )?.value
427
+ : readJsonFile<PlanSemanticRevisionStore>(
428
+ planSemanticRevisionStorePath(options), { schemaVersion: 1, updatedAt: nowIso(options), revisions: [] },
429
+ ).revisions.find((entry) => entry.planId === normalizedPlanId && entry.revision === revision);
430
+ return record ? {
431
+ ...record,
432
+ semanticScope: record.semanticScope ?? (record.requirementId
433
+ ? { schemaVersion: 1, kind: 'requirement', id: record.requirementId }
434
+ : { schemaVersion: 1, kind: 'plan', id: record.planId }),
435
+ } : undefined;
436
+ }
437
+
438
+ export function listPlanSemanticRevisionRecords(options: PlanContractStoreOptions, planId?: string): PlanSemanticRevisionRecord[] {
439
+ const normalizedPlanId = planId ? sanitizeFileComponent(planId) : undefined;
440
+ if (!sqliteBacked(options)) {
441
+ const store = readJsonFile<PlanSemanticRevisionStore>(planSemanticRevisionStorePath(options), { schemaVersion: 1, updatedAt: nowIso(options), revisions: [] });
442
+ return store.revisions.filter((record) => !normalizedPlanId || record.planId === normalizedPlanId)
443
+ .map((record) => ({
444
+ ...record,
445
+ semanticScope: record.semanticScope ?? (record.requirementId
446
+ ? { schemaVersion: 1, kind: 'requirement', id: record.requirementId }
447
+ : { schemaVersion: 1, kind: 'plan', id: record.planId }),
448
+ } as PlanSemanticRevisionRecord))
449
+ .sort((left, right) => right.revision - left.revision);
450
+ }
451
+ return listControlPlaneRecords<PlanSemanticRevisionRecord>(options.controllerHome, {
452
+ namespace: 'plan_semantic_revision', scope: requirePlanContractStoreScopeKey(options), limit: 5_000,
453
+ }).map((record) => record.value)
454
+ .filter((record) => !normalizedPlanId || record.planId === normalizedPlanId)
455
+ .map((record) => ({
456
+ ...record,
457
+ semanticScope: record.semanticScope ?? (record.requirementId
458
+ ? { schemaVersion: 1, kind: 'requirement', id: record.requirementId }
459
+ : { schemaVersion: 1, kind: 'plan', id: record.planId }),
460
+ } as PlanSemanticRevisionRecord))
461
+ .sort((left, right) => right.revision - left.revision);
462
+ }
463
+
464
+ function appendJsonPlanSemanticRevisionRecord(options: PlanContractStoreOptions, record: PlanSemanticRevisionRecord): void {
465
+ const path = planSemanticRevisionStorePath(options);
466
+ const store = readJsonFile<PlanSemanticRevisionStore>(path, { schemaVersion: 1, updatedAt: record.recordedAt, revisions: [] });
467
+ if (store.revisions.some((existing) => existing.planId === record.planId && existing.revision === record.revision)) return;
468
+ writeJsonAtomic(path, { schemaVersion: 1, updatedAt: record.recordedAt, revisions: [record, ...store.revisions].slice(0, 5_000) });
469
+ }
368
470
  function planRevisionRecord(plan: PlanContract, input: { recordedAt: string; reason: string; requestedRevisionLabel?: string }): PlanRevisionRecord {
369
471
  return {
370
472
  schemaVersion: 1, repoId: plan.repoId, planId: plan.planId, revision: currentPlanRevision(plan),
@@ -389,30 +491,6 @@ function revisionDraftFromCandidate(candidate: PlanContract, input: { requestedR
389
491
  };
390
492
  }
391
493
 
392
- function planFromPendingRevision(current: PlanContract, pending: PlanRevisionDraft, at: string): PlanContract {
393
- return {
394
- ...current, revision: pending.revision, sourceRevision: pending.sourceRevision, goal: pending.goal, nonGoals: [...pending.nonGoals],
395
- assumptions: [...pending.assumptions], resolvedDecisions: [...pending.resolvedDecisions], stopConditions: [...pending.stopConditions],
396
- replanConditions: [...pending.replanConditions], integrationStrategy: pending.integrationStrategy, steps: structuredClone(pending.steps),
397
- obligationDispositions: pending.obligationDispositions ? structuredClone(pending.obligationDispositions) : undefined,
398
- deliveryCarries: pending.deliveryCarries ? structuredClone(pending.deliveryCarries) : undefined,
399
- pendingRevision: undefined, updatedAt: at,
400
- };
401
- }
402
-
403
- function revisionContinuityErrors(current: PlanContract, candidate: PlanContract, allPlans: readonly PlanContract[]): string[] {
404
- const validationCandidate: PlanContract = { ...candidate, supersedes: [current.planId] };
405
- return obligationContinuityErrors(validationCandidate, allPlans);
406
- }
407
-
408
- function retireResolvedRevisionObligationDispositions(current: PlanContract, candidate: PlanContract): PlanContract {
409
- const dispositions = candidate.obligationDispositions ?? [];
410
- if (dispositions.length === 0) return candidate;
411
- const completed = new Set(listPlanObligations(current, 'completed_steps').map((obligation) => obligation.obligationId));
412
- if (completed.size === 0) return candidate;
413
- const retained = dispositions.filter((disposition) => !(disposition.predecessorPlanId === current.planId && completed.has(disposition.obligationId)));
414
- return retained.length === dispositions.length ? candidate : { ...candidate, obligationDispositions: retained };
415
- }
416
494
 
417
495
  function assertRevisionScopeAuthority(current: PlanContract, allPlans: readonly PlanContract[]): void {
418
496
  const conflicting = allPlans.find((plan) => plan.planId !== current.planId
@@ -420,35 +498,25 @@ function assertRevisionScopeAuthority(current: PlanContract, allPlans: readonly
420
498
  if (conflicting) throw new Error(`PLAN_SCOPE_ALREADY_OWNED: ${current.scopeKey}:${conflicting.planId}`);
421
499
  }
422
500
 
423
- function deriveRequiredRevisionDeliveryCarries(
424
- options: PlanContractStoreOptions,
425
- current: PlanContract,
426
- candidate: PlanContract,
427
- at: string,
428
- ): PlanStepDeliveryCarry[] {
429
- const carries = derivePlanStepDeliveryCarries(options, current, candidate, at);
430
- // A validating projection backed by a still-completed Work but missing its
431
- // immutable completion receipt is corrupt and cannot be silently downgraded.
432
- // A changed execution contract, failed/cancelled Work, or a successor source
433
- // that does not contain the delivered revision simply means "do not carry".
434
- for (const previousStep of current.steps) {
435
- if (previousStep.status !== 'validating' || !previousStep.workId) continue;
436
- const nextStep = candidate.steps.find((step) => step.id === previousStep.id);
437
- if (!nextStep || !samePlanStepExecutionContract(previousStep, nextStep)) continue;
438
- if (!acceptanceChangesExplicitlyReconciled(current, previousStep, candidate, nextStep)) continue;
439
- const work = getWorkContract(options, previousStep.workId);
440
- if (work?.status === 'completed' && !work.completionReceipt) {
441
- throw new Error(`PLAN_DELIVERY_CARRY_RECEIPT_REQUIRED: ${current.planId}/${previousStep.id}`);
442
- }
443
- }
444
- return carries;
445
- }
446
501
 
447
502
  export function emptyPlanContractStore(updatedAt: string): PlanContractStore {
448
503
  return { schemaVersion: 1, updatedAt, contracts: [] };
449
504
  }
450
- function sqliteBacked(options: PlanContractStoreOptions): options is PlanContractStoreOptions & { controllerHome: string; repoId: string } {
451
- return Boolean(!options.root && options.controllerHome?.trim() && options.repoId?.trim());
505
+ /** Canonical Plan store scope key: an explicit portable scope, else repository placement. */
506
+ export function planContractStoreScopeKey(options: PlanContractStoreLocation): string | undefined {
507
+ return options.scopeKey?.trim() || options.repoId?.trim() || undefined;
508
+ }
509
+
510
+ function requirePlanContractStoreScopeKey(options: PlanContractStoreLocation): string {
511
+ const key = planContractStoreScopeKey(options);
512
+ if (!key) throw new Error('PLAN_STORE_SCOPE_REQUIRED: controllerHome requires repoId or scopeKey');
513
+ return key;
514
+ }
515
+
516
+ type ControlPlanePlanStoreOptions = PlanContractStoreOptions & { controllerHome: string };
517
+
518
+ function sqliteBacked(options: PlanContractStoreOptions): options is ControlPlanePlanStoreOptions {
519
+ return Boolean(!options.root && options.controllerHome?.trim() && planContractStoreScopeKey(options));
452
520
  }
453
521
 
454
522
  export function readPlanContractStore(options: PlanContractStoreOptions): PlanContractStore {
@@ -457,7 +525,7 @@ export function readPlanContractStore(options: PlanContractStoreOptions): PlanCo
457
525
  }
458
526
  const records = listControlPlaneRecords<PlanContract>(options.controllerHome, {
459
527
  namespace: 'plan_contract',
460
- scope: options.repoId,
528
+ scope: requirePlanContractStoreScopeKey(options),
461
529
  limit: 5_000,
462
530
  });
463
531
  if (records.length > 0) {
@@ -474,10 +542,10 @@ export function readPlanContractStore(options: PlanContractStoreOptions): PlanCo
474
542
  if (legacy.contracts.length > 0) {
475
543
  withControlPlaneTransaction(options.controllerHome, (database) => {
476
544
  for (const contract of legacy.contracts) {
477
- if (readControlPlaneRecordWithinTransaction<PlanContract>(database, 'plan_contract', options.repoId, contract.planId)) continue;
545
+ if (readControlPlaneRecordWithinTransaction<PlanContract>(database, 'plan_contract', requirePlanContractStoreScopeKey(options), contract.planId)) continue;
478
546
  writeControlPlaneRecordWithinTransaction(database, {
479
547
  namespace: 'plan_contract',
480
- scope: options.repoId,
548
+ scope: requirePlanContractStoreScopeKey(options),
481
549
  key: contract.planId,
482
550
  schemaVersion: 1,
483
551
  value: contract,
@@ -500,7 +568,7 @@ function writePlanContractStore(options: PlanContractStoreOptions, store: PlanCo
500
568
  const current = readControlPlaneRecordWithinTransaction<PlanContract>(
501
569
  database,
502
570
  'plan_contract',
503
- options.repoId,
571
+ requirePlanContractStoreScopeKey(options),
504
572
  contract.planId,
505
573
  );
506
574
  // SQLite is authoritative per Plan row. A sibling Plan appearing in an
@@ -508,7 +576,7 @@ function writePlanContractStore(options: PlanContractStoreOptions, store: PlanCo
508
576
  if (current && JSON.stringify(current.value) === JSON.stringify(contract)) continue;
509
577
  writeControlPlaneRecordWithinTransaction(database, {
510
578
  namespace: 'plan_contract',
511
- scope: options.repoId,
579
+ scope: requirePlanContractStoreScopeKey(options),
512
580
  key: contract.planId,
513
581
  schemaVersion: 1,
514
582
  value: contract,
@@ -520,73 +588,6 @@ function writePlanContractStore(options: PlanContractStoreOptions, store: PlanCo
520
588
  return store;
521
589
  }
522
590
 
523
- function writePlanSupersessionWithWorkRetirement(
524
- options: PlanContractStoreOptions,
525
- store: PlanContractStore,
526
- predecessor: PlanContract,
527
- successor: PlanContract,
528
- at: string,
529
- ): PlanContract {
530
- if (!sqliteBacked(options)) {
531
- writePlanContractStore(options, store);
532
- return successor;
533
- }
534
-
535
- let writtenSuccessor = successor;
536
-
537
- withControlPlaneTransaction(options.controllerHome, (database) => {
538
- let successorNext = successor;
539
- const workWrites: Array<{ value: WorkContract; revision: number; action: string }> = [];
540
- const workRecords = listAllControlPlaneRecordsWithinTransaction<WorkContract>(database, {
541
- namespace: 'work_contract', scope: options.repoId,
542
- }).filter((record) => record.value.planId === predecessor.planId);
543
- for (const current of workRecords) {
544
- if (isTerminalWorkContractStatus(current.value.status)) continue;
545
- const predecessorStep = predecessor.steps.find((step) => workMatchesPlanStep(current.value, predecessor, step));
546
- const successorIndex = predecessorStep ? successorNext.steps.findIndex((step) => step.id === predecessorStep.id) : -1;
547
- const successorStep = successorIndex >= 0 ? successorNext.steps[successorIndex] : undefined;
548
- if (predecessorStep && successorStep
549
- && samePlanStepExecutionContract(predecessorStep, successorStep)
550
- && sameOrderedStrings(predecessorStep.acceptanceCriteria, successorStep.acceptanceCriteria)) {
551
- const rebound = rebindPlanBoundWorkContract(current.value, {
552
- predecessorPlanId: predecessor.planId, successorPlanId: successorNext.planId, planStepId: successorStep.id,
553
- planSourceRevision: successorNext.sourceRevision, allowedPaths: successorStep.allowedPaths, forbiddenPaths: successorStep.forbiddenPaths,
554
- checks: successorStep.checks, recordedAt: at, reason: 'Plan successor preserved this exact active Work contract.',
555
- });
556
- const steps = [...successorNext.steps];
557
- steps[successorIndex] = {
558
- ...successorStep, status: 'executing', workId: rebound.workId,
559
- evidenceRefs: [{ title: 'active Work carried to successor', summary: `Rebound exact Work ${rebound.workId} from ${predecessor.planId}; successor approval changed no Work contract semantics.`, detailLevel: 'summary' as const }, ...predecessorStep.evidenceRefs].slice(0, 20),
560
- };
561
- successorNext = { ...successorNext, status: 'executing', steps, updatedAt: at };
562
- workWrites.push({ value: rebound, revision: current.revision, action: 'work_plan_authority_rebound' });
563
- continue;
564
- }
565
- const retired = retirePlanBoundWorkContract(current.value, {
566
- predecessorPlanId: predecessor.planId, successorPlanId: successorNext.planId, recordedAt: at,
567
- reason: predecessor.supersessionReason ?? 'owning Plan was superseded',
568
- });
569
- workWrites.push({ value: retired, revision: current.revision, action: 'work_plan_authority_retired' });
570
- }
571
-
572
- for (const plan of [predecessor, successorNext]) {
573
- const current = readControlPlaneRecordWithinTransaction<PlanContract>(database, 'plan_contract', options.repoId, plan.planId);
574
- writeControlPlaneRecordWithinTransaction(database, {
575
- namespace: 'plan_contract', scope: options.repoId, key: plan.planId, schemaVersion: 1, value: plan,
576
- action: plan.planId === predecessor.planId ? 'plan_contract_superseded' : 'plan_contract_successor_written',
577
- expectedRevision: current?.revision ?? null,
578
- });
579
- }
580
- for (const write of workWrites) {
581
- writeControlPlaneRecordWithinTransaction(database, {
582
- namespace: 'work_contract', scope: options.repoId, key: write.value.workId, schemaVersion: 2, value: write.value,
583
- action: write.action, expectedRevision: write.revision,
584
- });
585
- }
586
- writtenSuccessor = successorNext;
587
- });
588
- return writtenSuccessor;
589
- }
590
591
 
591
592
  function buildPlanContract(input: CreatePlanContractInput, at: string): PlanContract {
592
593
  const planId = sanitizeFileComponent(input.planId);
@@ -641,175 +642,13 @@ function draftContentErrors(plan: PlanContract): string[] {
641
642
  return [...new Set(errors)];
642
643
  }
643
644
 
644
- function sameOrderedStrings(left: readonly string[], right: readonly string[]): boolean {
645
- return left.length === right.length && left.every((value, index) => value === right[index]);
646
- }
647
-
648
- function samePlanStepExecutionContract(predecessor: PlanStep, successor: PlanStep): boolean {
649
- return predecessor.id === successor.id
650
- && predecessor.objective === successor.objective
651
- && sameOrderedStrings(predecessor.dependencies, successor.dependencies)
652
- && sameOrderedStrings(predecessor.authoritativeFiles, successor.authoritativeFiles)
653
- && sameOrderedStrings(predecessor.allowedPaths, successor.allowedPaths)
654
- && sameOrderedStrings(predecessor.forbiddenPaths, successor.forbiddenPaths)
655
- && sameOrderedStrings(predecessor.checks, successor.checks);
656
- }
657
-
658
- function workMatchesPlanStep(work: WorkContract, plan: PlanContract, step: PlanStep): boolean {
659
- return step.status === 'executing' && work.planId === plan.planId && work.planStepId === step.id && step.workId === work.workId
660
- && work.requirementId === plan.requirementId && work.planSourceRevision === plan.sourceRevision
661
- && work.objective === step.objective
662
- && sameOrderedStrings(work.acceptanceCriteria, step.acceptanceCriteria)
663
- && sameOrderedStrings(work.allowedPaths, step.allowedPaths)
664
- && sameOrderedStrings(work.forbiddenPaths, step.forbiddenPaths)
665
- && sameOrderedStrings(work.checks, step.checks);
666
- }
667
-
668
- function hasCoherentActivePlanBoundWork(options: PlanContractStoreOptions, plan: PlanContract): boolean {
669
- if (!sqliteBacked(options)) return false;
670
- return listControlPlaneRecords<WorkContract>(options.controllerHome, { namespace: 'work_contract', scope: options.repoId, limit: 5_000 })
671
- .some(({ value: work }) => !isTerminalWorkContractStatus(work.status)
672
- && plan.steps.some((step) => workMatchesPlanStep(work, plan, step)));
673
- }
674
-
675
- function acceptanceChangesExplicitlyReconciled(
676
- predecessorPlan: PlanContract,
677
- predecessorStep: PlanStep,
678
- successorPlan: PlanContract,
679
- successorStep: PlanStep,
680
- ): boolean {
681
- if (sameOrderedStrings(predecessorStep.acceptanceCriteria, successorStep.acceptanceCriteria)) return true;
682
- const obligations = listUnresolvedPlanObligations(predecessorPlan)
683
- .filter((entry) => entry.kind === 'step_acceptance' && entry.sourceRef.startsWith(`step:${predecessorStep.id}:acceptance:`));
684
- const dispositions = successorPlan.obligationDispositions ?? [];
685
- const dispositionFor = (obligationId: string) => dispositions.find((entry) =>
686
- entry.predecessorPlanId === predecessorPlan.planId && entry.obligationId === obligationId);
687
-
688
- for (const obligation of obligations) {
689
- if (successorStep.acceptanceCriteria.includes(obligation.summary)) continue;
690
- const disposition = dispositionFor(obligation.obligationId);
691
- if (!disposition || disposition.disposition === 'keep' || !disposition.rationale?.trim()) return false;
692
- }
693
- for (let index = 0; index < successorStep.acceptanceCriteria.length; index += 1) {
694
- const criterion = successorStep.acceptanceCriteria[index]!;
695
- if (predecessorStep.acceptanceCriteria.includes(criterion)) continue;
696
- const successorRef = `step:${successorStep.id}:acceptance:${index}`;
697
- const explicitlyChanged = obligations.some((obligation) => {
698
- const disposition = dispositionFor(obligation.obligationId);
699
- return disposition?.disposition === 'change'
700
- && Boolean(disposition.rationale?.trim())
701
- && disposition.successorRefs.includes(successorRef);
702
- });
703
- if (!explicitlyChanged) return false;
704
- }
705
- return true;
706
- }
707
645
 
708
- function deliveryCarryForStep(
709
- options: PlanContractStoreOptions,
710
- predecessorPlan: PlanContract,
711
- predecessorStep: PlanStep,
712
- successorPlan: PlanContract,
713
- successorStep: PlanStep,
714
- recordedAt: string,
715
- ): PlanStepDeliveryCarry | undefined {
716
- if (predecessorStep.status !== 'validating' || !predecessorStep.workId) return undefined;
717
- if (!samePlanStepExecutionContract(predecessorStep, successorStep)) return undefined;
718
- if (!acceptanceChangesExplicitlyReconciled(predecessorPlan, predecessorStep, successorPlan, successorStep)) return undefined;
719
- if (!predecessorPlan.requirementId || successorPlan.requirementId !== predecessorPlan.requirementId) return undefined;
720
-
721
- const work = getWorkContract(options, predecessorStep.workId);
722
- if (!work
723
- || work.status !== 'completed'
724
- || work.phase !== 'cleanup'
725
- || work.evidenceState !== 'valid'
726
- || !work.completionOutcome
727
- || work.completionOutcome === 'superseded'
728
- || work.requirementId !== predecessorPlan.requirementId
729
- || work.planId !== predecessorPlan.planId
730
- || work.planStepId !== predecessorStep.id
731
- || work.planSourceRevision !== predecessorPlan.sourceRevision
732
- || !work.completionReceipt) return undefined;
733
-
734
- const receipt = work.completionReceipt;
735
- if (!isRepositoryCompletionReceipt(receipt) && !isDirectEditWorkCompletionReceipt(receipt)) return undefined;
736
- if (receipt.workId !== work.workId
737
- || receipt.delivery.status !== 'integrated'
738
- || !receipt.delivery.reachable
739
- || receipt.cleanup.status === 'blocked') return undefined;
740
- const targetRevision = receipt.targetRevision.trim();
741
- const successorSourceRevision = successorPlan.sourceRevision.trim();
742
- if (!targetRevision || !successorSourceRevision) return undefined;
743
- if (targetRevision !== successorSourceRevision) {
744
- try {
745
- if (options.revisionContains?.(targetRevision, successorSourceRevision) !== true) return undefined;
746
- } catch {
747
- return undefined;
748
- }
749
- }
750
646
 
751
- return {
752
- predecessorPlanId: predecessorPlan.planId,
753
- predecessorStepId: predecessorStep.id,
754
- successorStepId: successorStep.id,
755
- workId: work.workId,
756
- completionReceiptId: receipt.receiptId,
757
- deliveredSourceRevision: targetRevision,
758
- recordedAt,
759
- };
760
- }
761
647
 
762
- function derivePlanStepDeliveryCarries(
763
- options: PlanContractStoreOptions,
764
- predecessorPlan: PlanContract,
765
- successorPlan: PlanContract,
766
- recordedAt: string,
767
- ): PlanStepDeliveryCarry[] {
768
- return successorPlan.steps.flatMap((successorStep) => {
769
- const predecessorStep = predecessorPlan.steps.find((candidate) => candidate.id === successorStep.id);
770
- if (!predecessorStep) return [];
771
- const carry = deliveryCarryForStep(options, predecessorPlan, predecessorStep, successorPlan, successorStep, recordedAt);
772
- return carry ? [carry] : [];
773
- });
774
- }
775
648
 
776
- function materializePlanStepDeliveryCarries(
777
- options: PlanContractStoreOptions,
778
- plan: PlanContract,
779
- allPlans: readonly PlanContract[],
780
- ): { steps: PlanStep[]; carriedCount: number } {
781
- if (!plan.deliveryCarries?.length) return { steps: plan.steps, carriedCount: 0 };
782
- const carriedSteps = new Set<string>();
783
- const steps = plan.steps.map((step) => ({ ...step }));
784
- for (const carry of plan.deliveryCarries) {
785
- if (carriedSteps.has(carry.successorStepId)) throw new Error(`PLAN_DELIVERY_CARRY_DUPLICATE_STEP: ${carry.successorStepId}`);
786
- const predecessorPlan = allPlans.find((candidate) => candidate.planId === carry.predecessorPlanId);
787
- const predecessorStep = predecessorPlan?.steps.find((candidate) => candidate.id === carry.predecessorStepId);
788
- const successorIndex = steps.findIndex((candidate) => candidate.id === carry.successorStepId);
789
- const successorStep = successorIndex >= 0 ? steps[successorIndex] : undefined;
790
- if (!predecessorPlan || !predecessorStep || !successorStep) throw new Error(`PLAN_DELIVERY_CARRY_INVALID: ${carry.successorStepId}`);
791
- const expected = deliveryCarryForStep(options, predecessorPlan, predecessorStep, plan, successorStep, carry.recordedAt);
792
- if (!expected
793
- || expected.workId !== carry.workId
794
- || expected.completionReceiptId !== carry.completionReceiptId
795
- || expected.deliveredSourceRevision !== carry.deliveredSourceRevision) {
796
- throw new Error(`PLAN_DELIVERY_CARRY_INVALID: ${carry.successorStepId}`);
797
- }
798
- steps[successorIndex] = {
799
- ...successorStep,
800
- status: 'validating',
801
- workId: carry.workId,
802
- evidenceRefs: [{
803
- evidenceId: carry.completionReceiptId,
804
- title: 'successor delivery carried',
805
- summary: `Reused immutable delivery from ${carry.predecessorPlanId}/${carry.predecessorStepId}; semantic acceptance remains explicit.`,
806
- detailLevel: 'summary' as const,
807
- }, ...predecessorStep.evidenceRefs].slice(0, 20),
808
- };
809
- carriedSteps.add(carry.successorStepId);
810
- }
811
- return { steps, carriedCount: carriedSteps.size };
812
- }
649
+
650
+
651
+
813
652
 
814
653
  function createPlanContractUnlocked(options: PlanContractStoreOptions, input: CreatePlanContractInput): PlanContract {
815
654
  assertRequirementReference(options, input.requirementId);
@@ -837,20 +676,9 @@ function replacePlanContractUnlocked(
837
676
  const current = store.contracts[predecessorIndex]!;
838
677
  if (!isPlanExtensionPredecessor(current)) throw new Error(`plan contract ${current.planId} is terminal (${current.status})`);
839
678
 
840
- // Before approval Plan identity is already provisional, so an extension simply
841
- // repairs that exact draft instead of minting an r2/r3 entity.
679
+ // Draft extension keeps the stable Plan identity, but authored content is still
680
+ // a semantic revision: the previous head is archived and the current head advances.
842
681
  if (current.status === 'draft') {
843
- const draftCandidate = buildPlanContract({
844
- ...input,
845
- planId: current.planId,
846
- repoId: current.repoId,
847
- requirementId: current.requirementId,
848
- scopeKey: current.scopeKey,
849
- }, at);
850
- const continuityErrors = revisionContinuityErrors(current, draftCandidate, store.contracts);
851
- if (continuityErrors.length > 0) {
852
- throw new Error(`PLAN_OBLIGATION_CONTINUITY_REQUIRED: ${continuityErrors.join('; ')}`);
853
- }
854
682
  return repairDraftPlanContractUnlocked(options, current.planId, {
855
683
  scopeKey: input.scopeKey, sourceRevision: input.sourceRevision, goal: input.goal, nonGoals: input.nonGoals, assumptions: input.assumptions,
856
684
  resolvedDecisions: input.resolvedDecisions, stopConditions: input.stopConditions, replanConditions: input.replanConditions,
@@ -868,13 +696,9 @@ function replacePlanContractUnlocked(
868
696
  };
869
697
  const contentErrors = draftContentErrors(candidate);
870
698
  if (contentErrors.length > 0) throw new Error(`PLAN_REVISION_DRAFT_INVALID: ${contentErrors.join('; ')}`);
871
- const continuityErrors = revisionContinuityErrors(current, candidate, store.contracts);
872
- if (continuityErrors.length > 0) throw new Error(`PLAN_OBLIGATION_CONTINUITY_REQUIRED: ${continuityErrors.join('; ')}`);
873
699
  assertRevisionScopeAuthority(current, store.contracts);
874
- const deliveryCarries = deriveRequiredRevisionDeliveryCarries(options, current, candidate, at);
875
- const stagedCandidate = deliveryCarries.length > 0 ? { ...candidate, deliveryCarries } : candidate;
876
700
  const requestedRevisionLabel = sanitizeFileComponent(input.planId);
877
- const pendingRevision = revisionDraftFromCandidate(stagedCandidate, {
701
+ const pendingRevision = revisionDraftFromCandidate(candidate, {
878
702
  requestedRevisionLabel: requestedRevisionLabel !== current.planId ? requestedRevisionLabel : undefined, createdAt: at, updatedAt: at,
879
703
  });
880
704
  const next: PlanContract = { ...current, status: 'replanning', pendingRevision, updatedAt: at };
@@ -909,27 +733,18 @@ function repairDraftPlanContractUnlocked(
909
733
  requirementId: current.requirementId,
910
734
  evidenceRefs: current.evidenceRefs,
911
735
  }, at);
912
- const candidateWithLineage = retireResolvedPredecessorObligationDispositions({
736
+ const candidateWithLineage = {
913
737
  ...candidate,
914
738
  ...(current.supersedes?.length ? { supersedes: [...current.supersedes] } : {}),
915
- }, store.contracts);
739
+ };
916
740
  const contentErrors = draftContentErrors(candidateWithLineage);
917
741
  if (contentErrors.length > 0) {
918
742
  throw new Error(`PLAN_DRAFT_REPAIR_INVALID: ${contentErrors.join('; ')}`);
919
743
  }
920
- const continuityErrors = obligationContinuityErrors(candidateWithLineage, store.contracts);
921
- if (continuityErrors.length > 0) {
922
- throw new Error(`PLAN_DRAFT_REPAIR_INVALID: ${continuityErrors.join('; ')}`);
923
- }
924
744
  if (pending) {
925
745
  if (normalizeScopeKey(input.scopeKey) !== current.scopeKey) throw new Error(`PLAN_REVISION_SCOPE_IDENTITY_MISMATCH: ${current.scopeKey}`);
926
746
  assertRevisionScopeAuthority(current, store.contracts);
927
747
  let revisionCandidate: PlanContract = { ...candidateWithLineage, revision: pending.revision, supersedes: current.supersedes ? [...current.supersedes] : undefined };
928
- revisionCandidate = retireResolvedRevisionObligationDispositions(current, revisionCandidate);
929
- const revisionErrors = revisionContinuityErrors(current, revisionCandidate, store.contracts);
930
- if (revisionErrors.length > 0) throw new Error(`PLAN_DRAFT_REPAIR_INVALID: ${revisionErrors.join('; ')}`);
931
- const deliveryCarries = deriveRequiredRevisionDeliveryCarries(options, current, revisionCandidate, at);
932
- if (deliveryCarries.length > 0) revisionCandidate = { ...revisionCandidate, deliveryCarries };
933
748
  const repairedPending = revisionDraftFromCandidate(revisionCandidate, {
934
749
  requestedRevisionLabel: pending.requestedRevisionLabel, createdAt: pending.createdAt, updatedAt: at,
935
750
  });
@@ -945,18 +760,46 @@ function repairDraftPlanContractUnlocked(
945
760
  && existing.scopeKey === candidateWithLineage.scopeKey
946
761
  && !explicitPredecessors.has(existing.planId));
947
762
  if (conflictingScope) throw new Error(`PLAN_SCOPE_ALREADY_OWNED: ${candidateWithLineage.scopeKey}:${conflictingScope.planId}`);
948
- const predecessor = current.supersedes?.length === 1
949
- ? store.contracts.find((existing) => existing.planId === current.supersedes![0])
950
- : undefined;
951
- const deliveryCarries = predecessor
952
- ? derivePlanStepDeliveryCarries(options, predecessor, candidateWithLineage, at)
953
- : [];
763
+ const previousSemantic = planSemanticView(current);
764
+ const candidateSemantic = legacyPlanSemanticContext(candidateWithLineage);
954
765
  const repaired: PlanContract = {
955
766
  ...candidateWithLineage,
956
- ...(deliveryCarries.length ? { deliveryCarries } : {}),
767
+ // Mechanical lifecycle revision is independent from the model-facing semantic
768
+ // head. Draft/extend compatibility must never silently overwrite semantic r1.
769
+ revision: current.revision,
770
+ semanticRevision: previousSemantic.revision + 1,
771
+ semanticUpdatedAt: at,
772
+ semanticContext: {
773
+ ...candidateSemantic,
774
+ requirementBasisRevision: previousSemantic.requirementBasisRevision,
775
+ },
957
776
  createdAt: current.createdAt,
958
777
  updatedAt: at,
959
778
  };
779
+ if (sqliteBacked(options)) {
780
+ return withControlPlaneTransaction(options.controllerHome, (database) => {
781
+ const scope = requirePlanContractStoreScopeKey(options);
782
+ const currentRecord = readControlPlaneRecordWithinTransaction<PlanContract>(database, 'plan_contract', scope, current.planId);
783
+ if (!currentRecord) throw new Error(`plan contract not found: ${current.planId}`);
784
+ const actualSemanticRevision = currentPlanSemanticRevision(currentRecord.value);
785
+ if (actualSemanticRevision !== previousSemantic.revision) {
786
+ throw new Error(`PLAN_REVISION_CONFLICT:${current.planId}:expected=${previousSemantic.revision}:actual=${actualSemanticRevision}`);
787
+ }
788
+ const semanticRevisionKey = `${current.planId}:r${previousSemantic.revision}`;
789
+ if (!readControlPlaneRecordWithinTransaction<PlanSemanticRevisionRecord>(database, 'plan_semantic_revision', scope, semanticRevisionKey)) {
790
+ writeControlPlaneRecordWithinTransaction(database, {
791
+ namespace: 'plan_semantic_revision', scope, key: semanticRevisionKey, schemaVersion: 1,
792
+ value: { schemaVersion: 1, ...previousSemantic, recordedAt: at },
793
+ action: 'plan_semantic_revision_archived', expectedRevision: null,
794
+ });
795
+ }
796
+ return writeControlPlaneRecordWithinTransaction(database, {
797
+ namespace: 'plan_contract', scope, key: current.planId, schemaVersion: 1,
798
+ value: repaired, action: 'plan_semantic_revised', expectedRevision: currentRecord.revision,
799
+ }).value;
800
+ });
801
+ }
802
+ appendJsonPlanSemanticRevisionRecord(options, { schemaVersion: 1, ...previousSemantic, recordedAt: at });
960
803
  const contracts = [...store.contracts];
961
804
  contracts[index] = repaired;
962
805
  writePlanContractStore(options, { schemaVersion: 1, updatedAt: at, contracts });
@@ -996,6 +839,83 @@ export function createPlanContract(options: PlanContractStoreOptions, input: Cre
996
839
  });
997
840
  }
998
841
 
842
+ export function createPlanSemanticContext(options: PlanContractStoreOptions, input: CreatePlanSemanticInput): PlanContract {
843
+ return withPlanAdmissionLock(options, () => {
844
+ assertRequirementReference(options, input.requirementId);
845
+ const store = readPlanContractStore(options);
846
+ const planId = sanitizeFileComponent(input.planId);
847
+ if (!String(input.planId ?? '').trim() || planId === 'unknown') throw new Error('plan_id is required');
848
+ if (store.contracts.some((existing) => existing.planId === planId)) throw new Error(`plan contract already exists: ${planId}`);
849
+
850
+ // Thin Plan scope identifies one semantic planning lineage; it is not an
851
+ // execution mutex. The same Requirement + scope has one current Plan head,
852
+ // while genuinely independent goals remain free to use distinct scopes.
853
+ const scopeKey = normalizeScopeKey(input.scopeKey?.trim() || planId);
854
+ const requirementId = input.requirementId?.trim().slice(0, 160) || undefined;
855
+ const existingLineage = store.contracts.find((existing) =>
856
+ existing.planId !== planId
857
+ && isCurrentPlanContract(existing)
858
+ && existing.scopeKey === scopeKey
859
+ && (existing.requirementId?.trim() || undefined) === requirementId);
860
+ if (existingLineage) {
861
+ throw new Error(`PLAN_SEMANTIC_LINEAGE_ALREADY_EXISTS:${scopeKey}:${existingLineage.planId}`);
862
+ }
863
+
864
+ const goal = String(input.goal ?? '').trim().slice(0, 2_000);
865
+ if (!goal) throw new Error('PLAN_GOAL_REQUIRED');
866
+ const requirementBasisRevision = input.requirementBasisRevision === undefined
867
+ ? undefined
868
+ : (() => {
869
+ if (!Number.isInteger(input.requirementBasisRevision) || Number(input.requirementBasisRevision) < 1) {
870
+ throw new Error('PLAN_REQUIREMENT_BASIS_REVISION_INVALID');
871
+ }
872
+ return Number(input.requirementBasisRevision);
873
+ })();
874
+ const sourceBasisRevision = String(input.sourceBasisRevision ?? '').trim().slice(0, 200);
875
+ const items = normalizePlanSemanticItems(input.items);
876
+ const at = nowIso(options);
877
+ const semanticContext = {
878
+ requirementBasisRevision,
879
+ sourceBasisRevision,
880
+ goal,
881
+ nonGoals: bounded(input.nonGoals, 20),
882
+ assumptions: bounded(input.assumptions, 30),
883
+ resolvedDecisions: bounded(input.resolvedDecisions, 30),
884
+ stopConditions: bounded(input.stopConditions, 20),
885
+ replanConditions: bounded(input.replanConditions, 20),
886
+ integrationStrategy: input.integrationStrategy?.trim().slice(0, 1_000) || undefined,
887
+ items,
888
+ };
889
+ const plan: PlanContract = {
890
+ schemaVersion: 1,
891
+ planId,
892
+ revision: 1,
893
+ semanticRevision: 1,
894
+ semanticUpdatedAt: at,
895
+ semanticContext,
896
+ repoId: input.repoId,
897
+ requirementId,
898
+ scopeKey,
899
+ sourceRevision: sourceBasisRevision,
900
+ goal,
901
+ nonGoals: [...semanticContext.nonGoals],
902
+ assumptions: [...semanticContext.assumptions],
903
+ resolvedDecisions: [...semanticContext.resolvedDecisions],
904
+ stopConditions: [...semanticContext.stopConditions],
905
+ replanConditions: [...semanticContext.replanConditions],
906
+ integrationStrategy: semanticContext.integrationStrategy,
907
+ // Legacy lifecycle projection only. Thin Plan items never become PlanSteps.
908
+ status: 'approved',
909
+ steps: [],
910
+ evidenceRefs: [],
911
+ createdAt: at,
912
+ updatedAt: at,
913
+ };
914
+ writePlanContractStore(options, { schemaVersion: 1, updatedAt: at, contracts: [plan, ...store.contracts] });
915
+ return plan;
916
+ });
917
+ }
918
+
999
919
  function admitPlanContractUnlocked(options: PlanContractStoreOptions, input: AdmitPlanContractInput): AdmitPlanContractResult {
1000
920
  assertRequirementReference(options, input.requirementId);
1001
921
  const plans = listPlanContracts({ ...options, status: 'all', limit: 100 });
@@ -1025,6 +945,113 @@ export function getPlanContract(options: PlanContractStoreOptions, planId: strin
1025
945
  return readPlanContractStore(options).contracts.find((contract) => contract.planId === sanitizeFileComponent(planId));
1026
946
  }
1027
947
 
948
+ export function revisePlanSemanticContext(
949
+ options: PlanContractStoreOptions,
950
+ planIdInput: string,
951
+ input: RevisePlanSemanticInput,
952
+ ): PlanContract {
953
+ if (!Number.isInteger(input.expectedRevision) || input.expectedRevision < 1) throw new Error('PLAN_EXPECTED_REVISION_INVALID');
954
+ const planId = sanitizeFileComponent(planIdInput);
955
+ const requestedSuccessor = input.supersededBy === undefined
956
+ ? undefined
957
+ : (() => {
958
+ const raw = String(input.supersededBy).trim();
959
+ if (!raw) throw new Error('PLAN_SUCCESSOR_REQUIRED');
960
+ const successorId = sanitizeFileComponent(raw);
961
+ if (!successorId || successorId === 'unknown') throw new Error('PLAN_SUCCESSOR_INVALID');
962
+ if (successorId === planId) throw new Error(`PLAN_SUCCESSOR_SELF_REFERENCE:${planId}`);
963
+ return successorId;
964
+ })();
965
+ return withPlanAdmissionLock(options, () => {
966
+ const applyRevision = (current: PlanContract, at: string): { previous: PlanSemanticView; next: PlanContract } => {
967
+ const revision = currentPlanSemanticRevision(current);
968
+ if (revision !== input.expectedRevision) throw new Error(`PLAN_REVISION_CONFLICT:${planId}:expected=${input.expectedRevision}:actual=${revision}`);
969
+ const previous = planSemanticView(current);
970
+ const existingSuccessor = current.supersededBy?.trim() || undefined;
971
+ if (existingSuccessor && requestedSuccessor && existingSuccessor !== requestedSuccessor) {
972
+ throw new Error(`PLAN_SUCCESSOR_IMMUTABLE:${planId}:current=${existingSuccessor}:requested=${requestedSuccessor}`);
973
+ }
974
+ const supersededBy = requestedSuccessor ?? existingSuccessor;
975
+ const goal = input.goal === undefined ? previous.goal : String(input.goal).trim().slice(0, 2_000);
976
+ if (!goal) throw new Error('PLAN_GOAL_REQUIRED');
977
+ const requirementBasisRevision = input.requirementBasisRevision === undefined
978
+ ? previous.requirementBasisRevision
979
+ : (() => {
980
+ if (!Number.isInteger(input.requirementBasisRevision) || Number(input.requirementBasisRevision) < 1) throw new Error('PLAN_REQUIREMENT_BASIS_REVISION_INVALID');
981
+ return Number(input.requirementBasisRevision);
982
+ })();
983
+ const sourceBasisRevision = input.sourceBasisRevision === undefined ? previous.sourceBasisRevision : String(input.sourceBasisRevision).trim().slice(0, 200);
984
+ const items = input.items === undefined ? previous.items : normalizePlanSemanticItems(input.items);
985
+ const semanticContext = {
986
+ requirementBasisRevision,
987
+ sourceBasisRevision,
988
+ goal,
989
+ nonGoals: input.nonGoals === undefined ? previous.nonGoals : bounded(input.nonGoals, 20),
990
+ assumptions: input.assumptions === undefined ? previous.assumptions : bounded(input.assumptions, 30),
991
+ resolvedDecisions: input.resolvedDecisions === undefined ? previous.resolvedDecisions : bounded(input.resolvedDecisions, 30),
992
+ stopConditions: input.stopConditions === undefined ? previous.stopConditions : bounded(input.stopConditions, 20),
993
+ replanConditions: input.replanConditions === undefined ? previous.replanConditions : bounded(input.replanConditions, 20),
994
+ integrationStrategy: input.integrationStrategy === undefined ? previous.integrationStrategy : input.integrationStrategy === null ? undefined : String(input.integrationStrategy).trim().slice(0, 1_000) || undefined,
995
+ items,
996
+ };
997
+ return {
998
+ previous,
999
+ next: {
1000
+ ...current,
1001
+ ...(supersededBy ? { supersededBy } : {}),
1002
+ semanticRevision: revision + 1,
1003
+ semanticUpdatedAt: at,
1004
+ semanticContext,
1005
+ updatedAt: at,
1006
+ },
1007
+ };
1008
+ };
1009
+
1010
+ if (sqliteBacked(options)) {
1011
+ return withControlPlaneTransaction(options.controllerHome, (database) => {
1012
+ const scope = requirePlanContractStoreScopeKey(options);
1013
+ const currentRecord = readControlPlaneRecordWithinTransaction<PlanContract>(database, 'plan_contract', scope, planId);
1014
+ if (!currentRecord) throw new Error(`plan contract not found: ${planId}`);
1015
+ if (requestedSuccessor) {
1016
+ const successorRecord = readControlPlaneRecordWithinTransaction<PlanContract>(database, 'plan_contract', scope, requestedSuccessor);
1017
+ if (!successorRecord) throw new Error(`PLAN_SUCCESSOR_NOT_FOUND:${requestedSuccessor}`);
1018
+ if (!isCurrentPlanContract(successorRecord.value)) throw new Error(`PLAN_SUCCESSOR_NOT_CURRENT:${requestedSuccessor}`);
1019
+ }
1020
+ const at = nowIso(options);
1021
+ const { previous, next } = applyRevision(currentRecord.value, at);
1022
+ const semanticRevisionKey = `${planId}:r${previous.revision}`;
1023
+ if (!readControlPlaneRecordWithinTransaction<PlanSemanticRevisionRecord>(database, 'plan_semantic_revision', requirePlanContractStoreScopeKey(options), semanticRevisionKey)) {
1024
+ writeControlPlaneRecordWithinTransaction(database, {
1025
+ namespace: 'plan_semantic_revision', scope, key: semanticRevisionKey, schemaVersion: 1,
1026
+ value: { schemaVersion: 1, ...previous, recordedAt: at },
1027
+ action: 'plan_semantic_revision_archived', expectedRevision: null,
1028
+ });
1029
+ }
1030
+ return writeControlPlaneRecordWithinTransaction(database, {
1031
+ namespace: 'plan_contract', scope, key: planId, schemaVersion: 1,
1032
+ value: next, action: 'plan_semantic_revised', expectedRevision: currentRecord.revision,
1033
+ }).value;
1034
+ });
1035
+ }
1036
+
1037
+ const store = readPlanContractStore(options);
1038
+ const index = store.contracts.findIndex((contract) => contract.planId === planId);
1039
+ if (index < 0) throw new Error(`plan contract not found: ${planId}`);
1040
+ if (requestedSuccessor) {
1041
+ const successor = store.contracts.find((contract) => contract.planId === requestedSuccessor);
1042
+ if (!successor) throw new Error(`PLAN_SUCCESSOR_NOT_FOUND:${requestedSuccessor}`);
1043
+ if (!isCurrentPlanContract(successor)) throw new Error(`PLAN_SUCCESSOR_NOT_CURRENT:${requestedSuccessor}`);
1044
+ }
1045
+ const at = nowIso(options);
1046
+ const { previous, next } = applyRevision(store.contracts[index]!, at);
1047
+ appendJsonPlanSemanticRevisionRecord(options, { schemaVersion: 1, ...previous, recordedAt: at });
1048
+ const contracts = [...store.contracts];
1049
+ contracts[index] = next;
1050
+ writePlanContractStore(options, { schemaVersion: 1, updatedAt: at, contracts });
1051
+ return next;
1052
+ });
1053
+ }
1054
+
1028
1055
  export function isCurrentPlanContract(contract: PlanContract): boolean {
1029
1056
  return !isTerminalPlanContractStatus(contract.status) && !contract.supersededBy?.trim();
1030
1057
  }
@@ -1053,570 +1080,3 @@ export function summarizePlanContract(plan: PlanContract): PlanContractSummary {
1053
1080
  updatedAt: plan.updatedAt,
1054
1081
  };
1055
1082
  }
1056
-
1057
- function approvalErrors(plan: PlanContract, allPlans: readonly PlanContract[]): string[] {
1058
- const errors = draftContentErrors(plan).map((error) => `${error} before approval`);
1059
- const scopeIsCommitted = (status: PlanContractStatus): boolean => [
1060
- 'approved',
1061
- 'executing',
1062
- 'replanning',
1063
- 'verifying',
1064
- 'ready_to_finalize',
1065
- ].includes(status);
1066
- const stagedPredecessorId = plan.supersedes?.length === 1 ? plan.supersedes[0] : undefined;
1067
- if (allPlans.some((existing) => existing.planId !== plan.planId && existing.scopeKey === plan.scopeKey
1068
- && scopeIsCommitted(existing.status)
1069
- && !(existing.planId === stagedPredecessorId && existing.status === 'replanning'))) {
1070
- errors.push(`active plan already owns scope_key ${plan.scopeKey}`);
1071
- }
1072
- errors.push(...obligationContinuityErrors(plan, allPlans));
1073
- return [...new Set(errors)];
1074
- }
1075
-
1076
- function approvePendingPlanRevisionUnlocked(
1077
- options: PlanContractStoreOptions & { controllerHome: string; repoId: string },
1078
- current: PlanContract,
1079
- ): PlanContract {
1080
- const pending = current.pendingRevision;
1081
- if (!pending) throw new Error('PLAN_PENDING_REVISION_REQUIRED');
1082
- const at = nowIso(options);
1083
- return withControlPlaneTransaction(options.controllerHome, (database) => {
1084
- const currentRecord = readControlPlaneRecordWithinTransaction<PlanContract>(database, 'plan_contract', options.repoId, current.planId);
1085
- if (!currentRecord?.value.pendingRevision) throw new Error('PLAN_PENDING_REVISION_STALE');
1086
- const predecessor = currentRecord.value;
1087
- const staged = predecessor.pendingRevision!;
1088
- const archived = planRevisionRecord(predecessor, { recordedAt: at, reason: 'approved Plan revision', requestedRevisionLabel: staged.requestedRevisionLabel });
1089
- let candidate = planFromPendingRevision(predecessor, staged, at);
1090
- candidate = { ...candidate, status: 'approved', steps: projectDependencyReadySteps(candidate.steps) };
1091
- const allPlans = readPlanContractStore(options).contracts;
1092
- assertRevisionScopeAuthority(predecessor, allPlans);
1093
- const continuityErrors = revisionContinuityErrors(predecessor, candidate, allPlans);
1094
- if (continuityErrors.length > 0) throw new Error(`PLAN_OBLIGATION_CONTINUITY_REQUIRED: ${continuityErrors.join('; ')}`);
1095
- const revalidatedCarries = deriveRequiredRevisionDeliveryCarries(options, predecessor, candidate, at);
1096
- const stagedCarryKeys = new Set((staged.deliveryCarries ?? []).map((carry) => `${carry.successorStepId}:${carry.workId}:${carry.completionReceiptId}`));
1097
- const revalidatedCarryKeys = new Set(revalidatedCarries.map((carry) => `${carry.successorStepId}:${carry.workId}:${carry.completionReceiptId}`));
1098
- for (const carry of revalidatedCarries) {
1099
- const key = `${carry.successorStepId}:${carry.workId}:${carry.completionReceiptId}`;
1100
- if (!stagedCarryKeys.has(key)) throw new Error(`PLAN_DELIVERY_CARRY_STAGING_MISMATCH: ${carry.successorStepId}`);
1101
- }
1102
- for (const carry of staged.deliveryCarries ?? []) {
1103
- const key = `${carry.successorStepId}:${carry.workId}:${carry.completionReceiptId}`;
1104
- if (!revalidatedCarryKeys.has(key)) throw new Error(`PLAN_DELIVERY_CARRY_INVALID: ${carry.successorStepId}`);
1105
- }
1106
-
1107
- const workWrites: Array<{ value: WorkContract; revision: number; action: string }> = [];
1108
- let hasExecuting = false;
1109
- let hasValidating = false;
1110
- const steps = [...candidate.steps];
1111
- // Preserve already completed immutable semantics without replay.
1112
- for (let index = 0; index < steps.length; index += 1) {
1113
- const nextStep = steps[index]!;
1114
- const previousStep = predecessor.steps.find((step) => step.id === nextStep.id);
1115
- if (!previousStep) continue;
1116
- if (previousStep.status === 'completed' && samePlanStepExecutionContract(previousStep, nextStep)
1117
- && sameOrderedStrings(previousStep.acceptanceCriteria, nextStep.acceptanceCriteria)) {
1118
- steps[index] = { ...nextStep, status: 'completed', workId: previousStep.workId, evidenceRefs: previousStep.evidenceRefs };
1119
- } else if (previousStep.status === 'validating' && deliveryCarryForStep(options, predecessor, previousStep, candidate, nextStep, at)) {
1120
- steps[index] = { ...nextStep, status: 'validating', workId: previousStep.workId, evidenceRefs: previousStep.evidenceRefs };
1121
- hasValidating = true;
1122
- }
1123
- }
1124
- candidate = { ...candidate, steps };
1125
-
1126
- const workRecords = listAllControlPlaneRecordsWithinTransaction<WorkContract>(database, { namespace: 'work_contract', scope: options.repoId })
1127
- .filter((record) => record.value.planId === predecessor.planId && !isTerminalWorkContractStatus(record.value.status));
1128
- for (const workRecord of workRecords) {
1129
- const previousStep = predecessor.steps.find((step) => workMatchesPlanStep(workRecord.value, predecessor, step));
1130
- const nextIndex = previousStep ? candidate.steps.findIndex((step) => step.id === previousStep.id) : -1;
1131
- const nextStep = nextIndex >= 0 ? candidate.steps[nextIndex] : undefined;
1132
- if (previousStep && nextStep && previousStep.workId === workRecord.value.workId
1133
- && samePlanStepExecutionContract(previousStep, nextStep)
1134
- && sameOrderedStrings(previousStep.acceptanceCriteria, nextStep.acceptanceCriteria)) {
1135
- const refreshed = refreshPlanBoundWorkRevision(workRecord.value, {
1136
- planId: predecessor.planId, planStepId: nextStep.id, planSourceRevision: candidate.sourceRevision,
1137
- allowedPaths: nextStep.allowedPaths, forbiddenPaths: nextStep.forbiddenPaths, checks: nextStep.checks,
1138
- recordedAt: at, reason: `Plan revision r${currentPlanRevision(predecessor)} -> r${staged.revision} preserved the exact Work contract.`,
1139
- });
1140
- const nextSteps = [...candidate.steps];
1141
- nextSteps[nextIndex] = { ...nextStep, status: 'executing', workId: refreshed.workId, evidenceRefs: previousStep.evidenceRefs };
1142
- candidate = { ...candidate, steps: nextSteps };
1143
- hasExecuting = true;
1144
- workWrites.push({ value: refreshed, revision: workRecord.revision, action: 'plan_revision_work_refreshed' });
1145
- } else {
1146
- const retired = retirePlanBoundWorkContract(workRecord.value, {
1147
- predecessorPlanId: predecessor.planId, recordedAt: at, reason: `Plan revision r${currentPlanRevision(predecessor)} -> r${staged.revision} changed the frozen Work execution contract.`,
1148
- });
1149
- workWrites.push({ value: retired, revision: workRecord.revision, action: 'plan_revision_work_retired' });
1150
- }
1151
- }
1152
- const allCompleted = candidate.steps.length > 0 && candidate.steps.every((step) => step.status === 'completed');
1153
- candidate = { ...candidate, deliveryCarries: undefined, status: hasExecuting ? 'executing' : hasValidating ? 'verifying' : allCompleted ? 'ready_to_finalize' : 'approved', updatedAt: at };
1154
- const revisionKey = planRevisionRecordKey(predecessor.planId, currentPlanRevision(predecessor));
1155
- if (readControlPlaneRecordWithinTransaction<PlanRevisionRecord>(database, 'plan_revision', options.repoId, revisionKey)) {
1156
- throw new Error(`PLAN_REVISION_ALREADY_EXISTS: ${predecessor.planId}:r${currentPlanRevision(predecessor)}`);
1157
- }
1158
- writeControlPlaneRecordWithinTransaction(database, { namespace: 'plan_revision', scope: options.repoId, key: revisionKey, schemaVersion: 1, value: archived, action: 'plan_revision_archived', expectedRevision: null });
1159
- writeControlPlaneRecordWithinTransaction(database, { namespace: 'plan_contract', scope: options.repoId, key: predecessor.planId, schemaVersion: 1, value: candidate, action: 'plan_revision_approved', expectedRevision: currentRecord.revision });
1160
- for (const write of workWrites) {
1161
- writeControlPlaneRecordWithinTransaction(database, { namespace: 'work_contract', scope: options.repoId, key: write.value.workId, schemaVersion: 2, value: write.value, action: write.action, expectedRevision: write.revision });
1162
- }
1163
- return candidate;
1164
- });
1165
- }
1166
-
1167
- function approvePlanContractUnlocked(options: PlanContractStoreOptions, planId: string): PlanContract {
1168
- const store = readPlanContractStore(options);
1169
- const index = store.contracts.findIndex((contract) => contract.planId === sanitizeFileComponent(planId));
1170
- if (index < 0) throw new Error(`plan contract not found: ${sanitizeFileComponent(planId)}`);
1171
- const current = store.contracts[index];
1172
- assertRequirementReference(options, current.requirementId);
1173
- if (current.status === 'replanning' && current.pendingRevision) {
1174
- if (!sqliteBacked(options)) {
1175
- const at = nowIso(options);
1176
- const candidate = { ...planFromPendingRevision(current, current.pendingRevision, at), status: 'approved' as const };
1177
- appendJsonPlanRevisionRecord(options, planRevisionRecord(current, { recordedAt: at, reason: 'approved Plan revision', requestedRevisionLabel: current.pendingRevision.requestedRevisionLabel }));
1178
- const contracts = [...store.contracts];
1179
- contracts[index] = candidate;
1180
- writePlanContractStore(options, { schemaVersion: 1, updatedAt: at, contracts });
1181
- return candidate;
1182
- }
1183
- return approvePendingPlanRevisionUnlocked(options, current);
1184
- }
1185
- if (current.status !== 'draft' && current.status !== 'reviewing') throw new Error(`plan contract ${current.planId} cannot be approved from ${current.status}`);
1186
- const errors = approvalErrors(current, store.contracts);
1187
- if (errors.length > 0) throw new Error(`plan contract ${current.planId} cannot be approved: ${errors.join('; ')}`);
1188
- const at = nowIso(options);
1189
- const materializedCarry = materializePlanStepDeliveryCarries(options, current, store.contracts);
1190
- const next = {
1191
- ...current,
1192
- status: materializedCarry.carriedCount > 0 ? 'verifying' as const : 'approved' as const,
1193
- steps: projectDependencyReadySteps(materializedCarry.steps),
1194
- updatedAt: at,
1195
- };
1196
- const stagedPredecessor = current.supersedes?.length === 1
1197
- ? store.contracts.find((candidate) => candidate.planId === current.supersedes![0] && candidate.status === 'replanning')
1198
- : undefined;
1199
- if (stagedPredecessor) {
1200
- const predecessorNext: PlanContract = { ...stagedPredecessor, status: 'superseded', supersededBy: current.planId, supersessionReason: 'extend_existing', updatedAt: at };
1201
- const contracts = store.contracts.map((candidate) => candidate.planId === predecessorNext.planId ? predecessorNext : candidate.planId === next.planId ? next : candidate);
1202
- return writePlanSupersessionWithWorkRetirement(options, { schemaVersion: 1, updatedAt: at, contracts }, predecessorNext, next, at);
1203
- }
1204
- const contracts = [...store.contracts];
1205
- contracts[index] = next;
1206
- writePlanContractStore(options, { schemaVersion: 1, updatedAt: at, contracts });
1207
- return next;
1208
- }
1209
-
1210
- export function approvePlanContract(options: PlanContractStoreOptions, planId: string): PlanContract {
1211
- return withPlanAdmissionLock(options, () => approvePlanContractUnlocked(options, planId));
1212
- }
1213
-
1214
- export async function approvePlanContractAsync(options: PlanContractStoreOptions, planId: string): Promise<PlanContract> {
1215
- return await withPlanAdmissionLockAsync(options, () => approvePlanContractUnlocked(options, planId));
1216
- }
1217
-
1218
- export function supersedePlanContract(options: PlanContractStoreOptions, planId: string, supersededBy: string, reason = 'explicit_supersession'): PlanContract {
1219
- return withPlanAdmissionLock(options, () => {
1220
- const store = readPlanContractStore(options);
1221
- const index = store.contracts.findIndex((contract) => contract.planId === sanitizeFileComponent(planId));
1222
- if (index < 0) throw new Error(`plan contract not found: ${sanitizeFileComponent(planId)}`);
1223
- const current = store.contracts[index]!;
1224
- if (isTerminalPlanContractStatus(current.status)) throw new Error(`plan contract ${current.planId} is terminal (${current.status})`);
1225
- const replacement = sanitizeFileComponent(supersededBy);
1226
- if (!replacement || replacement === 'unknown') throw new Error('superseded_by is required');
1227
- if (replacement === current.planId) throw new Error('PLAN_SUCCESSOR_ID_MUST_CHANGE');
1228
- const successor = store.contracts.find((contract) => contract.planId === replacement);
1229
- if (!successor) throw new Error(`PLAN_SUCCESSOR_NOT_FOUND: ${replacement}`);
1230
- const successorCandidate = { ...successor, supersedes: [...new Set([...(successor.supersedes ?? []), current.planId])] };
1231
- const continuityErrors = obligationContinuityErrors(successorCandidate, store.contracts);
1232
- if (continuityErrors.length > 0) throw new Error(`PLAN_OBLIGATION_CONTINUITY_REQUIRED: ${continuityErrors.join('; ')}`);
1233
- const at = nowIso(options);
1234
- const boundedReason = String(reason ?? '').trim().slice(0, 500) || 'explicit_supersession';
1235
- const next = {
1236
- ...current,
1237
- status: 'superseded' as const,
1238
- supersededBy: successor.planId,
1239
- supersessionReason: boundedReason,
1240
- updatedAt: at,
1241
- };
1242
- const successorNext = {
1243
- ...successorCandidate,
1244
- updatedAt: at,
1245
- };
1246
- const contracts = [...store.contracts];
1247
- contracts[index] = next;
1248
- contracts[contracts.findIndex((contract) => contract.planId === successor.planId)] = successorNext;
1249
- const nextStore = { schemaVersion: 1 as const, updatedAt: at, contracts };
1250
- writePlanSupersessionWithWorkRetirement(options, nextStore, next, successorNext, at);
1251
- return next;
1252
- });
1253
- }
1254
-
1255
- /**
1256
- * Maintenance reconciliation for historical V2 state created before Plan ->
1257
- * Work authority retirement was atomic. It terminalizes only non-terminal Work
1258
- * whose bound Plan is already terminal; it never deletes history and never
1259
- * touches Work bound to a current Plan.
1260
- */
1261
- export function retireTerminalPlanBoundWorkAuthorities(options: PlanContractStoreOptions): string[] {
1262
- if (!sqliteBacked(options)) return [];
1263
- const terminalPlans = new Map(
1264
- readPlanContractStore(options).contracts
1265
- .filter((plan) => isTerminalPlanContractStatus(plan.status))
1266
- .map((plan) => [plan.planId, plan] as const),
1267
- );
1268
- if (terminalPlans.size === 0) return [];
1269
- const candidates = listControlPlaneRecords<WorkContract>(options.controllerHome, {
1270
- namespace: 'work_contract',
1271
- scope: options.repoId,
1272
- limit: 5_000,
1273
- }).filter((record) => record.value.planId && terminalPlans.has(record.value.planId) && !isTerminalWorkContractStatus(record.value.status));
1274
- if (candidates.length === 0) return [];
1275
- const retired: string[] = [];
1276
- const at = nowIso(options);
1277
- withControlPlaneTransaction(options.controllerHome, (database) => {
1278
- for (const candidate of candidates) {
1279
- const current = readControlPlaneRecordWithinTransaction<WorkContract>(database, 'work_contract', options.repoId, candidate.key);
1280
- const planId = current?.value.planId;
1281
- if (!current || !planId || isTerminalWorkContractStatus(current.value.status)) continue;
1282
- const plan = terminalPlans.get(planId);
1283
- if (!plan || !isTerminalPlanContractStatus(plan.status)) continue;
1284
- const next = retirePlanBoundWorkContract(current.value, {
1285
- predecessorPlanId: plan.planId,
1286
- successorPlanId: plan.supersededBy,
1287
- recordedAt: at,
1288
- reason: `maintenance reconciliation: owning Plan is ${plan.status}`,
1289
- });
1290
- writeControlPlaneRecordWithinTransaction(database, {
1291
- namespace: 'work_contract',
1292
- scope: options.repoId,
1293
- key: next.workId,
1294
- schemaVersion: 2,
1295
- value: next,
1296
- action: 'work_terminal_plan_authority_reconciled',
1297
- expectedRevision: current.revision,
1298
- });
1299
- retired.push(next.workId);
1300
- }
1301
- });
1302
- return retired.sort();
1303
- }
1304
-
1305
- export interface ReplanActivePlanBoundWorkScopeInput {
1306
- planId: string;
1307
- stepId: string;
1308
- workId: string;
1309
- requestedRevisionLabel: string;
1310
- sourceRevision: string;
1311
- allowedPaths: string[];
1312
- reason: string;
1313
- }
1314
-
1315
- export interface ReplanActivePlanBoundWorkScopeResult {
1316
- priorPlan: PlanContract;
1317
- currentPlan: PlanContract;
1318
- work: WorkContract;
1319
- }
1320
-
1321
- /**
1322
- * Atomically advance one executing Plan revision while retaining its exact
1323
- * active Work binding. This repair is intentionally scope-only: objective,
1324
- * acceptance, checks, forbidden paths, dependencies, Plan identity, and Work
1325
- * identity remain unchanged; allowed paths may only widen.
1326
- */
1327
- export function replanActivePlanBoundWorkScope(
1328
- options: PlanContractStoreOptions,
1329
- input: ReplanActivePlanBoundWorkScopeInput,
1330
- ): ReplanActivePlanBoundWorkScopeResult {
1331
- if (!sqliteBacked(options)) throw new Error('PLAN_WORK_REPLAN_REQUIRES_CONTROLLER_STORE');
1332
- const planId = sanitizeFileComponent(input.planId);
1333
- const requestedRevisionLabel = sanitizeFileComponent(input.requestedRevisionLabel);
1334
- const stepId = sanitizeFileComponent(input.stepId);
1335
- const workId = sanitizeFileComponent(input.workId);
1336
- const sourceRevision = input.sourceRevision.trim();
1337
- const reason = input.reason.trim();
1338
- if (!requestedRevisionLabel || requestedRevisionLabel === 'unknown' || requestedRevisionLabel === planId) throw new Error('PLAN_REVISION_LABEL_MUST_CHANGE');
1339
- if (!sourceRevision) throw new Error('PLAN_WORK_REPLAN_SOURCE_REVISION_REQUIRED');
1340
- if (!reason) throw new Error('PLAN_WORK_REPLAN_REASON_REQUIRED');
1341
- const requestedAllowedPaths = [...new Set(input.allowedPaths.map((value) => value.trim()).filter(Boolean))].slice(0, 50);
1342
-
1343
- return withPlanAdmissionLock(options, () => {
1344
- const observed = getPlanContract(options, planId);
1345
- if (!observed) throw new Error(`plan contract not found: ${planId}`);
1346
- assertRequirementReference(options, observed.requirementId);
1347
- return withControlPlaneTransaction(options.controllerHome, (database) => {
1348
- const planRecord = readControlPlaneRecordWithinTransaction<PlanContract>(database, 'plan_contract', options.repoId, planId);
1349
- if (!planRecord) throw new Error(`plan contract not found: ${planId}`);
1350
- const priorPlan = planRecord.value;
1351
- if (priorPlan.supersededBy || isTerminalPlanContractStatus(priorPlan.status)) {
1352
- throw new Error(`PLAN_WORK_REPLAN_PREDECESSOR_TERMINAL: ${priorPlan.planId}:${priorPlan.status}`);
1353
- }
1354
- if (priorPlan.status !== 'executing' && priorPlan.status !== 'replanning') {
1355
- throw new Error(`PLAN_WORK_REPLAN_STATUS_INVALID: ${priorPlan.planId}:${priorPlan.status}`);
1356
- }
1357
- const stepIndex = priorPlan.steps.findIndex((candidate) => candidate.id === stepId);
1358
- if (stepIndex < 0) throw new Error(`PLAN_STEP_NOT_FOUND: ${stepId}`);
1359
- const step = priorPlan.steps[stepIndex]!;
1360
- if (step.status !== 'executing' || step.workId !== workId) {
1361
- throw new Error(`PLAN_WORK_REPLAN_STEP_BINDING_MISMATCH: ${priorPlan.planId}/${stepId}:${step.status}:${step.workId ?? 'none'}`);
1362
- }
1363
- for (const path of step.allowedPaths) {
1364
- if (!requestedAllowedPaths.includes(path)) throw new Error(`PLAN_WORK_REPLAN_SCOPE_NARROWING_FORBIDDEN: ${path}`);
1365
- }
1366
- const widened = requestedAllowedPaths.some((path) => !step.allowedPaths.includes(path));
1367
- if (!widened) throw new Error('PLAN_WORK_REPLAN_SCOPE_NOT_WIDENED');
1368
- const workRecord = readControlPlaneRecordWithinTransaction<WorkContract>(database, 'work_contract', options.repoId, workId);
1369
- if (!workRecord) throw new Error(`work contract not found: ${workId}`);
1370
- // Replan runs inside the Plan + Work transaction. Normalize the exact
1371
- // row read there so a legacy Work missing the first-class review
1372
- // checkpoint is migrated by the canonical Work authority before the
1373
- // scope-only refresh validates phase evidence.
1374
- const work = canonicalizeWorkContractForAuthority(workRecord.value);
1375
- if (work.requirementId !== priorPlan.requirementId) throw new Error('PLAN_WORK_REPLAN_REQUIREMENT_MISMATCH');
1376
- const at = nowIso(options);
1377
- const priorRevision = currentPlanRevision(priorPlan);
1378
- const archived = planRevisionRecord(priorPlan, { recordedAt: at, reason, requestedRevisionLabel });
1379
- const steps = [...priorPlan.steps];
1380
- steps[stepIndex] = {
1381
- ...step,
1382
- allowedPaths: requestedAllowedPaths,
1383
- evidenceRefs: [{
1384
- title: 'scope-only Plan revision',
1385
- summary: `${priorPlan.planId} r${priorRevision} -> r${priorRevision + 1}: ${reason}`.slice(0, 2_000),
1386
- detailLevel: 'summary' as const,
1387
- }, ...step.evidenceRefs].slice(0, 20),
1388
- };
1389
- const currentPlan: PlanContract = {
1390
- ...priorPlan,
1391
- revision: priorRevision + 1,
1392
- sourceRevision,
1393
- status: 'executing',
1394
- steps,
1395
- evidenceRefs: [{
1396
- title: 'active Work Plan revision advanced',
1397
- summary: `${priorPlan.planId} retained exact Work ${workId}; current revision advanced r${priorRevision} -> r${priorRevision + 1} without creating another Plan authority.`.slice(0, 2_000),
1398
- detailLevel: 'summary' as const,
1399
- }, ...priorPlan.evidenceRefs].slice(0, 20),
1400
- updatedAt: at,
1401
- };
1402
- const workNext = refreshPlanBoundWorkRevision(work, {
1403
- planId: priorPlan.planId, planStepId: stepId, planSourceRevision: sourceRevision,
1404
- allowedPaths: requestedAllowedPaths, forbiddenPaths: step.forbiddenPaths, checks: step.checks, recordedAt: at, reason,
1405
- });
1406
- const revisionKey = planRevisionRecordKey(priorPlan.planId, priorRevision);
1407
- const existingRevision = readControlPlaneRecordWithinTransaction<PlanRevisionRecord>(database, 'plan_revision', options.repoId, revisionKey);
1408
- if (existingRevision) throw new Error(`PLAN_REVISION_ALREADY_EXISTS: ${priorPlan.planId}:r${priorRevision}`);
1409
- writeControlPlaneRecordWithinTransaction(database, {
1410
- namespace: 'plan_revision', scope: options.repoId, key: revisionKey, schemaVersion: 1,
1411
- value: archived, action: 'plan_revision_archived', expectedRevision: null,
1412
- });
1413
- writeControlPlaneRecordWithinTransaction(database, {
1414
- namespace: 'plan_contract', scope: options.repoId, key: priorPlan.planId, schemaVersion: 1,
1415
- value: currentPlan, action: 'plan_current_revision_advanced', expectedRevision: planRecord.revision,
1416
- });
1417
- writeControlPlaneRecordWithinTransaction(database, {
1418
- namespace: 'work_contract', scope: options.repoId, key: workNext.workId, schemaVersion: 2,
1419
- value: workNext, action: 'plan_revision_work_refreshed', expectedRevision: workRecord.revision,
1420
- });
1421
- return { priorPlan, currentPlan, work: workNext };
1422
- });
1423
- });
1424
- }
1425
-
1426
- /**
1427
- * Atomically reserve one eligible step for one WorkContract. This is the
1428
- * Plan-to-Work concurrency boundary: competing callers cannot bind the same
1429
- * step, and a stale source revision causes no WorkContract side effect.
1430
- */
1431
- export function claimPlanStepForWork(
1432
- options: PlanContractStoreOptions,
1433
- input: { planId: string; stepId: string; workId: string; sourceRevision: string },
1434
- ): PlanContract {
1435
- return updatePlanContract(options, input.planId, (current) => {
1436
- assertRequirementReference(options, current.requirementId);
1437
- if (current.status !== 'approved' && current.status !== 'executing') {
1438
- throw new Error(`PLAN_NOT_EXECUTABLE: ${current.planId} is ${current.status}`);
1439
- }
1440
- if (current.sourceRevision !== input.sourceRevision) {
1441
- return { ...current, status: 'invalidated_by_drift', updatedAt: nowIso(options) };
1442
- }
1443
- const stepIndex = current.steps.findIndex((step) => step.id === sanitizeFileComponent(input.stepId));
1444
- if (stepIndex < 0) throw new Error(`PLAN_STEP_NOT_FOUND: ${input.stepId}`);
1445
- const step = current.steps[stepIndex];
1446
- if (step.status === 'executing' || step.workId) throw new Error(`PLAN_STEP_ALREADY_ACTIVE: ${step.id}`);
1447
- if (step.status === 'completed') throw new Error(`PLAN_STEP_ALREADY_COMPLETED: ${step.id}`);
1448
- const unresolved = step.dependencies.filter((dependency) => current.steps.find((candidate) => candidate.id === dependency)?.status !== 'completed');
1449
- if (unresolved.length > 0) throw new Error(`PLAN_STEP_DEPENDENCIES_PENDING: ${unresolved.join(', ')}`);
1450
- const at = nowIso(options);
1451
- const steps = [...current.steps];
1452
- steps[stepIndex] = { ...step, status: 'executing', workId: input.workId };
1453
- return { ...current, status: 'executing', steps, updatedAt: at };
1454
- });
1455
- }
1456
-
1457
- export function repairDanglingPlanStepWorkBinding(
1458
- options: PlanContractStoreOptions,
1459
- input: { planId: string; stepId: string; expectedWorkId: string; reason: string },
1460
- ): PlanContract {
1461
- const expectedWorkId = input.expectedWorkId.trim();
1462
- const reason = input.reason.trim();
1463
- if (!expectedWorkId) throw new Error('PLAN_STEP_REPAIR_EXPECTED_WORK_REQUIRED');
1464
- if (!reason) throw new Error('PLAN_STEP_REPAIR_REASON_REQUIRED');
1465
- return updatePlanContract(options, input.planId, (current) => {
1466
- if (isTerminalPlanContractStatus(current.status)) throw new Error(`PLAN_STEP_REPAIR_PLAN_TERMINAL: ${current.planId} is ${current.status}`);
1467
- const stepIndex = current.steps.findIndex((step) => step.id === sanitizeFileComponent(input.stepId));
1468
- if (stepIndex < 0) throw new Error(`PLAN_STEP_NOT_FOUND: ${input.stepId}`);
1469
- const step = current.steps[stepIndex];
1470
- if (step.workId !== expectedWorkId) {
1471
- throw new Error(`PLAN_STEP_REPAIR_BINDING_CHANGED: ${step.id} is bound to ${step.workId ?? 'no Work'}, expected ${expectedWorkId}`);
1472
- }
1473
- if (step.status !== 'executing') {
1474
- throw new Error(`PLAN_STEP_REPAIR_STATUS_INVALID: ${step.id} is ${step.status}`);
1475
- }
1476
- const at = nowIso(options);
1477
- const steps = [...current.steps];
1478
- steps[stepIndex] = {
1479
- ...step,
1480
- status: 'ready',
1481
- workId: undefined,
1482
- evidenceRefs: [{
1483
- title: 'dangling Work binding repaired',
1484
- summary: `${expectedWorkId}: ${reason}`.slice(0, 2_000),
1485
- detailLevel: 'summary' as const,
1486
- }, ...step.evidenceRefs].slice(0, 20),
1487
- };
1488
- // This is admission-state repair, not semantic replanning: the Plan and its
1489
- // acceptance criteria remain authoritative. Return the exact step to ready
1490
- // so a later start can atomically admit one replacement for the missing
1491
- // record without forcing a second Plan authority.
1492
- return { ...current, status: 'executing', steps, updatedAt: at };
1493
- });
1494
- }
1495
-
1496
- export function repairPlanStepForTechnicalRetry(
1497
- options: PlanContractStoreOptions,
1498
- input: { work: WorkContract; cleanupComplete: boolean; reason: string },
1499
- ): PlanContract {
1500
- const work = input.work;
1501
- const reason = input.reason.trim();
1502
- if (!reason) throw new Error('PLAN_STEP_TECHNICAL_RETRY_REASON_REQUIRED');
1503
- if (work.status !== 'cancelled' && work.status !== 'failed') {
1504
- throw new Error(`PLAN_STEP_TECHNICAL_RETRY_WORK_STATUS_INVALID: ${work.workId}:${work.status}`);
1505
- }
1506
- if (work.phase !== 'cleanup') throw new Error(`PLAN_STEP_TECHNICAL_RETRY_WORK_PHASE_INVALID: ${work.workId}:${work.phase}`);
1507
- if (work.completionReceipt || work.completionOutcome) throw new Error(`PLAN_STEP_TECHNICAL_RETRY_DELIVERY_PRESENT: ${work.workId}`);
1508
- if (work.scopeEvidence?.actualChangedPaths.length !== 0) throw new Error(`PLAN_STEP_TECHNICAL_RETRY_SOURCE_DELTA_PRESENT: ${work.workId}`);
1509
- if (!input.cleanupComplete) throw new Error(`PLAN_STEP_TECHNICAL_RETRY_CLEANUP_INCOMPLETE: ${work.workId}`);
1510
- if (!work.planId?.trim() || !work.planStepId?.trim() || !work.planSourceRevision?.trim()) {
1511
- throw new Error(`PLAN_STEP_TECHNICAL_RETRY_LINEAGE_REQUIRED: ${work.workId}`);
1512
- }
1513
- if (work.status === 'failed') {
1514
- const checks = work.checkRefs ?? [];
1515
- if (checks.length === 0 || checks.some((check) => check.outcome !== 'infrastructure_failure')) {
1516
- throw new Error(`PLAN_STEP_TECHNICAL_RETRY_FAILED_WORK_NOT_INFRASTRUCTURE_ONLY: ${work.workId}`);
1517
- }
1518
- }
1519
- return updatePlanContract(options, work.planId, (current) => {
1520
- if (current.status !== 'replanning') throw new Error(`PLAN_STEP_TECHNICAL_RETRY_PLAN_STATUS_INVALID: ${current.planId}:${current.status}`);
1521
- if (current.sourceRevision !== work.planSourceRevision) {
1522
- throw new Error(`PLAN_STEP_TECHNICAL_RETRY_SOURCE_MISMATCH: ${current.planId}:plan=${current.sourceRevision}:work=${work.planSourceRevision}`);
1523
- }
1524
- if (work.requirementId !== current.requirementId) throw new Error(`PLAN_STEP_TECHNICAL_RETRY_REQUIREMENT_MISMATCH: ${work.workId}`);
1525
- const stepIndex = current.steps.findIndex((step) => step.id === work.planStepId);
1526
- if (stepIndex < 0) throw new Error(`PLAN_STEP_NOT_FOUND: ${work.planStepId}`);
1527
- const step = current.steps[stepIndex]!;
1528
- if (step.status !== 'ready' || step.workId) {
1529
- throw new Error(`PLAN_STEP_TECHNICAL_RETRY_STEP_STATE_INVALID: ${step.id}:${step.status}:${step.workId ?? 'unbound'}`);
1530
- }
1531
- const dependenciesSatisfied = step.dependencies.every((dependency) => current.steps.find((candidate) => candidate.id === dependency)?.status === 'completed');
1532
- if (!dependenciesSatisfied) throw new Error(`PLAN_STEP_TECHNICAL_RETRY_DEPENDENCY_INVALID: ${step.id}`);
1533
- const contractMatches = work.objective === step.objective
1534
- && sameOrderedStrings(work.acceptanceCriteria, step.acceptanceCriteria)
1535
- && sameOrderedStrings(work.allowedPaths, step.allowedPaths)
1536
- && sameOrderedStrings(work.forbiddenPaths, step.forbiddenPaths)
1537
- && sameOrderedStrings(work.checks, step.checks);
1538
- if (!contractMatches) throw new Error(`PLAN_STEP_TECHNICAL_RETRY_CONTRACT_MISMATCH: ${work.workId}`);
1539
- const at = nowIso(options);
1540
- const steps = [...current.steps];
1541
- steps[stepIndex] = {
1542
- ...step,
1543
- evidenceRefs: [{
1544
- title: 'technical Work retry authorized',
1545
- summary: `${work.workId}: ${reason}`.slice(0, 2_000),
1546
- detailLevel: 'summary' as const,
1547
- }, ...step.evidenceRefs].slice(0, 20),
1548
- };
1549
- return { ...current, status: 'executing', steps, updatedAt: at };
1550
- });
1551
- }
1552
-
1553
- export function completePlanStepForWork(
1554
- options: PlanContractStoreOptions,
1555
- input: {
1556
- planId: string;
1557
- stepId: string;
1558
- work: Pick<WorkContract, 'workId' | 'status' | 'phase' | 'evidenceState' | 'completionOutcome' | 'completionReceipt' | 'evidenceRefs'>;
1559
- },
1560
- ): PlanContract {
1561
- return updatePlanContract(options, input.planId, (current) => {
1562
- const stepIndex = current.steps.findIndex((step) => step.id === sanitizeFileComponent(input.stepId));
1563
- if (stepIndex < 0) throw new Error(`PLAN_STEP_NOT_FOUND: ${input.stepId}`);
1564
- const step = current.steps[stepIndex];
1565
- if (step.workId !== input.work.workId) throw new Error(`PLAN_STEP_WORK_MISMATCH: ${input.stepId}`);
1566
- if (step.status === 'completed') return current;
1567
- const delivered = input.work.status === 'completed'
1568
- && input.work.phase === 'cleanup'
1569
- && input.work.evidenceState === 'valid'
1570
- && Boolean(input.work.completionOutcome && input.work.completionOutcome !== 'superseded')
1571
- && Boolean(input.work.completionReceipt);
1572
- const failed = input.work.status === 'failed' || input.work.status === 'cancelled';
1573
- if (!delivered && !failed) throw new Error(`PLAN_STEP_WORK_NOT_TERMINAL: ${input.work.workId}`);
1574
- const at = nowIso(options);
1575
- const steps = [...current.steps];
1576
- steps[stepIndex] = {
1577
- ...step,
1578
- // Work completion proves machine delivery only. The Plan step remains in
1579
- // semantic validation until an explicit Controller acceptance records it.
1580
- // Failed/cancelled Work returns the slice to ready for replanning.
1581
- status: delivered ? 'validating' : 'ready',
1582
- // Successful delivery keeps the exact Work identity through semantic
1583
- // validation. Failed/cancelled Work releases the step authority so the
1584
- // replanning path cannot retain a ghost binding to a terminal Work.
1585
- workId: delivered ? step.workId : undefined,
1586
- evidenceRefs: input.work.evidenceRefs.length > 0 ? input.work.evidenceRefs.slice(0, 20) : step.evidenceRefs,
1587
- };
1588
- return { ...current, status: current.pendingRevision ? 'replanning' : delivered ? 'verifying' : 'replanning', steps, updatedAt: at };
1589
- });
1590
- }
1591
-
1592
- export function acceptPlanStepEvidence(
1593
- options: PlanContractStoreOptions,
1594
- input: { planId: string; stepId: string; reviewer: string; rationale: string; acceptedSourceRevision?: string },
1595
- ): PlanContract {
1596
- const reviewer = input.reviewer.trim();
1597
- const rationale = input.rationale.trim();
1598
- if (!reviewer || !rationale) throw new Error('PLAN_STEP_SEMANTIC_ACCEPTANCE_METADATA_REQUIRED');
1599
- return updatePlanContract(options, input.planId, (current) => {
1600
- const stepIndex = current.steps.findIndex((step) => step.id === sanitizeFileComponent(input.stepId));
1601
- if (stepIndex < 0) throw new Error(`PLAN_STEP_NOT_FOUND: ${input.stepId}`);
1602
- const step = current.steps[stepIndex];
1603
- if (step.status === 'completed') return current;
1604
- if (step.status !== 'validating') throw new Error(`PLAN_STEP_NOT_READY_FOR_SEMANTIC_ACCEPTANCE: ${step.id} is ${step.status}`);
1605
- const steps = [...current.steps];
1606
- steps[stepIndex] = {
1607
- ...step,
1608
- status: 'completed',
1609
- evidenceRefs: [{ title: 'semantic acceptance', summary: `${reviewer}: ${rationale}`, detailLevel: 'summary' as const }, ...step.evidenceRefs].slice(0, 20),
1610
- };
1611
- const allCompleted = steps.every((candidate) => candidate.status === 'completed');
1612
- const projectedSteps = allCompleted ? steps : projectDependencyReadySteps(steps);
1613
- const acceptedSourceRevision = input.acceptedSourceRevision?.trim();
1614
- return {
1615
- ...current,
1616
- ...(acceptedSourceRevision ? { sourceRevision: acceptedSourceRevision } : {}),
1617
- status: current.pendingRevision ? 'replanning' : allCompleted ? 'finalized' : 'executing',
1618
- steps: projectedSteps,
1619
- updatedAt: nowIso(options),
1620
- };
1621
- });
1622
- }