@moretea-labs/forge 1.8.0 → 1.8.1

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 (528) hide show
  1. package/AGENTS.md +70 -7
  2. package/CHANGELOG.md +14 -0
  3. package/CLAUDE.md +17 -2
  4. package/CONTRIBUTING.md +3 -2
  5. package/THIRD_PARTY_NOTICES.md +1 -1
  6. package/adapters/chatgpt/browser-delivery-host.ts +7 -1
  7. package/adapters/chatgpt/browser-delivery-runtime.ts +529 -56
  8. package/adapters/chatgpt/controller-binding-store.ts +101 -22
  9. package/adapters/chatgpt/controller-round-host.ts +15 -25
  10. package/adapters/chatgpt/controller-round-settlement-store.ts +58 -4
  11. package/adapters/chatgpt/provider-delivery.ts +228 -1
  12. package/adapters/chatgpt/work-conversation-binding-store.ts +79 -53
  13. package/adapters/computer/chatgpt-extension-target.ts +463 -0
  14. package/adapters/computer/chatgpt-target-router.ts +103 -0
  15. package/adapters/computer/chrome-extension/background.js +385 -0
  16. package/adapters/computer/chrome-extension/content.js +378 -0
  17. package/adapters/computer/chrome-extension/core.js +24 -0
  18. package/adapters/computer/chrome-extension/manifest.json +30 -0
  19. package/adapters/computer/desktop-operator-discovery.ts +11 -31
  20. package/adapters/computer/interaction-target-authority.ts +142 -18
  21. package/adapters/mcp/auth.ts +0 -1
  22. package/adapters/mcp/instructions.ts +5 -4
  23. package/adapters/mcp/multi-repository.ts +3 -19
  24. package/adapters/mcp/oauth.ts +73 -0
  25. package/adapters/mcp/paths.ts +4 -1
  26. package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +87 -0
  27. package/adapters/mcp/runtime-gateway/bounded-result-runtime.ts +62 -0
  28. package/adapters/mcp/runtime-gateway/cognition-settlement.ts +201 -0
  29. package/adapters/mcp/runtime-gateway/context-adapter.ts +411 -73
  30. package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +91 -0
  31. package/adapters/mcp/runtime-gateway/controller-authority-adapter.ts +20 -5
  32. package/adapters/mcp/runtime-gateway/core-capability-adapter.ts +258 -0
  33. package/adapters/mcp/runtime-gateway/engineering-preconditions.ts +10 -27
  34. package/adapters/mcp/runtime-gateway/engineering-tool-contract.ts +15 -0
  35. package/adapters/mcp/runtime-gateway/execution-tools.ts +8 -42
  36. package/adapters/mcp/runtime-gateway/gateway-execution-adapter.ts +2 -1
  37. package/adapters/mcp/runtime-gateway/plugin-adapter.ts +117 -45
  38. package/adapters/mcp/runtime-gateway/process-tools.ts +190 -22
  39. package/adapters/mcp/runtime-gateway/protected-computer-adapter.ts +16 -4
  40. package/adapters/mcp/runtime-gateway/recovery-adapter.ts +69 -52
  41. package/adapters/mcp/runtime-gateway/repository-compat-adapter.ts +4 -35
  42. package/adapters/mcp/runtime-gateway/result-adapter.ts +10 -4
  43. package/adapters/mcp/runtime-gateway/router.ts +18 -504
  44. package/adapters/mcp/runtime-gateway/routing-policy.ts +499 -0
  45. package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +14 -65
  46. package/adapters/mcp/runtime-gateway/runtime-readiness-observation.ts +218 -0
  47. package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +113 -173
  48. package/adapters/mcp/runtime-gateway/runtime-tools.ts +15 -5
  49. package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +1 -0
  50. package/adapters/mcp/runtime-gateway/shared-adapter.ts +20 -0
  51. package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +358 -266
  52. package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +136 -0
  53. package/adapters/mcp/runtime-gateway/work-adapter.ts +261 -3122
  54. package/adapters/mcp/runtime-gateway/work-controller-operations.ts +257 -0
  55. package/adapters/mcp/runtime-gateway/work-detail-projection.ts +60 -0
  56. package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +246 -0
  57. package/adapters/mcp/runtime-gateway/work-learning-operations.ts +145 -0
  58. package/adapters/mcp/runtime-gateway/work-list-projection.ts +38 -0
  59. package/adapters/mcp/runtime-gateway/work-plan-operations.ts +382 -0
  60. package/adapters/mcp/runtime-gateway/work-repair-adapter.ts +308 -0
  61. package/adapters/mcp/runtime-gateway/work-requirement-operations.ts +148 -0
  62. package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +263 -0
  63. package/adapters/mcp/runtime-gateway/work-workflow-operations.ts +113 -0
  64. package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +156 -0
  65. package/adapters/mcp/server.ts +67 -23
  66. package/adapters/mcp/setup.ts +1 -3
  67. package/adapters/mcp/tool-mapping/legacy-tool-service.ts +44 -170
  68. package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +4 -9
  69. package/adapters/mcp/tool-mapping/repository-tools.ts +133 -142
  70. package/adapters/mcp/toolset-names.ts +5 -0
  71. package/adapters/mcp/toolset.ts +6 -5
  72. package/adapters/mcp/transports/http-observation.ts +365 -0
  73. package/adapters/mcp/transports/http.ts +154 -677
  74. package/adapters/mcp/transports/oauth-http.ts +322 -0
  75. package/adapters/mcp/transports/session-registry.ts +6 -0
  76. package/adapters/mcp/tunnels/openai-secure-tunnel.ts +89 -5
  77. package/assets/plugin-registry.v1.json +6 -4
  78. package/assets/reference-configs/agentic-development-flow.md +25 -0
  79. package/assets/reference-configs/global-working-rules.md +19 -0
  80. package/assets/skill-version.json +2 -2
  81. package/docs/architecture/CURRENT.md +111 -41
  82. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +14 -5
  83. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +6 -3
  84. package/docs/architecture/decisions/20260907-assistant-learning-loop.md +2 -0
  85. package/docs/architecture/decisions/20260907-controller-round-transition-policy.md +19 -4
  86. package/docs/architecture/decisions/20260916-workflow-supervisor-authority.md +249 -0
  87. package/docs/architecture/decisions/20260917-cognitive-memory-foundation.md +82 -0
  88. package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +221 -0
  89. package/docs/architecture/decisions/20260919-autonomous-work-liveness.md +62 -0
  90. package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +72 -0
  91. package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +237 -0
  92. package/docs/architecture/decisions/20261005-forge-v3-desktop-client-boundary.md +77 -0
  93. package/docs/architecture/domains/verification.md +1 -0
  94. package/docs/architecture/domains/workflow-engine.md +6 -0
  95. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
  96. package/docs/forge-plugin-management.md +2 -2
  97. package/docs/operations/controller-performance-and-502.md +17 -0
  98. package/docs/operations/controller-reliability-runbook.md +2 -2
  99. package/docs/operations/features.md +1 -1
  100. package/docs/operations/features.zh-CN.md +1 -1
  101. package/docs/operations/homebrew.md +2 -2
  102. package/docs/operations/mcp-tool-exposure.md +2 -0
  103. package/docs/operations/releasing.md +2 -0
  104. package/docs/operations/standalone-disaster-recovery.md +14 -6
  105. package/docs/wiki/Implementation.md +1 -1
  106. package/package.json +25 -14
  107. package/packages/kernel/cognition/api/index.ts +6 -0
  108. package/packages/kernel/cognition/application/activation-service.ts +451 -0
  109. package/packages/kernel/cognition/application/consolidation-service.ts +185 -0
  110. package/packages/kernel/cognition/application/legacy-experience-adapter.ts +38 -0
  111. package/packages/kernel/cognition/application/memory-service.ts +64 -0
  112. package/packages/kernel/cognition/domain/learning.ts +106 -0
  113. package/packages/kernel/cognition/domain/memory.ts +254 -0
  114. package/packages/kernel/controller/api/index.ts +1 -0
  115. package/packages/kernel/controller/application/continuation-service.ts +281 -0
  116. package/packages/kernel/controller/application/controller-round-service.ts +12 -0
  117. package/packages/kernel/controller/domain/controller-round-transition-policy.ts +242 -21
  118. package/packages/kernel/controller/domain/controller-round.ts +4 -1
  119. package/packages/kernel/controller/domain/execution-quality.ts +3 -1
  120. package/packages/kernel/controller/infrastructure/controller-round-store.ts +689 -103
  121. package/packages/kernel/controller/infrastructure/controller-session-store.ts +297 -71
  122. package/packages/kernel/controller/ports/controller-host.ts +2 -0
  123. package/packages/kernel/identity/api/index.ts +10 -0
  124. package/packages/kernel/identity/domain/activity-projection.ts +43 -0
  125. package/packages/kernel/identity/domain/capability-broker.ts +60 -0
  126. package/packages/kernel/identity/domain/grant.ts +72 -0
  127. package/packages/kernel/identity/domain/owned-resource.ts +116 -0
  128. package/packages/kernel/identity/domain/user-request.ts +93 -0
  129. package/packages/kernel/identity/infrastructure/activity-projection-store.ts +77 -0
  130. package/packages/kernel/identity/infrastructure/capability-broker.ts +81 -0
  131. package/packages/kernel/identity/infrastructure/grant-store.ts +284 -0
  132. package/packages/kernel/identity/infrastructure/owned-resource-store.ts +259 -0
  133. package/packages/kernel/identity/infrastructure/user-request-store.ts +182 -0
  134. package/packages/kernel/scheduler/api/index.ts +0 -2
  135. package/packages/kernel/scheduler/application/schedule-service.ts +3 -0
  136. package/packages/kernel/scheduler/application/settlement.ts +0 -15
  137. package/packages/kernel/scheduler/domain/schedule.ts +87 -0
  138. package/packages/kernel/scheduler/infrastructure/schedule-store.ts +153 -133
  139. package/packages/kernel/work/api/index.ts +3 -2
  140. package/packages/kernel/work/application/work-service.ts +17 -42
  141. package/packages/kernel/work/domain/current-task-lineage.ts +104 -0
  142. package/packages/kernel/work/domain/engineering-design.ts +10 -5
  143. package/packages/kernel/work/domain/engineering-profile.ts +8 -12
  144. package/packages/kernel/work/domain/evaluation-promotion-receipt.ts +186 -0
  145. package/packages/kernel/work/domain/execution-concurrency.ts +19 -26
  146. package/packages/kernel/work/domain/execution-snapshot.ts +0 -31
  147. package/packages/kernel/work/domain/implementation-review.ts +14 -331
  148. package/packages/kernel/work/domain/objective-graph.ts +119 -0
  149. package/packages/kernel/work/domain/project-engineering-contract.ts +23 -1
  150. package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
  151. package/packages/kernel/work/domain/state-machine.ts +25 -63
  152. package/packages/kernel/work/domain/types.ts +57 -51
  153. package/packages/kernel/work/infrastructure/work-contract-store.ts +819 -728
  154. package/packages/kernel/work/ports/work-contract-store.ts +5 -2
  155. package/packages/plugin-runtime/browser/session-authority.ts +2 -1
  156. package/packages/plugin-runtime/computer/chatgpt-identity.ts +24 -0
  157. package/packages/plugin-runtime/computer/conversation-target.ts +143 -0
  158. package/packages/plugin-runtime/computer/index.ts +2 -0
  159. package/packages/plugin-runtime/computer/target-authority.ts +26 -1
  160. package/packages/plugin-runtime/external/unix-jsonl-transport.ts +142 -18
  161. package/packages/protocols/computer/contract.ts +27 -1
  162. package/scripts/activate-source-baseline.ts +7 -9
  163. package/scripts/canary-controller-semantic-wait.ts +12 -24
  164. package/scripts/check-mcp-compatibility.ts +48 -136
  165. package/scripts/check-runtime-architecture.mjs +622 -217
  166. package/scripts/forge-local-recovery-helper.mjs +93 -15
  167. package/scripts/lib/project-init-lib.sh +3 -0
  168. package/scripts/open-source-audit-allowlist.txt +1 -2
  169. package/scripts/run-bounded-command.mjs +5 -0
  170. package/scripts/run-governed-gate.ts +0 -1
  171. package/scripts/run-mobile-monitor-bridge.ts +0 -1
  172. package/scripts/smoke-control-plane-failure-domain.ts +94 -0
  173. package/scripts/smoke-mcp-http-runtime.ts +5 -0
  174. package/scripts/smoke-runtime-recovery.ts +12 -43
  175. package/scripts/smoke-schedule-engine.ts +23 -15
  176. package/scripts/smoke-unattended-transport-recovery.ts +223 -0
  177. package/scripts/smoke-user-request-continuation-restart.ts +232 -0
  178. package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +92 -0
  179. package/scripts/smoke-workflow-supervisor-process-restart.ts +276 -0
  180. package/scripts/smoke-workflow-supervisor-restart-recovery.ts +150 -0
  181. package/scripts/smoke-workflow-supervisor.ts +103 -0
  182. package/scripts/verify-forge-runtime.sh +2 -1
  183. package/src/cli/agent-jobs/executor-health.ts +11 -34
  184. package/src/cli/agent-jobs/integration.ts +18 -49
  185. package/src/cli/agent-jobs/job-manager.ts +7 -3
  186. package/src/cli/commands/brain-assistant.ts +27 -13
  187. package/src/cli/commands/brain.ts +13 -15
  188. package/src/cli/commands/chatgpt.ts +1 -163
  189. package/src/cli/commands/recovery.ts +135 -121
  190. package/src/cli/commands/runtime.ts +3 -3
  191. package/src/cli/commands/setup-profile.ts +8 -0
  192. package/src/cli/controller/check-runner.ts +13 -2
  193. package/src/cli/controller/completion-backlog.ts +3 -3
  194. package/src/cli/controller/completion-orchestrator.ts +4 -43
  195. package/src/cli/controller/completion-target.ts +3 -72
  196. package/src/cli/controller/context/source-materializer.ts +81 -100
  197. package/src/cli/controller/context-pack.ts +10 -8
  198. package/src/cli/controller/execution-completion.ts +4 -7
  199. package/src/cli/controller/execution-policy.ts +36 -153
  200. package/src/cli/controller/governance.ts +2 -3
  201. package/src/cli/controller/issue-store.ts +4 -99
  202. package/src/cli/controller/launch-agents.ts +59 -18
  203. package/src/cli/controller/operational-plan.ts +24 -51
  204. package/src/cli/controller/runtime-config.ts +5 -1
  205. package/src/cli/controller/stuck-state-migration.ts +2 -3
  206. package/src/cli/controller/systemd-user.ts +39 -1
  207. package/src/cli/editing/edit-session.ts +1 -7
  208. package/src/cli/local-bridge/console-automations.ts +18 -13
  209. package/src/cli/local-bridge/console-view-models.ts +3 -12
  210. package/src/cli/local-bridge/facade-api.ts +64 -191
  211. package/src/cli/local-bridge/server.ts +24 -147
  212. package/src/cli/local-bridge/types.ts +0 -1
  213. package/src/cli/mcp/oauth-client-retirement.ts +74 -0
  214. package/src/cli/repositories/command-executor.ts +29 -23
  215. package/src/cli/repositories/command-scope.ts +35 -16
  216. package/src/cli/repositories/controller-home.ts +48 -0
  217. package/src/cli/repositories/ephemeral-workspace.ts +20 -30
  218. package/src/cli/repositories/git-command-executor.ts +2 -2
  219. package/src/cli/repositories/registry.ts +77 -11
  220. package/src/cli/repositories/structured-git.ts +62 -2
  221. package/src/cli/repository/session-cache.ts +92 -6
  222. package/src/runtime/context/assistant-context.ts +20 -7
  223. package/src/runtime/context/assistant-work-context.ts +89 -27
  224. package/src/runtime/context/automatic-learning.ts +565 -0
  225. package/src/runtime/context/cognitive-attention.ts +173 -0
  226. package/src/runtime/context/cognitive-skill-canary.ts +158 -0
  227. package/src/runtime/context/context-closure.ts +46 -9
  228. package/src/runtime/context/context-pack-process.ts +103 -0
  229. package/src/runtime/context/context-plane.ts +1 -9
  230. package/src/runtime/context/direct-controller-learning.ts +298 -0
  231. package/src/runtime/context/semantic-navigation-contract.ts +9 -0
  232. package/src/runtime/context/semantic-navigation.ts +6 -34
  233. package/src/runtime/context/typescript-navigation-process.ts +145 -0
  234. package/src/runtime/context/typescript-navigation.ts +89 -3
  235. package/src/runtime/control-plane/cleanup-artifact-retention.ts +212 -9
  236. package/src/runtime/control-plane/concurrency/work-execution-concurrency.ts +9 -47
  237. package/src/runtime/control-plane/execution/controller-authority-recovery.ts +34 -4
  238. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +29 -20
  239. package/src/runtime/control-plane/execution/execution-identity.ts +23 -3
  240. package/src/runtime/control-plane/execution/repository-work-attribution.ts +7 -30
  241. package/src/runtime/control-plane/execution/retained-work-resume.ts +206 -199
  242. package/src/runtime/control-plane/execution/session-store.ts +31 -1
  243. package/src/runtime/control-plane/execution/validation.ts +6 -8
  244. package/src/runtime/control-plane/execution/work-authority-reconciler.ts +13 -42
  245. package/src/runtime/control-plane/execution/work-check-evidence.ts +137 -0
  246. package/src/runtime/control-plane/execution/work-completion-authority.ts +30 -85
  247. package/src/runtime/control-plane/execution/work-delivery-service.ts +497 -0
  248. package/src/runtime/control-plane/execution/work-evidence-policy.ts +12 -50
  249. package/src/runtime/control-plane/execution/work-execution-support.ts +50 -149
  250. package/src/runtime/control-plane/execution/work-handle-authority.ts +188 -109
  251. package/src/runtime/control-plane/execution/work-handle-store.ts +19 -43
  252. package/src/runtime/control-plane/execution/work-head-settlement.ts +53 -20
  253. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +52 -29
  254. package/src/runtime/control-plane/execution/work-operation-service.ts +43 -27
  255. package/src/runtime/control-plane/execution/work-preparation-service.ts +126 -266
  256. package/src/runtime/control-plane/execution/work-remote-delivery.ts +2 -3
  257. package/src/runtime/control-plane/execution/work-revision-diff.ts +39 -0
  258. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +173 -186
  259. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +28 -84
  260. package/src/runtime/control-plane/execution/work-verification-service.ts +406 -50
  261. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
  262. package/src/runtime/control-plane/facade/capability-registry.ts +362 -37
  263. package/src/runtime/control-plane/facade/check-normalization.ts +3 -9
  264. package/src/runtime/control-plane/facade/codex-delegation.ts +10 -11
  265. package/src/runtime/control-plane/facade/controller-connection.ts +135 -0
  266. package/src/runtime/control-plane/facade/goal-workloop-access.ts +1 -20
  267. package/src/runtime/control-plane/facade/goal-workloop.ts +281 -1753
  268. package/src/runtime/control-plane/facade/handoff-inbox-application.ts +12 -25
  269. package/src/runtime/control-plane/facade/handoff-inbox-store.ts +256 -52
  270. package/src/runtime/control-plane/facade/index.ts +0 -1
  271. package/src/runtime/control-plane/facade/operation-digest.ts +12 -15
  272. package/src/runtime/control-plane/facade/plan-contract-store.ts +451 -991
  273. package/src/runtime/control-plane/facade/policy-gate.ts +0 -23
  274. package/src/runtime/control-plane/facade/repository-work-admission.ts +12 -22
  275. package/src/runtime/control-plane/facade/requirement-authority.ts +102 -199
  276. package/src/runtime/control-plane/facade/requirement-board.ts +3 -3
  277. package/src/runtime/control-plane/facade/rh-work-operation-contract.ts +51 -14
  278. package/src/runtime/control-plane/facade/self-healing-loop.ts +18 -53
  279. package/src/runtime/control-plane/facade/suggested-actions.ts +15 -18
  280. package/src/runtime/control-plane/facade/types.ts +49 -108
  281. package/src/runtime/control-plane/facade/work-continuation.ts +8 -8
  282. package/src/runtime/control-plane/generated-cache-retention.ts +97 -1
  283. package/src/runtime/control-plane/global-scheduler/autonomous-continuation.ts +248 -0
  284. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +1 -1
  285. package/src/runtime/control-plane/global-scheduler/maintenance.ts +239 -112
  286. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +2 -1
  287. package/src/runtime/control-plane/global-scheduler/scheduler.ts +66 -40
  288. package/src/runtime/control-plane/global-scheduler/wake-signal.ts +42 -2
  289. package/src/runtime/control-plane/governance/access-policy.ts +19 -2
  290. package/src/runtime/control-plane/governance/authorization.ts +20 -3
  291. package/src/runtime/control-plane/launcher/chatgpt-round-continuation.ts +63 -300
  292. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +28 -300
  293. package/src/runtime/control-plane/launcher/local-conversation-controller.ts +238 -0
  294. package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +5 -2
  295. package/src/runtime/control-plane/launcher/thin-launcher.ts +96 -240
  296. package/src/runtime/control-plane/persistence/cognition-store.ts +820 -0
  297. package/src/runtime/control-plane/persistence/experience-store.ts +162 -29
  298. package/src/runtime/control-plane/persistence/requirement-store.ts +216 -74
  299. package/src/runtime/control-plane/persistence/sqlite-store.ts +113 -6
  300. package/src/runtime/control-plane/release-retention.ts +135 -47
  301. package/src/runtime/control-plane/repository-namespace-retention.ts +2 -2
  302. package/src/runtime/control-plane/runtime-cleanup.ts +290 -21
  303. package/src/runtime/control-plane/workspace/project-onboarding.ts +80 -0
  304. package/src/runtime/control-plane/workspace/semantic-sync-service.ts +19 -5
  305. package/src/runtime/control-plane/workspace/workspace-store.ts +169 -0
  306. package/src/runtime/diagnostics/incident-repair.ts +313 -146
  307. package/src/runtime/diagnostics/mcp-timing.ts +114 -0
  308. package/src/runtime/evidence/artifact-store.ts +158 -40
  309. package/src/runtime/evidence/evidence-store.ts +112 -8
  310. package/src/runtime/evidence/output-handle-index.ts +225 -0
  311. package/src/runtime/evidence/result-store.ts +158 -44
  312. package/src/runtime/execution/jobs/store.ts +3 -4
  313. package/src/runtime/execution/managed-workspace.ts +57 -0
  314. package/src/runtime/execution/process-runtime/check-facade.ts +1 -1
  315. package/src/runtime/execution/process-runtime/check-result-retention.ts +2 -2
  316. package/src/runtime/execution/process-runtime/check-result.ts +105 -1
  317. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +18 -8
  318. package/src/runtime/execution/process-runtime/command-facade.ts +79 -27
  319. package/src/runtime/execution/process-runtime/gc.ts +15 -2
  320. package/src/runtime/execution/process-runtime/handle-index.ts +176 -0
  321. package/src/runtime/execution/process-runtime/host-command-lane.ts +139 -0
  322. package/src/runtime/execution/process-runtime/index.ts +3 -0
  323. package/src/runtime/execution/process-runtime/lightweight-managed.ts +251 -72
  324. package/src/runtime/execution/process-runtime/persisted-check.ts +3 -3
  325. package/src/runtime/execution/process-runtime/process-runner-entry.ts +47 -5
  326. package/src/runtime/execution/process-runtime/process-scope.ts +160 -0
  327. package/src/runtime/execution/process-runtime/resource-claims.ts +65 -61
  328. package/src/runtime/execution/process-runtime/runtime.ts +35 -2
  329. package/src/runtime/execution/process-runtime/store.ts +30 -2
  330. package/src/runtime/execution/process-runtime/types.ts +6 -1
  331. package/src/runtime/execution/workers/executor.ts +2 -6
  332. package/src/runtime/platform/service-inventory.ts +130 -0
  333. package/src/runtime/platform/service-manager.ts +8 -0
  334. package/src/runtime/plugins/action-application.ts +85 -28
  335. package/src/runtime/plugins/app-store-connect-adapter.ts +38 -14
  336. package/src/runtime/plugins/browser-adapter.ts +757 -34
  337. package/src/runtime/plugins/browser-automation-service.ts +23 -3
  338. package/src/runtime/plugins/browser-macos-bridge.ts +321 -29
  339. package/src/runtime/plugins/browser-manifest-surface.ts +45 -11
  340. package/src/runtime/plugins/browser-native-session-service.ts +63 -13
  341. package/src/runtime/plugins/browser-registration.ts +16 -0
  342. package/src/runtime/plugins/browser-runtime.ts +14 -1
  343. package/src/runtime/plugins/browser-session-authority.ts +2 -2
  344. package/src/runtime/plugins/browser-session-store.ts +46 -76
  345. package/src/runtime/plugins/capability-authorization-grants.ts +258 -62
  346. package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +219 -0
  347. package/src/runtime/plugins/computer-chatgpt-macos-target.ts +891 -0
  348. package/src/runtime/plugins/computer-registration.ts +222 -11
  349. package/src/runtime/plugins/desktop-operator-external-policy.ts +36 -4
  350. package/src/runtime/plugins/desktop-operator-registration.ts +12 -4
  351. package/src/runtime/plugins/external-adapter.ts +42 -6
  352. package/src/runtime/plugins/external-registration.ts +21 -1
  353. package/src/runtime/plugins/gmail-adapter.ts +23 -2
  354. package/src/runtime/plugins/google-calendar-adapter.ts +2 -2
  355. package/src/runtime/plugins/google-shared.ts +14 -7
  356. package/src/runtime/plugins/google-tasks-adapter.ts +2 -2
  357. package/src/runtime/plugins/ios-adapter.ts +1 -1
  358. package/src/runtime/plugins/lightweight-action.ts +208 -2
  359. package/src/runtime/plugins/local-system-adapter.ts +12 -2
  360. package/src/runtime/plugins/managed-process-adapter.ts +12 -5
  361. package/src/runtime/plugins/plugin-action-sidecar.ts +17 -13
  362. package/src/runtime/plugins/plugin-management-adapter.ts +1 -1
  363. package/src/runtime/plugins/store.ts +602 -428
  364. package/src/runtime/plugins/types.ts +20 -0
  365. package/src/runtime/plugins/xiaohongshu-publish.ts +16 -0
  366. package/src/runtime/projections/invalidation.ts +7 -3
  367. package/src/runtime/projections/materialized-view.ts +80 -36
  368. package/src/runtime/recovery/actions.ts +3 -3
  369. package/src/runtime/recovery/capability-action-application.ts +46 -45
  370. package/src/runtime/recovery/lifecycle-inventory.ts +29 -29
  371. package/src/runtime/recovery/maintenance-executor.ts +374 -123
  372. package/src/runtime/recovery/store.ts +29 -11
  373. package/src/runtime/release/release-coordinator.ts +296 -0
  374. package/src/runtime/release/release-session.ts +716 -0
  375. package/src/runtime/release/release-state-migration.ts +25 -0
  376. package/src/runtime/resources/leases/store.ts +56 -25
  377. package/src/runtime/root/computer-composition.ts +191 -6
  378. package/src/runtime/root/controller-progression-composition.ts +192 -0
  379. package/src/runtime/root/controller-round-composition.ts +20 -1
  380. package/src/runtime/root/deployment-topology.ts +106 -0
  381. package/src/runtime/root/entry.ts +4 -3
  382. package/src/runtime/root/known-good-recovery.ts +184 -0
  383. package/src/runtime/root/local-bridge.ts +0 -1
  384. package/src/runtime/root/mcp-transport.ts +111 -18
  385. package/src/runtime/root/ownership.ts +261 -9
  386. package/src/runtime/root/package-connector-service.ts +105 -19
  387. package/src/runtime/root/package-runtime-release.ts +117 -18
  388. package/src/runtime/root/package-runtime-service.ts +17 -11
  389. package/src/runtime/root/release-execution-canary.ts +44 -12
  390. package/src/runtime/root/release-loader.ts +90 -8
  391. package/src/runtime/root/release-manifest.ts +80 -6
  392. package/src/runtime/root/release-materialize.ts +372 -28
  393. package/src/runtime/root/release-store.ts +395 -36
  394. package/src/runtime/root/runtime-lane.ts +191 -0
  395. package/src/runtime/root/runtime.ts +159 -27
  396. package/src/runtime/root/scheduled-controller-composition.ts +28 -3
  397. package/src/runtime/root/service-runner.ts +2 -2
  398. package/src/runtime/root/service.ts +36 -33
  399. package/src/runtime/root/status.ts +48 -0
  400. package/src/runtime/root/types.ts +32 -3
  401. package/src/runtime/root/workflow-supervisor-composition.ts +493 -0
  402. package/src/runtime/root/workflow-supervisor-runtime.ts +209 -0
  403. package/src/runtime/root/write-fence.ts +50 -2
  404. package/src/runtime/safe-tooling/google-credential-store.ts +76 -21
  405. package/src/runtime/safe-tooling/google-oauth-broker.ts +25 -7
  406. package/src/runtime/safe-tooling/workspace-auth.ts +1 -1
  407. package/src/runtime/shared/bounded-child-supervisor.ts +9 -1
  408. package/src/runtime/shared/process-environment.ts +20 -9
  409. package/src/runtime/shared/response-budget.ts +1 -3
  410. package/src/runtime/standalone-recovery/controller-home-migration.ts +16 -24
  411. package/src/runtime/standalone-recovery/core.ts +3702 -342
  412. package/src/runtime/standalone-recovery/entry.ts +469 -39
  413. package/src/runtime/standalone-recovery/installer.ts +262 -148
  414. package/src/runtime/standalone-recovery/mcp-server.ts +29 -193
  415. package/src/runtime/standalone-recovery/operation-lock.ts +1 -1
  416. package/src/runtime/standalone-recovery/performance.ts +54 -16
  417. package/src/runtime/standalone-recovery/release.ts +53 -5
  418. package/src/runtime/standalone-recovery/service-labels.ts +5 -2
  419. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +5 -2
  420. package/src/runtime/workflow/schedules/browser-probe.ts +17 -6
  421. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +10 -3
  422. package/src/runtime/workflow/schedules/engine.ts +138 -172
  423. package/src/runtime/workflow/schedules/work-continuation.ts +44 -5
  424. package/src/runtime/workflows/runtime.ts +15 -19
  425. package/src/testing/test-governance.ts +5 -1
  426. package/supervisor/chatgpt-conversation.ts +23 -0
  427. package/supervisor/client.ts +201 -0
  428. package/supervisor/control-plane.ts +868 -0
  429. package/supervisor/entry.ts +15 -0
  430. package/supervisor/forge-validators.ts +80 -0
  431. package/supervisor/index.ts +5 -0
  432. package/supervisor/native-browser-adapter.ts +786 -0
  433. package/supervisor/native-messaging/host.ts +39 -0
  434. package/supervisor/native-messaging/manifest.ts +26 -0
  435. package/supervisor/native-messaging/protocol.ts +27 -0
  436. package/supervisor/paths.ts +18 -0
  437. package/supervisor/protocol.ts +202 -0
  438. package/supervisor/server.ts +404 -0
  439. package/supervisor/store.ts +1451 -0
  440. package/supervisor/tsconfig.json +8 -0
  441. package/supervisor/types.ts +255 -0
  442. package/adapters/chatgpt/controller-host.ts +0 -119
  443. package/adapters/mcp/controller-round-compatibility.ts +0 -184
  444. package/adapters/mcp/frozen-client-semantic-compatibility.ts +0 -292
  445. package/adapters/mcp/runtime-gateway/work-compat-adapter.ts +0 -502
  446. package/packages/kernel/progression/api/index.ts +0 -2
  447. package/packages/kernel/progression/application/projector.ts +0 -211
  448. package/packages/kernel/progression/domain/types.ts +0 -116
  449. package/packages/kernel/progression/index.ts +0 -1
  450. package/packages/kernel/scheduler/application/continuation-service.ts +0 -212
  451. package/packages/kernel/scheduler/domain/continuation.ts +0 -38
  452. package/packages/kernel/scheduler/infrastructure/continuation-dispatch-store.ts +0 -38
  453. package/packages/kernel/work/domain/admission-policy.ts +0 -34
  454. package/scripts/benchmark-background-check-overlap.ts +0 -228
  455. package/scripts/benchmark-computer-hotpath.ts +0 -255
  456. package/scripts/benchmark-controller-hotpath.ts +0 -212
  457. package/scripts/benchmark-controller-round2.ts +0 -191
  458. package/scripts/benchmark-ios-agent-device.ts +0 -511
  459. package/scripts/benchmark-mcp-host.sh +0 -117
  460. package/scripts/benchmark-mcp-transport.ts +0 -169
  461. package/scripts/benchmark-operational-memory-activation.ts +0 -235
  462. package/scripts/benchmark-operational-shadow.ts +0 -28
  463. package/scripts/benchmark-quality-harness.ts +0 -146
  464. package/scripts/benchmark-route-session-concurrency.ts +0 -915
  465. package/scripts/benchmark-thin-harness-gateway-ab.ts +0 -632
  466. package/scripts/benchmark-thin-harness-mcp-e2e.ts +0 -498
  467. package/scripts/benchmark-thin-harness.ts +0 -251
  468. package/scripts/benchmark-typescript-navigation.ts +0 -186
  469. package/scripts/benchmark-work-efficiency.ts +0 -334
  470. package/scripts/build-controller-ui.ts +0 -28
  471. package/scripts/check-npm-release.sh +0 -32
  472. package/scripts/check-public-release-surface.sh +0 -19
  473. package/scripts/check-release-published.sh +0 -57
  474. package/scripts/check-release-readiness.sh +0 -37
  475. package/scripts/check-release-version.mjs +0 -74
  476. package/scripts/check-tarball-install-smoke.sh +0 -150
  477. package/scripts/package-source-archive.sh +0 -147
  478. package/scripts/public-release-files.txt +0 -39
  479. package/scripts/public-release.ts +0 -174
  480. package/scripts/publish-release-tarball.sh +0 -27
  481. package/scripts/route-nl-vs-ts-eval.ts +0 -679
  482. package/scripts/stage-runtime-release.ts +0 -44
  483. package/src/cli/controller/progress.ts +0 -656
  484. package/src/cli/controller/work-mode.ts +0 -181
  485. package/src/cli/local-bridge/console-assets.ts +0 -17
  486. package/src/cli/local-bridge/dashboard.ts +0 -17
  487. package/src/cli/local-bridge/frontend/src/App.tsx +0 -10
  488. package/src/cli/local-bridge/frontend/src/components/Button.tsx +0 -1
  489. package/src/cli/local-bridge/frontend/src/components/DetailPane.tsx +0 -3
  490. package/src/cli/local-bridge/frontend/src/components/SearchField.tsx +0 -1
  491. package/src/cli/local-bridge/frontend/src/components/SectionHeader.tsx +0 -1
  492. package/src/cli/local-bridge/frontend/src/components/Segmented.tsx +0 -1
  493. package/src/cli/local-bridge/frontend/src/components/StatusText.tsx +0 -3
  494. package/src/cli/local-bridge/frontend/src/design/icons.tsx +0 -11
  495. package/src/cli/local-bridge/frontend/src/design/tokens.css +0 -8
  496. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +0 -153
  497. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +0 -37
  498. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +0 -183
  499. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +0 -2
  500. package/src/cli/local-bridge/frontend/src/features/system/SystemView.tsx +0 -2
  501. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +0 -4
  502. package/src/cli/local-bridge/frontend/src/lib/api.ts +0 -30
  503. package/src/cli/local-bridge/frontend/src/lib/format.ts +0 -4
  504. package/src/cli/local-bridge/frontend/src/lib/routes.ts +0 -7
  505. package/src/cli/local-bridge/frontend/src/main.tsx +0 -1
  506. package/src/cli/local-bridge/frontend/src/shell/AppShell.tsx +0 -1
  507. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +0 -2
  508. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +0 -4
  509. package/src/cli/local-bridge/frontend/src/styles.css +0 -28
  510. package/src/cli/local-bridge/frontend/src/types.ts +0 -15
  511. package/src/cli/local-bridge/frontend/src/vite-env.d.ts +0 -1
  512. package/src/cli/local-bridge/frontend/vite.config.ts +0 -27
  513. package/src/cli/local-bridge/ui-dist/app.css +0 -1
  514. package/src/cli/local-bridge/ui-dist/app.js +0 -11228
  515. package/src/runtime/control-plane/execution/content-equivalent-commit-authority.ts +0 -169
  516. package/src/runtime/control-plane/execution/direct-canonical-work-reconciliation.ts +0 -85
  517. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +0 -655
  518. package/src/runtime/control-plane/execution/work-finalization-service.ts +0 -2850
  519. package/src/runtime/control-plane/execution/work-path-scope.ts +0 -37
  520. package/src/runtime/control-plane/execution/work-task-receipt.ts +0 -468
  521. package/src/runtime/control-plane/facade/controller-round-relay.ts +0 -3
  522. package/src/runtime/control-plane/facade/work-admission-policy.ts +0 -142
  523. package/src/runtime/control-plane/facade/work-implementation-review.ts +0 -2
  524. package/src/runtime/control-plane/facade/work-state-machine.ts +0 -2
  525. package/src/runtime/control-plane/facade/work-task-projection.ts +0 -66
  526. package/src/runtime/control-plane/routing/index.ts +0 -1
  527. package/src/runtime/control-plane/routing/route-policy.ts +0 -401
  528. package/src/runtime/control-plane/routing/workspace-admission.ts +0 -70
@@ -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
- }