@moretea-labs/forge 1.7.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (685) hide show
  1. package/AGENTS.md +58 -62
  2. package/CHANGELOG.md +39 -0
  3. package/CLAUDE.md +42 -62
  4. package/README.md +2 -2
  5. package/SKILL.md +0 -1
  6. package/THIRD_PARTY_NOTICES.md +5 -2
  7. package/adapters/browser/index.ts +2 -0
  8. package/adapters/chatgpt/browser-delivery-host.ts +71 -0
  9. package/adapters/chatgpt/browser-delivery-runtime.ts +783 -0
  10. package/adapters/chatgpt/controller-binding-store.ts +55 -0
  11. package/adapters/chatgpt/controller-host.ts +119 -0
  12. package/adapters/chatgpt/controller-round-host.ts +78 -0
  13. package/adapters/chatgpt/controller-round-settlement-store.ts +45 -0
  14. package/adapters/chatgpt/provider-delivery.ts +113 -0
  15. package/adapters/chatgpt/work-conversation-binding-store.ts +192 -0
  16. package/adapters/chatgpt/wsl-bridge-delivery-host.ts +63 -0
  17. package/adapters/computer/desktop-operator-contract.ts +7 -0
  18. package/adapters/computer/desktop-operator-discovery.ts +201 -0
  19. package/adapters/computer/desktop-operator-negotiation.ts +174 -0
  20. package/adapters/computer/desktop-operator-provider.ts +312 -0
  21. package/adapters/computer/index.ts +1 -0
  22. package/adapters/computer/interaction-target-authority.ts +944 -0
  23. package/adapters/controller-process/binding-store.ts +34 -0
  24. package/adapters/controller-process/controller-host.ts +26 -0
  25. package/adapters/mcp/access-mode.ts +132 -0
  26. package/adapters/mcp/audit.ts +32 -0
  27. package/adapters/mcp/auth.ts +407 -0
  28. package/adapters/mcp/controller-round-compatibility.ts +184 -0
  29. package/adapters/mcp/frozen-client-semantic-compatibility.ts +292 -0
  30. package/adapters/mcp/instructions.ts +36 -0
  31. package/adapters/mcp/legacy-context.ts +18 -0
  32. package/adapters/mcp/multi-repository.ts +437 -0
  33. package/adapters/mcp/oauth.ts +391 -0
  34. package/adapters/mcp/paths.ts +140 -0
  35. package/adapters/mcp/policy.ts +232 -0
  36. package/adapters/mcp/proxy-env.ts +62 -0
  37. package/adapters/mcp/readiness-projection.ts +24 -0
  38. package/adapters/mcp/redaction.ts +21 -0
  39. package/adapters/mcp/repo.ts +24 -0
  40. package/adapters/mcp/runtime-gateway/artifact-adapter.ts +52 -0
  41. package/adapters/mcp/runtime-gateway/context-adapter.ts +829 -0
  42. package/adapters/mcp/runtime-gateway/controller-authority-adapter.ts +191 -0
  43. package/adapters/mcp/runtime-gateway/engineering-preconditions.ts +147 -0
  44. package/adapters/mcp/runtime-gateway/execution-control-adapter.ts +28 -0
  45. package/adapters/mcp/runtime-gateway/execution-tools.ts +207 -0
  46. package/adapters/mcp/runtime-gateway/filesystem-adapter.ts +38 -0
  47. package/adapters/mcp/runtime-gateway/gateway-execution-adapter.ts +411 -0
  48. package/adapters/mcp/runtime-gateway/legacy-ios-tool-adapter.ts +105 -0
  49. package/adapters/mcp/runtime-gateway/maintenance-adapter.ts +44 -0
  50. package/adapters/mcp/runtime-gateway/model-adapter.ts +63 -0
  51. package/adapters/mcp/runtime-gateway/persisted-check-process.ts +3 -0
  52. package/adapters/mcp/runtime-gateway/plugin-adapter.ts +308 -0
  53. package/adapters/mcp/runtime-gateway/process-tools.ts +289 -0
  54. package/adapters/mcp/runtime-gateway/protected-computer-adapter.ts +76 -0
  55. package/adapters/mcp/runtime-gateway/recovery-adapter.ts +268 -0
  56. package/adapters/mcp/runtime-gateway/recovery-client-adapter.ts +70 -0
  57. package/adapters/mcp/runtime-gateway/recovery-tool-contract.ts +99 -0
  58. package/adapters/mcp/runtime-gateway/repository-compat-adapter.ts +110 -0
  59. package/adapters/mcp/runtime-gateway/result-adapter.ts +108 -0
  60. package/adapters/mcp/runtime-gateway/router.ts +728 -0
  61. package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1251 -0
  62. package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +634 -0
  63. package/adapters/mcp/runtime-gateway/runtime-tool-shared.ts +27 -0
  64. package/adapters/mcp/runtime-gateway/runtime-tools.ts +68 -0
  65. package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +186 -0
  66. package/adapters/mcp/runtime-gateway/shared-adapter.ts +25 -0
  67. package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +870 -0
  68. package/adapters/mcp/runtime-gateway/work-adapter.ts +3186 -0
  69. package/adapters/mcp/runtime-gateway/work-compat-adapter.ts +502 -0
  70. package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +1 -0
  71. package/adapters/mcp/server.ts +1048 -0
  72. package/adapters/mcp/setup.ts +791 -0
  73. package/adapters/mcp/tool-capability-cache.ts +87 -0
  74. package/adapters/mcp/tool-mapping/access-tools.ts +387 -0
  75. package/adapters/mcp/tool-mapping/legacy-tool-service.ts +5265 -0
  76. package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +150 -0
  77. package/adapters/mcp/tool-mapping/repository-tools.ts +1691 -0
  78. package/adapters/mcp/tool-mapping/tools.ts +9 -0
  79. package/adapters/mcp/tool-mapping/umbrella-tools.ts +117 -0
  80. package/adapters/mcp/toolset-names.ts +74 -0
  81. package/adapters/mcp/toolset-selection.ts +41 -0
  82. package/adapters/mcp/toolset.ts +336 -0
  83. package/adapters/mcp/transports/http.ts +1456 -0
  84. package/adapters/mcp/transports/session-registry.ts +513 -0
  85. package/adapters/mcp/transports/stdio.ts +8 -0
  86. package/adapters/mcp/tunnels/openai-secure-tunnel.ts +139 -0
  87. package/adapters/mcp/types.ts +41 -0
  88. package/assets/AGENTS.md +2 -16
  89. package/assets/CLAUDE.md +2 -16
  90. package/assets/hooks/AGENTS.md +3 -14
  91. package/assets/hooks/CLAUDE.md +3 -14
  92. package/assets/hooks/hook-input.sh +9 -2
  93. package/assets/hooks/lib/workflow-state.sh +28 -10
  94. package/assets/hooks/prompt-guard.sh +63 -37
  95. package/assets/hooks/security-sentinel.sh +2 -1
  96. package/assets/hooks/session-start-context.sh +7 -5
  97. package/assets/hooks/stop-orchestrator.sh +13 -3
  98. package/assets/initializer-question-pack.v4.json +11 -7
  99. package/assets/partials/05-workflow.partial.md +18 -73
  100. package/assets/partials/07-footer.partial.md +1 -1
  101. package/assets/partials-agents/02-operating-mode.partial.md +1 -2
  102. package/assets/partials-agents/04-task-protocol.partial.md +14 -59
  103. package/assets/partials-agents/06-quality-safety.partial.md +11 -16
  104. package/assets/partials-agents/08-deep-docs.partial.md +1 -2
  105. package/assets/plan-map.json +11 -11
  106. package/assets/plugin-registry.v1.json +9 -3
  107. package/assets/plugin-trust-policy.v1.json +6 -0
  108. package/assets/recovery/ForgeRecovery.ps1 +100 -0
  109. package/assets/recovery/forge-wsl-rescue +271 -0
  110. package/assets/reference-configs/development-protocol.md +11 -15
  111. package/assets/reference-configs/document-generation.md +1 -1
  112. package/assets/reference-configs/global-working-rules.md +9 -1
  113. package/assets/reference-configs/harness-overview.md +22 -112
  114. package/assets/reference-configs/hook-operations.md +1 -1
  115. package/assets/reference-configs/sprint-contracts.md +2 -2
  116. package/assets/skill-version.json +2 -2
  117. package/assets/templates/helpers/architecture-event.ts +3 -34
  118. package/assets/templates/helpers/architecture-queue.sh +61 -10
  119. package/assets/templates/helpers/archive-workflow.sh +90 -180
  120. package/assets/templates/helpers/capability-config.ts +8 -11
  121. package/assets/templates/helpers/capability-resolver.ts +37 -19
  122. package/assets/templates/helpers/capture-plan.sh +2 -2
  123. package/assets/templates/helpers/check-architecture-sync.sh +1 -9
  124. package/assets/templates/helpers/check-task-sync.sh +82 -11
  125. package/assets/templates/helpers/check-task-workflow.sh +1 -3
  126. package/assets/templates/helpers/context-contract-sync.sh +1 -12
  127. package/assets/templates/helpers/contract-worktree.sh +26 -24
  128. package/assets/templates/helpers/ensure-task-workflow.sh +6 -8
  129. package/assets/templates/helpers/inspect-project-state.ts +1 -1
  130. package/assets/templates/helpers/migrate-project-template.sh +1 -1
  131. package/assets/templates/helpers/migrate-workflow-docs.ts +53 -239
  132. package/assets/templates/helpers/new-plan.sh +2 -2
  133. package/assets/templates/helpers/plan-to-todo.sh +3 -31
  134. package/assets/templates/helpers/sprint-backlog.sh +1 -1
  135. package/assets/templates/helpers/switch-plan.sh +1 -1
  136. package/assets/templates/helpers/workflow-contract.ts +1 -0
  137. package/assets/workflow-contract.v1.json +63 -171
  138. package/assets/workflows/xiaohongshu/xiaohongshu.collect-metrics.v1.draft.json +118 -0
  139. package/assets/workflows/xiaohongshu/xiaohongshu.publish-image-note.v1.draft.json +316 -0
  140. package/assets/workflows/xiaohongshu/xiaohongshu.publish-long-text.v1.draft.json +324 -0
  141. package/docs/architecture/CURRENT.md +148 -13
  142. package/docs/architecture/EVOLUTION.md +13 -0
  143. package/docs/architecture/decisions/20260801-controller-home-sqlite-state.md +2 -1
  144. package/docs/architecture/decisions/20260831-independent-windows-wsl-host-rescue.md +59 -0
  145. package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +120 -0
  146. package/docs/architecture/decisions/20260904-repository-runtime-data-boundary.md +42 -0
  147. package/docs/architecture/decisions/20260907-assistant-learning-loop.md +35 -0
  148. package/docs/architecture/decisions/20260907-controller-round-transition-policy.md +296 -0
  149. package/docs/architecture/decisions/20260907-maintenance-diagnosis-bounded-read.md +39 -0
  150. package/docs/architecture/domains/verification.md +7 -0
  151. package/docs/architecture/domains/workflow-engine.md +1 -1
  152. package/docs/architecture/index.md +3 -3
  153. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +19 -8
  154. package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +124 -0
  155. package/docs/forge-chatgpt-mcp-setup.md +5 -3
  156. package/docs/forge-plugin-management.md +20 -4
  157. package/docs/forge-runtime-self-healing-loop.md +2 -0
  158. package/docs/operations/benchmark-cross-version-freeze.md +38 -0
  159. package/docs/operations/controller-browser-plugin.md +3 -1
  160. package/docs/operations/homebrew.md +2 -2
  161. package/docs/operations/platform-support.md +16 -8
  162. package/docs/operations/platform-support.zh-CN.md +2 -2
  163. package/docs/operations/standalone-disaster-recovery.md +26 -1
  164. package/docs/operations/uu-remote-rescue.md +41 -0
  165. package/docs/operations/windows-wsl-host-rescue.md +133 -0
  166. package/docs/public-usage-guide.md +1 -1
  167. package/docs/public-usage-guide.zh-CN.md +1 -1
  168. package/docs/tutorials/01-install-and-start.md +1 -1
  169. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  170. package/docs/tutorials/02-connect-chatgpt.md +4 -4
  171. package/docs/tutorials/02-connect-chatgpt.zh-CN.md +4 -4
  172. package/docs/wiki/Implementation.md +1 -1
  173. package/package.json +23 -14
  174. package/packages/kernel/controller/api/index.ts +2 -0
  175. package/packages/kernel/controller/application/controller-round-service.ts +29 -0
  176. package/packages/kernel/controller/application/controller-service.ts +30 -0
  177. package/packages/kernel/controller/domain/controller-round-transition-policy.ts +308 -0
  178. package/packages/kernel/controller/domain/controller-round.ts +107 -0
  179. package/packages/kernel/controller/domain/execution-quality.ts +133 -0
  180. package/packages/kernel/controller/domain/types.ts +56 -0
  181. package/packages/kernel/controller/index.ts +1 -0
  182. package/packages/kernel/controller/infrastructure/controller-binding-store.ts +84 -0
  183. package/packages/kernel/controller/infrastructure/controller-round-store.ts +1384 -0
  184. package/packages/kernel/controller/infrastructure/controller-session-store.ts +578 -0
  185. package/packages/kernel/controller/ports/controller-host.ts +16 -0
  186. package/packages/kernel/goal/api/index.ts +1 -0
  187. package/packages/kernel/goal/domain/types.ts +40 -0
  188. package/packages/kernel/goal/index.ts +1 -0
  189. package/packages/kernel/identity/api/index.ts +5 -0
  190. package/packages/kernel/identity/application/identity-service.ts +67 -0
  191. package/packages/kernel/identity/domain/scope.ts +63 -0
  192. package/packages/kernel/identity/domain/semantic-sync.ts +81 -0
  193. package/packages/kernel/identity/domain/types.ts +44 -0
  194. package/packages/kernel/identity/domain/workspace.ts +56 -0
  195. package/packages/kernel/identity/index.ts +1 -0
  196. package/packages/kernel/identity/infrastructure/identity-store.ts +75 -0
  197. package/packages/kernel/memory/api/index.ts +5 -0
  198. package/packages/kernel/memory/application/experience-service.ts +118 -0
  199. package/packages/kernel/memory/application/outcome-service.ts +77 -0
  200. package/packages/kernel/memory/domain/experience.ts +103 -0
  201. package/packages/kernel/memory/domain/operational-prior.ts +92 -0
  202. package/packages/kernel/memory/index.ts +1 -0
  203. package/packages/kernel/progression/api/index.ts +2 -0
  204. package/packages/kernel/progression/application/projector.ts +211 -0
  205. package/packages/kernel/progression/domain/types.ts +116 -0
  206. package/packages/kernel/progression/index.ts +1 -0
  207. package/packages/kernel/scheduler/api/index.ts +6 -0
  208. package/packages/kernel/scheduler/application/continuation-service.ts +212 -0
  209. package/packages/kernel/scheduler/application/eligibility.ts +252 -0
  210. package/packages/kernel/scheduler/application/schedule-service.ts +24 -0
  211. package/packages/kernel/scheduler/application/settlement.ts +197 -0
  212. package/packages/kernel/scheduler/domain/continuation.ts +38 -0
  213. package/packages/kernel/scheduler/domain/schedule.ts +149 -0
  214. package/packages/kernel/scheduler/index.ts +1 -0
  215. package/packages/kernel/scheduler/infrastructure/continuation-dispatch-store.ts +38 -0
  216. package/packages/kernel/scheduler/infrastructure/schedule-store.ts +785 -0
  217. package/packages/kernel/work/api/index.ts +15 -0
  218. package/packages/kernel/work/application/work-service.ts +148 -0
  219. package/packages/kernel/work/domain/admission-policy.ts +34 -0
  220. package/packages/kernel/work/domain/check-receipt.ts +43 -0
  221. package/packages/kernel/work/domain/context-closure-receipt.ts +32 -0
  222. package/packages/kernel/work/domain/engineering-contracts.ts +157 -0
  223. package/packages/kernel/work/domain/engineering-design.ts +281 -0
  224. package/packages/kernel/work/domain/engineering-profile.ts +242 -0
  225. package/packages/kernel/work/domain/execution-concurrency.ts +254 -0
  226. package/packages/kernel/work/domain/execution-snapshot.ts +33 -0
  227. package/packages/kernel/work/domain/implementation-review.ts +510 -0
  228. package/packages/kernel/work/domain/project-engineering-contract.ts +202 -0
  229. package/packages/kernel/work/domain/repository-completion-receipt.ts +73 -0
  230. package/packages/kernel/work/domain/state-machine.ts +259 -0
  231. package/packages/kernel/work/domain/types.ts +528 -0
  232. package/packages/kernel/work/index.ts +1 -0
  233. package/packages/kernel/work/infrastructure/work-contract-store.ts +1876 -0
  234. package/packages/kernel/work/ports/work-contract-store.ts +18 -0
  235. package/packages/plugin-runtime/browser/index.ts +3 -0
  236. package/packages/plugin-runtime/browser/provider-registry.ts +110 -0
  237. package/packages/plugin-runtime/browser/runtime-contract.ts +150 -0
  238. package/packages/plugin-runtime/browser/session-authority.ts +61 -0
  239. package/packages/plugin-runtime/computer/index.ts +6 -0
  240. package/packages/plugin-runtime/computer/provider-error.ts +26 -0
  241. package/packages/plugin-runtime/computer/provider-registration.ts +52 -0
  242. package/packages/plugin-runtime/computer/provider-registry.ts +63 -0
  243. package/packages/plugin-runtime/computer/provider.ts +19 -0
  244. package/packages/plugin-runtime/computer/target-authority.ts +190 -0
  245. package/packages/plugin-runtime/computer/target-persistence.ts +54 -0
  246. package/packages/plugin-runtime/external/index.ts +1 -0
  247. package/packages/plugin-runtime/external/unix-jsonl-transport.ts +457 -0
  248. package/packages/plugin-runtime/index.ts +3 -0
  249. package/packages/protocols/browser/index.ts +1 -0
  250. package/packages/protocols/browser/session.ts +98 -0
  251. package/packages/protocols/computer/contract.ts +199 -0
  252. package/packages/protocols/computer/index.ts +1 -0
  253. package/packages/protocols/failure/contract.ts +158 -0
  254. package/packages/protocols/failure/index.ts +1 -0
  255. package/packages/protocols/handoff/index.ts +1 -0
  256. package/packages/protocols/handoff/status.ts +22 -0
  257. package/packages/protocols/mcp/execution-context.ts +11 -0
  258. package/packages/protocols/mcp/index.ts +2 -0
  259. package/packages/protocols/mcp/tool-contract.ts +42 -0
  260. package/packages/workflow-runtime/api/index.ts +2 -0
  261. package/packages/workflow-runtime/application/interpreter.ts +342 -0
  262. package/packages/workflow-runtime/domain/workflow-asset.ts +223 -0
  263. package/packages/workflow-runtime/index.ts +1 -0
  264. package/scripts/assemble-template.ts +1 -1
  265. package/scripts/benchmark-computer-hotpath.ts +255 -0
  266. package/scripts/benchmark-controller-round2.ts +54 -2
  267. package/scripts/benchmark-ios-agent-device.ts +3 -6
  268. package/scripts/benchmark-mcp-transport.ts +3 -4
  269. package/scripts/benchmark-operational-memory-activation.ts +235 -0
  270. package/scripts/benchmark-operational-shadow.ts +28 -0
  271. package/scripts/bootstrap-runtime-maintenance-recovery.sh +4 -1
  272. package/scripts/canary-chatgpt-provider-delivery.ts +97 -0
  273. package/scripts/canary-controller-semantic-wait.ts +195 -0
  274. package/scripts/canary-global-hook.sh +2 -2
  275. package/scripts/check-mcp-compatibility.ts +173 -7
  276. package/scripts/check-platform-support.mjs +28 -2
  277. package/scripts/check-public-docs.sh +1 -1
  278. package/scripts/check-release-readiness.sh +1 -1
  279. package/scripts/check-repository-hygiene.mjs +92 -0
  280. package/scripts/check-runtime-architecture.mjs +1206 -95
  281. package/scripts/check-source-duplication.mjs +9 -1
  282. package/scripts/check-tarball-install-smoke.sh +98 -15
  283. package/scripts/controller-ngrok-rotation.sh +5 -4
  284. package/scripts/create-project-dirs.sh +10 -74
  285. package/scripts/forge-local-recovery-helper.mjs +204 -0
  286. package/scripts/forge-uu-remote-rescue-helper.d.mts +31 -0
  287. package/scripts/forge-uu-remote-rescue-helper.mjs +409 -0
  288. package/scripts/forge-windows-host-recovery-helper.mjs +136 -0
  289. package/scripts/hook-shim.sh +2 -2
  290. package/scripts/init-project.sh +12 -70
  291. package/scripts/install-desktop-operator-registration.ts +4 -2
  292. package/scripts/install-independent-windows-wsl-recovery.ts +125 -0
  293. package/scripts/install-standalone-recovery.ts +122 -10
  294. package/scripts/install-uu-remote-rescue-registration.ts +74 -0
  295. package/scripts/install-windows-host-recovery-registration.ts +22 -0
  296. package/scripts/lib/controller-home-env.sh +6 -3
  297. package/scripts/lib/project-init-lib.sh +39 -582
  298. package/scripts/migrate-chatgpt-automation-schedules.ts +2 -2
  299. package/scripts/migrate-windows-wsl-controller-home.ts +306 -0
  300. package/scripts/open-source-audit-allowlist.txt +12 -1
  301. package/scripts/portability-bindings.mjs +70 -0
  302. package/scripts/run-bounded-command.mjs +55 -0
  303. package/scripts/run-bun-test-file.ts +22 -6
  304. package/scripts/run-governed-gate.ts +4 -1
  305. package/scripts/smoke-mcp-http-runtime.ts +1 -1
  306. package/scripts/smoke-runtime-recovery.ts +34 -1
  307. package/scripts/smoke-schedule-engine.ts +1 -1
  308. package/scripts/stage-runtime-release.ts +6 -1
  309. package/scripts/test-governance.ts +51 -2
  310. package/src/cli/agent-jobs/job-manager.ts +10 -1
  311. package/src/cli/chatgpt-browser/bind-server.ts +2 -1
  312. package/src/cli/chatgpt-browser/binding.ts +20 -10
  313. package/src/cli/chatgpt-browser/bridge-extension.ts +75 -0
  314. package/src/cli/chatgpt-browser/bridge-provider.ts +376 -11
  315. package/src/cli/chatgpt-browser/engine.ts +10 -11
  316. package/src/cli/chatgpt-browser/native-provider.ts +24 -83
  317. package/src/cli/chatgpt-browser/oracle-provider.ts +5 -3
  318. package/src/cli/chatgpt-browser/types.ts +1 -0
  319. package/src/cli/commands/bootstrap-control.ts +334 -0
  320. package/src/cli/commands/brain-assistant.ts +38 -0
  321. package/src/cli/commands/brain.ts +193 -40
  322. package/src/cli/commands/chatgpt.ts +127 -0
  323. package/src/cli/commands/computer.ts +437 -0
  324. package/src/cli/commands/doctor.ts +18 -1
  325. package/src/cli/commands/init-hook.ts +71 -61
  326. package/src/cli/commands/init.ts +3 -1
  327. package/src/cli/commands/mcp.ts +16 -10
  328. package/src/cli/commands/plugin.ts +38 -12
  329. package/src/cli/commands/recovery.ts +493 -80
  330. package/src/cli/commands/runtime.ts +9 -4
  331. package/src/cli/commands/setup-profile.ts +89 -39
  332. package/src/cli/commands/status.ts +2 -1
  333. package/src/cli/controller/check-runner.ts +487 -98
  334. package/src/cli/controller/context/types.ts +7 -1
  335. package/src/cli/controller/context-pack.ts +23 -3
  336. package/src/cli/controller/issue-store.ts +1 -1
  337. package/src/cli/controller/systemd-user.ts +110 -0
  338. package/src/cli/controller/types.ts +10 -70
  339. package/src/cli/controller/worklog.ts +67 -13
  340. package/src/cli/editing/edit-operation-contract.ts +92 -0
  341. package/src/cli/editing/edit-session.ts +289 -13
  342. package/src/cli/github/plugin.ts +17 -12
  343. package/src/cli/hook/route-registry.ts +3 -5
  344. package/src/cli/hook/runtime.ts +49 -3
  345. package/src/cli/index.ts +3 -0
  346. package/src/cli/local-bridge/connector-freshness.ts +2 -2
  347. package/src/cli/local-bridge/console-automations.ts +2 -2
  348. package/src/cli/local-bridge/facade-api.ts +41 -14
  349. package/src/cli/local-bridge/job-store.ts +1 -1
  350. package/src/cli/local-bridge/server.ts +24 -27
  351. package/src/cli/mcp/access-mode.ts +2 -132
  352. package/src/cli/mcp/access-tools.ts +2 -387
  353. package/src/cli/mcp/audit.ts +2 -32
  354. package/src/cli/mcp/auth.ts +2 -404
  355. package/src/cli/mcp/instructions.ts +2 -34
  356. package/src/cli/mcp/legacy-context.ts +2 -16
  357. package/src/cli/mcp/legacy-tool-service.ts +2 -5308
  358. package/src/cli/mcp/multi-repository.ts +2 -436
  359. package/src/cli/mcp/oauth.ts +2 -375
  360. package/src/cli/mcp/openai-secure-tunnel.ts +2 -0
  361. package/src/cli/mcp/paths.ts +2 -140
  362. package/src/cli/mcp/policy.ts +2 -232
  363. package/src/cli/mcp/post-finalize-work-attribution.ts +2 -150
  364. package/src/cli/mcp/proxy-env.ts +2 -62
  365. package/src/cli/mcp/readiness-projection.ts +2 -24
  366. package/src/cli/mcp/redaction.ts +2 -21
  367. package/src/cli/mcp/repo.ts +2 -24
  368. package/src/cli/mcp/repository-tools.ts +2 -1585
  369. package/src/cli/mcp/server.ts +2 -951
  370. package/src/cli/mcp/setup.ts +2 -737
  371. package/src/cli/mcp/tool-capability-cache.ts +2 -87
  372. package/src/cli/mcp/tool-contract.ts +2 -14
  373. package/src/cli/mcp/tools.ts +2 -9
  374. package/src/cli/mcp/toolset-names.ts +2 -70
  375. package/src/cli/mcp/toolset-selection.ts +2 -41
  376. package/src/cli/mcp/toolset.ts +2 -336
  377. package/src/cli/mcp/transports/AGENTS.md +1 -10
  378. package/src/cli/mcp/transports/CLAUDE.md +1 -10
  379. package/src/cli/mcp/transports/http.ts +2 -1414
  380. package/src/cli/mcp/transports/session-registry.ts +2 -490
  381. package/src/cli/mcp/transports/stdio.ts +2 -8
  382. package/src/cli/mcp/types.ts +2 -41
  383. package/src/cli/mcp/umbrella-tools.ts +2 -117
  384. package/src/cli/repositories/command-executor.ts +64 -18
  385. package/src/cli/repositories/command-scope.ts +117 -1
  386. package/src/cli/repositories/controller-home.ts +119 -1
  387. package/src/cli/repositories/identity.ts +13 -0
  388. package/src/cli/repositories/registry.ts +68 -49
  389. package/src/cli/repositories/repository-snapshot.ts +53 -7
  390. package/src/cli/repositories/runtime-storage.ts +27 -2
  391. package/src/cli/repositories/safe-patch.ts +34 -38
  392. package/src/cli/repositories/selected-path-actions.ts +22 -2
  393. package/src/cli/repositories/structured-git.ts +105 -25
  394. package/src/cli/runtime/helper-runner.ts +7 -0
  395. package/src/cli/runtime-invocation.ts +8 -1
  396. package/src/cli/tools/codegraph.ts +44 -25
  397. package/src/core/adoption/manifest-templates.ts +1 -1
  398. package/src/core/adoption/plan.ts +3 -26
  399. package/src/core/adoption/workflow-contract-plan.ts +17 -9
  400. package/src/effects/process-runner.ts +8 -14
  401. package/src/runtime/context/assistant-context.ts +154 -0
  402. package/src/runtime/context/assistant-work-context.ts +147 -0
  403. package/src/runtime/context/codegraph-cache-boundary.ts +255 -0
  404. package/src/runtime/context/codegraph-read-provider.ts +55 -22
  405. package/src/runtime/context/codegraph-sidecar.cjs +8 -1
  406. package/src/runtime/context/context-closure.ts +268 -0
  407. package/src/runtime/context/context-plane.ts +211 -0
  408. package/src/runtime/context/project-engineering-contract.ts +60 -0
  409. package/src/runtime/control-plane/bootstrap/capability-provider.ts +74 -0
  410. package/src/runtime/control-plane/bootstrap/control-api.ts +57 -0
  411. package/src/runtime/control-plane/bootstrap/index.ts +5 -0
  412. package/src/runtime/control-plane/bootstrap/state-machine.ts +141 -0
  413. package/src/runtime/control-plane/bootstrap/store.ts +21 -0
  414. package/src/runtime/control-plane/bootstrap/types.ts +87 -0
  415. package/src/runtime/control-plane/cleanup-artifact-retention.ts +328 -0
  416. package/src/runtime/control-plane/codegraph-cache-retention.ts +164 -0
  417. package/src/runtime/control-plane/concurrency/work-execution-concurrency.ts +320 -0
  418. package/src/runtime/control-plane/execution/content-equivalent-commit-authority.ts +169 -0
  419. package/src/runtime/control-plane/execution/controller-authority-recovery.ts +350 -0
  420. package/src/runtime/control-plane/execution/direct-canonical-work-reconciliation.ts +85 -0
  421. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +417 -100
  422. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +103 -15
  423. package/src/runtime/control-plane/execution/implementation-review-content.ts +95 -0
  424. package/src/runtime/control-plane/execution/repository-work-attribution.ts +2 -2
  425. package/src/runtime/control-plane/execution/retained-work-resume.ts +336 -6
  426. package/src/runtime/control-plane/execution/validation.ts +28 -3
  427. package/src/runtime/control-plane/execution/work-authority-reconciler.ts +116 -0
  428. package/src/runtime/control-plane/execution/work-completion-authority.ts +44 -2
  429. package/src/runtime/control-plane/execution/work-evidence-policy.ts +34 -3
  430. package/src/runtime/control-plane/execution/work-execution-support.ts +42 -22
  431. package/src/runtime/control-plane/execution/work-finalization-service.ts +1103 -166
  432. package/src/runtime/control-plane/execution/work-handle-authority.ts +267 -16
  433. package/src/runtime/control-plane/execution/work-handle-store.ts +77 -0
  434. package/src/runtime/control-plane/execution/work-head-settlement.ts +75 -0
  435. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +145 -23
  436. package/src/runtime/control-plane/execution/work-operation-service.ts +49 -13
  437. package/src/runtime/control-plane/execution/work-preparation-service.ts +31 -15
  438. package/src/runtime/control-plane/execution/work-remote-delivery.ts +1 -1
  439. package/src/runtime/control-plane/execution/work-task-receipt.ts +20 -1
  440. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +407 -76
  441. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +88 -18
  442. package/src/runtime/control-plane/execution/work-verification-context.ts +1 -1
  443. package/src/runtime/control-plane/execution/work-verification-service.ts +258 -4
  444. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +200 -15
  445. package/src/runtime/control-plane/facade/capability-registry.ts +21 -10
  446. package/src/runtime/control-plane/facade/controller-round-relay.ts +3 -1118
  447. package/src/runtime/control-plane/facade/controller-session-store.ts +2 -488
  448. package/src/runtime/control-plane/facade/goal-workloop-access.ts +6 -3
  449. package/src/runtime/control-plane/facade/goal-workloop.ts +972 -137
  450. package/src/runtime/control-plane/facade/handoff-inbox-application.ts +158 -0
  451. package/src/runtime/control-plane/facade/handoff-inbox-store.ts +10 -0
  452. package/src/runtime/control-plane/facade/index.ts +4 -2
  453. package/src/runtime/control-plane/facade/operation-digest.ts +22 -58
  454. package/src/runtime/control-plane/facade/plan-contract-store.ts +1004 -38
  455. package/src/runtime/control-plane/facade/repository-work-admission.ts +19 -1
  456. package/src/runtime/control-plane/facade/requirement-authority.ts +218 -0
  457. package/src/runtime/control-plane/facade/requirement-board.ts +6 -1
  458. package/src/runtime/control-plane/facade/rh-work-operation-contract.ts +48 -0
  459. package/src/runtime/control-plane/facade/semantic-admission.ts +8 -16
  460. package/src/runtime/control-plane/facade/suggested-actions.ts +4 -1
  461. package/src/runtime/control-plane/facade/types.ts +189 -532
  462. package/src/runtime/control-plane/facade/work-admission-policy.ts +84 -35
  463. package/src/runtime/control-plane/facade/work-contract-store.ts +2 -1043
  464. package/src/runtime/control-plane/facade/work-implementation-review.ts +2 -0
  465. package/src/runtime/control-plane/facade/work-state-machine.ts +2 -250
  466. package/src/runtime/control-plane/generated-cache-retention.ts +239 -18
  467. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +12 -1
  468. package/src/runtime/control-plane/global-scheduler/maintenance.ts +76 -13
  469. package/src/runtime/control-plane/global-scheduler/periodic-cleanup-entry.ts +55 -0
  470. package/src/runtime/control-plane/global-scheduler/periodic-cleanup-process.ts +98 -0
  471. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +1 -1
  472. package/src/runtime/control-plane/global-scheduler/scheduler-worker-entry.ts +10 -0
  473. package/src/runtime/control-plane/global-scheduler/scheduler.ts +173 -81
  474. package/src/runtime/control-plane/global-scheduler/worker-launch.ts +18 -6
  475. package/src/runtime/control-plane/launcher/chatgpt-round-continuation.ts +397 -0
  476. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +2 -177
  477. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +301 -560
  478. package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +20 -4
  479. package/src/runtime/control-plane/launcher/thin-launcher.ts +50 -4
  480. package/src/runtime/control-plane/lifecycle-retention-metrics.ts +60 -0
  481. package/src/runtime/control-plane/persistence/experience-store.ts +169 -0
  482. package/src/runtime/control-plane/persistence/operational-prior-store.ts +348 -0
  483. package/src/runtime/control-plane/persistence/requirement-store.ts +30 -22
  484. package/src/runtime/control-plane/persistence/sqlite-store.ts +545 -76
  485. package/src/runtime/control-plane/persistence/workflow-content-store.ts +98 -0
  486. package/src/runtime/control-plane/persistence/workflow-registry-store.ts +186 -0
  487. package/src/runtime/control-plane/persistence/workflow-run-store.ts +123 -0
  488. package/src/runtime/control-plane/release-retention.ts +56 -16
  489. package/src/runtime/control-plane/repository-namespace-retention.ts +208 -0
  490. package/src/runtime/control-plane/routing/route-policy.ts +19 -0
  491. package/src/runtime/control-plane/runtime-cleanup.ts +574 -37
  492. package/src/runtime/control-plane/runtime-generation.ts +6 -0
  493. package/src/runtime/control-plane/workspace/semantic-sync-service.ts +465 -0
  494. package/src/runtime/control-plane/workspace/workspace-store.ts +80 -0
  495. package/src/runtime/diagnostics/incident-repair.ts +406 -0
  496. package/src/runtime/diagnostics/mcp-timing.ts +125 -6
  497. package/src/runtime/diagnostics/performance.ts +6 -14
  498. package/src/runtime/diagnostics/read-only-tool.ts +1 -1
  499. package/src/runtime/evidence/artifact-store.ts +65 -2
  500. package/src/runtime/evidence/operational-shadow.ts +38 -0
  501. package/src/runtime/evidence/process-check-receipt.ts +5 -43
  502. package/src/runtime/evidence/sensitive-output.ts +4 -4
  503. package/src/runtime/execution/jobs/receipt-recovery.ts +1 -1
  504. package/src/runtime/execution/jobs/store.ts +121 -2
  505. package/src/runtime/execution/managed-workspace.ts +55 -5
  506. package/src/runtime/execution/process-runtime/canary.ts +19 -5
  507. package/src/runtime/execution/process-runtime/check-classification.ts +48 -0
  508. package/src/runtime/execution/process-runtime/check-facade.ts +23 -9
  509. package/src/runtime/execution/process-runtime/check-receipt.ts +1 -1
  510. package/src/runtime/execution/process-runtime/check-result-retention.ts +132 -0
  511. package/src/runtime/execution/process-runtime/check-result.ts +73 -2
  512. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +42 -3
  513. package/src/runtime/execution/process-runtime/check-storage.ts +187 -0
  514. package/src/runtime/execution/process-runtime/command-facade.ts +38 -87
  515. package/src/runtime/execution/process-runtime/index.ts +1 -0
  516. package/src/runtime/execution/process-runtime/interactive-admission.ts +2 -1
  517. package/src/runtime/execution/process-runtime/lightweight-managed.ts +32 -1
  518. package/src/runtime/execution/process-runtime/persisted-check.ts +128 -21
  519. package/src/runtime/execution/process-runtime/resource-claims.ts +60 -3
  520. package/src/runtime/execution/process-runtime/runtime.ts +132 -15
  521. package/src/runtime/execution/process-runtime/types.ts +22 -0
  522. package/src/runtime/execution/workers/executor.ts +16 -13
  523. package/src/runtime/execution/workers/worker-entry.ts +1 -1
  524. package/src/runtime/gateway/mcp/execution-tools.ts +2 -207
  525. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +2 -61
  526. package/src/runtime/gateway/mcp/persisted-check-process.ts +2 -3
  527. package/src/runtime/gateway/mcp/process-tools.ts +2 -289
  528. package/src/runtime/gateway/mcp/router.ts +2 -1125
  529. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +2 -565
  530. package/src/runtime/gateway/mcp/runtime-tools.ts +2 -6651
  531. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +2 -1
  532. package/src/runtime/maintenance/cleanup.ts +38 -24
  533. package/src/runtime/platform/browser-product-discovery.ts +174 -0
  534. package/src/runtime/platform/computer-platform.ts +14 -0
  535. package/src/runtime/platform/executable-discovery.ts +54 -0
  536. package/src/runtime/platform/fetch-http-server.ts +90 -0
  537. package/src/runtime/platform/service-manager.ts +138 -0
  538. package/src/runtime/platform/windows-host-discovery.ts +118 -0
  539. package/src/runtime/platform/windows-recovery-deployment.ts +67 -0
  540. package/src/runtime/plugins/action-application.ts +135 -0
  541. package/src/runtime/plugins/app-store-connect-adapter.ts +9 -21
  542. package/src/runtime/plugins/browser-adapter.ts +140 -100
  543. package/src/runtime/plugins/browser-automation-service.ts +15 -36
  544. package/src/runtime/plugins/browser-macos-bridge.ts +69 -21
  545. package/src/runtime/plugins/browser-manifest-surface.ts +30 -6
  546. package/src/runtime/plugins/browser-native-session-service.ts +1 -1
  547. package/src/runtime/plugins/browser-provider-registry.ts +2 -109
  548. package/src/runtime/plugins/browser-registration.ts +16 -0
  549. package/src/runtime/plugins/browser-runtime-contract.ts +2 -154
  550. package/src/runtime/plugins/browser-runtime.ts +41 -9
  551. package/src/runtime/plugins/browser-session-authority.ts +101 -291
  552. package/src/runtime/plugins/browser-session-legacy-migration.ts +203 -0
  553. package/src/runtime/plugins/browser-session-store.ts +229 -36
  554. package/src/runtime/plugins/browser-session-types.ts +2 -98
  555. package/src/runtime/plugins/capability-authorization-grants.ts +17 -0
  556. package/src/runtime/plugins/catalog-trust.ts +82 -0
  557. package/src/runtime/plugins/computer-registration.ts +839 -0
  558. package/src/runtime/plugins/config-store.ts +119 -0
  559. package/src/runtime/plugins/desktop-operator-external-policy.ts +932 -0
  560. package/src/runtime/plugins/desktop-operator-registration.ts +111 -8
  561. package/src/runtime/plugins/errors.ts +8 -1
  562. package/src/runtime/plugins/external-adapter.ts +45 -322
  563. package/src/runtime/plugins/external-provider-policy.ts +48 -0
  564. package/src/runtime/plugins/external-registration.ts +9 -1
  565. package/src/runtime/plugins/external-unix-socket-probe.cjs +9 -3
  566. package/src/runtime/plugins/external-unix-socket.ts +64 -193
  567. package/src/runtime/plugins/first-party-external-registration.ts +163 -0
  568. package/src/runtime/plugins/first-party-registry.ts +3 -1
  569. package/src/runtime/plugins/github-adapter.ts +1 -2
  570. package/src/runtime/plugins/gmail-adapter.ts +6 -6
  571. package/src/runtime/plugins/google-calendar-adapter.ts +6 -6
  572. package/src/runtime/plugins/google-shared.ts +31 -46
  573. package/src/runtime/plugins/google-tasks-adapter.ts +6 -6
  574. package/src/runtime/plugins/ios-adapter.ts +1 -2
  575. package/src/runtime/plugins/local-system-adapter.ts +180 -0
  576. package/src/runtime/plugins/macos-capability-broker.ts +15 -105
  577. package/src/runtime/plugins/resend-adapter.ts +15 -24
  578. package/src/runtime/plugins/store.ts +493 -51
  579. package/src/runtime/plugins/types.ts +8 -1
  580. package/src/runtime/plugins/uu-remote-rescue-registration.ts +155 -0
  581. package/src/runtime/plugins/windows-host-recovery-install.ts +68 -0
  582. package/src/runtime/plugins/windows-host-recovery-registration.ts +85 -0
  583. package/src/runtime/plugins/xiaohongshu-publish.ts +86 -430
  584. package/src/runtime/projections/controller-context.ts +12 -6
  585. package/src/runtime/projections/git-status-sampler.ts +73 -22
  586. package/src/runtime/projections/materialized-view.ts +14 -8
  587. package/src/runtime/recovery/capability-action-application.ts +228 -0
  588. package/src/runtime/recovery/index.ts +3 -0
  589. package/src/runtime/recovery/lifecycle-inventory.ts +338 -0
  590. package/src/runtime/recovery/local-jobs-repair.ts +14 -14
  591. package/src/runtime/recovery/maintenance-executor.ts +358 -76
  592. package/src/runtime/recovery/quarantine-retention.ts +256 -0
  593. package/src/runtime/release/identity.ts +6 -0
  594. package/src/runtime/resources/leases/store.ts +1 -1
  595. package/src/runtime/root/active-execution-power-assertion.ts +139 -0
  596. package/src/runtime/root/assistant-learning-loop.ts +45 -0
  597. package/src/runtime/root/browser-session-composition.ts +59 -0
  598. package/src/runtime/root/computer-composition.ts +191 -0
  599. package/src/runtime/root/computer-target-composition.ts +16 -0
  600. package/src/runtime/root/computer-target-persistence.ts +39 -0
  601. package/src/runtime/root/controller-round-composition.ts +107 -0
  602. package/src/runtime/root/controller-services.ts +2 -0
  603. package/src/runtime/root/entry.ts +4 -3
  604. package/src/runtime/root/gateway-adapter.ts +3 -3
  605. package/src/runtime/root/local-bridge.ts +5 -2
  606. package/src/runtime/root/mcp-transport.ts +5 -5
  607. package/src/runtime/root/package-connector-service.ts +108 -56
  608. package/src/runtime/root/package-runtime-release.ts +86 -18
  609. package/src/runtime/root/package-runtime-service.ts +63 -86
  610. package/src/runtime/root/release-execution-canary.ts +71 -0
  611. package/src/runtime/root/release-loader.ts +68 -0
  612. package/src/runtime/root/release-manifest.ts +106 -2
  613. package/src/runtime/root/release-materialize.ts +103 -4
  614. package/src/runtime/root/release-store.ts +31 -2
  615. package/src/runtime/root/runtime.ts +240 -25
  616. package/src/runtime/root/scheduled-controller-composition.ts +51 -0
  617. package/src/runtime/root/scheduler.ts +12 -4
  618. package/src/runtime/root/service-runner.ts +1 -1
  619. package/src/runtime/root/service.ts +41 -16
  620. package/src/runtime/root/status.ts +1 -0
  621. package/src/runtime/root/types.ts +24 -1
  622. package/src/runtime/shared/local-bridge-surface.ts +1 -1
  623. package/src/runtime/shared/process-environment.ts +19 -4
  624. package/src/runtime/shared/storage-capacity.ts +68 -0
  625. package/src/runtime/standalone-recovery/controller-home-migration.ts +1116 -0
  626. package/src/runtime/standalone-recovery/core.ts +984 -230
  627. package/src/runtime/standalone-recovery/entry.ts +182 -51
  628. package/src/runtime/standalone-recovery/independent-host-rescue.ts +172 -0
  629. package/src/runtime/standalone-recovery/installer.ts +212 -104
  630. package/src/runtime/standalone-recovery/mcp-server.ts +243 -0
  631. package/src/runtime/standalone-recovery/mutation-identity-contract.ts +21 -0
  632. package/src/runtime/standalone-recovery/operation-lock.ts +160 -0
  633. package/src/runtime/standalone-recovery/performance.ts +135 -0
  634. package/src/runtime/standalone-recovery/release.ts +5 -0
  635. package/src/runtime/watchdog/workflow-watchdog.ts +1 -1
  636. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +1 -1
  637. package/src/runtime/workflow/schedules/engine.ts +191 -343
  638. package/src/runtime/workflow/schedules/pending-activation.ts +1 -1
  639. package/src/runtime/workflow/schedules/settlement.ts +2 -191
  640. package/src/runtime/workflow/schedules/store.ts +2 -607
  641. package/src/runtime/workflow/schedules/types.ts +2 -127
  642. package/src/runtime/workflow/schedules/work-continuation.ts +180 -15
  643. package/src/runtime/workflows/first-party/xiaohongshu.ts +122 -0
  644. package/src/runtime/workflows/runtime.ts +486 -0
  645. package/src/testing/test-governance.ts +90 -29
  646. package/assets/templates/helpers/archive-architecture-request.sh +0 -244
  647. package/docs/architecture/history/README.md +0 -7
  648. package/docs/architecture/history/chatgpt-handoff-facade.md +0 -397
  649. package/docs/architecture/history/global-hook-runtime.md +0 -166
  650. package/docs/architecture/history/ios-semantic-automation-provider-v2.md +0 -170
  651. package/docs/architecture/requests/archive/2026/20260528-154740-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
  652. package/docs/architecture/requests/archive/2026/20260528-154816-assets-hooks-assets-hooks-hook-input-sh.md +0 -59
  653. package/docs/architecture/requests/archive/2026/20260528-161851-ai-harness-workflow-contract-json-ai-harness-workflow-contract-json.md +0 -59
  654. package/docs/architecture/requests/archive/2026/20260528-161853-scripts-check-task-workflow-sh-scripts-check-task-workflow-sh.md +0 -59
  655. package/docs/architecture/requests/archive/2026/20260528-161911-assets-workflow-contract-v1-json-assets-workflow-contract-v1-json.md +0 -59
  656. package/docs/architecture/requests/archive/2026/20260528-222416-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
  657. package/docs/architecture/requests/archive/2026/20260528-222444-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
  658. package/docs/architecture/requests/archive/2026/20260528-222457-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
  659. package/docs/architecture/requests/archive/2026/20260528-222648-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
  660. package/docs/architecture/requests/archive/2026/20260528-222705-ai-hooks-ai-hooks-worktree-guard-sh.md +0 -59
  661. package/docs/architecture/requests/archive/2026/20260528-222726-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
  662. package/docs/architecture/requests/archive/2026/20260528-222727-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
  663. package/docs/architecture/requests/archive/2026/20260528-224736-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
  664. package/docs/architecture/requests/archive/2026/20260528-233259-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
  665. package/docs/architecture/requests/archive/2026/20260528-233335-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
  666. package/docs/architecture/requests/archive/2026/20260528-233456-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
  667. package/docs/architecture/requests/archive/2026/20260528-233457-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
  668. package/docs/architecture/requests/archive/2026/20260528-233637-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
  669. package/docs/architecture/requests/archive/2026/20260528-233638-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
  670. package/docs/architecture/requests/archive/2026/20260529-000322-root-package-json.md +0 -59
  671. package/docs/architecture/requests/archive/2026/20260529-004238-ai-hooks-ai-hooks-lib-workflow-state-sh.md +0 -59
  672. package/docs/architecture/requests/archive/2026/20260529-020654-ai-harness-policy-json-ai-harness-policy-json.md +0 -63
  673. package/docs/architecture/requests/archive/2026/20260529-094236-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -60
  674. package/docs/architecture/requests/archive/2026/20260529-094323-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -60
  675. package/docs/architecture/requests/archive/2026/20260529-094331-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -60
  676. package/docs/architecture/requests/archive/2026/20260529-094339-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -60
  677. package/docs/architecture/requests/archive/2026/20260529-094430-assets-hooks-assets-hooks-session-start-context-sh.md +0 -60
  678. package/docs/architecture/requests/archive/2026/20260529-094446-ai-hooks-ai-hooks-session-start-context-sh.md +0 -60
  679. package/docs/architecture/requests/archive/2026/20260613-002040-runtime-harness-hook-adapters.md +0 -68
  680. package/docs/architecture/requests/archive/2026/root.md +0 -68
  681. package/docs/architecture/requests/archive/2026/runtime-harness-hook-adapters.md +0 -76
  682. package/docs/architecture/requests/archive/2026/verification-evals-checks.md +0 -68
  683. package/docs/architecture/requests/archive/2026/workflow-engine-contract-assets.md +0 -69
  684. package/scripts/archive-architecture-request.sh +0 -4
  685. package/scripts/run-skill-evals.ts +0 -1022
@@ -1,3 +1,4 @@
1
+ import { createHash } from 'crypto';
1
2
  import { mkdirSync } from 'fs';
2
3
  import { join } from 'path';
3
4
  import { repositoryControllerRoot } from '../../../cli/repositories/controller-home';
@@ -5,6 +6,7 @@ import { withControllerLock } from '../../../cli/repositories/locks';
5
6
  import { readJsonFile, sanitizeFileComponent, writeJsonAtomic } from '../../shared/json-files';
6
7
  import {
7
8
  normalizePlanScopeKey,
9
+ isPlanExtensionPredecessor,
8
10
  resolvePlanAdmission,
9
11
  withPlanAdmissionLock,
10
12
  withPlanAdmissionLockAsync,
@@ -13,19 +15,34 @@ import {
13
15
  } from './semantic-admission';
14
16
  import {
15
17
  listControlPlaneRecords,
18
+ listAllControlPlaneRecordsWithinTransaction,
16
19
  readControlPlaneRecordWithinTransaction,
17
20
  withControlPlaneTransaction,
18
21
  writeControlPlaneRecordWithinTransaction,
19
22
  } from '../persistence/sqlite-store';
20
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';
21
35
  import {
22
36
  isTerminalPlanContractStatus,
23
37
  type EvidenceRef,
24
38
  type PlanContract,
25
39
  type PlanContractStatus,
26
40
  type PlanContractStore,
41
+ type PlanObligationDisposition,
42
+ type PlanRevisionDraft,
43
+ type PlanRevisionRecord,
27
44
  type PlanStep,
28
- type WorkContract,
45
+ type PlanStepDeliveryCarry,
29
46
  } from './types';
30
47
 
31
48
  export interface PlanContractStoreLocation {
@@ -36,6 +53,8 @@ export interface PlanContractStoreLocation {
36
53
 
37
54
  export interface PlanContractStoreOptions extends PlanContractStoreLocation {
38
55
  now?: () => string;
56
+ /** Prove that one delivered Git revision is contained in a later Plan source. Missing/failed proof is fail-closed. */
57
+ revisionContains?: (ancestorRevision: string, descendantRevision: string) => boolean;
39
58
  }
40
59
 
41
60
  export interface CreatePlanContractInput {
@@ -53,6 +72,7 @@ export interface CreatePlanContractInput {
53
72
  integrationStrategy?: string;
54
73
  steps: Array<Omit<PlanStep, 'status' | 'evidenceRefs'> & Partial<Pick<PlanStep, 'status' | 'evidenceRefs'>>>;
55
74
  evidenceRefs?: EvidenceRef[];
75
+ obligationDispositions?: PlanObligationDisposition[];
56
76
  }
57
77
 
58
78
  export interface AdmitPlanContractInput extends CreatePlanContractInput {
@@ -69,6 +89,7 @@ export type AdmitPlanContractResult = PlanAdmissionResolution;
69
89
 
70
90
  export interface PlanContractSummary {
71
91
  planId: string;
92
+ revision: number;
72
93
  repoId: string;
73
94
  requirementId?: string;
74
95
  scopeKey: string;
@@ -92,6 +113,29 @@ function normalizeScopeKey(value: string): string {
92
113
  return normalizePlanScopeKey(value) || 'unknown';
93
114
  }
94
115
 
116
+ export function currentPlanRevision(plan: PlanContract): number {
117
+ const revision = Number(plan.revision);
118
+ return Number.isInteger(revision) && revision > 0 ? revision : 1;
119
+ }
120
+
121
+ interface PlanRevisionRecordStore {
122
+ schemaVersion: 1;
123
+ updatedAt: string;
124
+ revisions: PlanRevisionRecord[];
125
+ }
126
+
127
+ function planRevisionRecordKey(planId: string, revision: number): string {
128
+ return `${sanitizeFileComponent(planId)}:r${revision}`;
129
+ }
130
+
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
+
95
139
  function normalizeStep(step: CreatePlanContractInput['steps'][number]): PlanStep {
96
140
  return {
97
141
  id: sanitizeFileComponent(step.id).slice(0, 120),
@@ -111,6 +155,133 @@ function normalizeStep(step: CreatePlanContractInput['steps'][number]): PlanStep
111
155
  };
112
156
  }
113
157
 
158
+ function normalizeObligationDisposition(value: PlanObligationDisposition): PlanObligationDisposition {
159
+ const disposition = value.disposition;
160
+ if (!['keep', 'change', 'defer', 'drop'].includes(disposition)) throw new Error(`PLAN_OBLIGATION_DISPOSITION_INVALID: ${String(disposition)}`);
161
+ return {
162
+ predecessorPlanId: sanitizeFileComponent(value.predecessorPlanId).slice(0, 160),
163
+ obligationId: String(value.obligationId ?? '').trim().slice(0, 160),
164
+ disposition,
165
+ successorRefs: bounded(value.successorRefs, 20, 240),
166
+ ...(value.rationale?.trim() ? { rationale: value.rationale.trim().slice(0, 1_000) } : {}),
167
+ };
168
+ }
169
+
170
+ export interface PlanObligation {
171
+ obligationId: string;
172
+ predecessorPlanId: string;
173
+ kind: 'step_objective' | 'step_acceptance' | 'non_goal' | 'resolved_decision' | 'stop_condition' | 'replan_condition';
174
+ sourceRef: string;
175
+ summary: string;
176
+ }
177
+
178
+ function planObligationId(planId: string, kind: PlanObligation['kind'], sourceRef: string, summary: string): string {
179
+ const digest = createHash('sha256').update(JSON.stringify({ planId, kind, sourceRef, summary })).digest('hex').slice(0, 24);
180
+ return `obl_${digest}`;
181
+ }
182
+
183
+ function listPlanObligations(plan: PlanContract, mode: 'unresolved' | 'completed_steps'): PlanObligation[] {
184
+ const out: PlanObligation[] = [];
185
+ const add = (kind: PlanObligation['kind'], sourceRef: string, summary: string) => {
186
+ const normalized = summary.trim();
187
+ if (!normalized) return;
188
+ out.push({ obligationId: planObligationId(plan.planId, kind, sourceRef, normalized), predecessorPlanId: plan.planId, kind, sourceRef, summary: normalized });
189
+ };
190
+ for (const step of plan.steps) {
191
+ if (mode === 'unresolved' && step.status === 'completed') continue;
192
+ if (mode === 'completed_steps' && step.status !== 'completed') continue;
193
+ add('step_objective', `step:${step.id}`, step.objective);
194
+ step.acceptanceCriteria.forEach((criterion, index) => add('step_acceptance', `step:${step.id}:acceptance:${index}`, criterion));
195
+ }
196
+ if (mode === 'unresolved') {
197
+ plan.nonGoals.forEach((value, index) => add('non_goal', `non_goal:${index}`, value));
198
+ plan.resolvedDecisions.forEach((value, index) => add('resolved_decision', `resolved_decision:${index}`, value));
199
+ plan.stopConditions.forEach((value, index) => add('stop_condition', `stop_condition:${index}`, value));
200
+ plan.replanConditions.forEach((value, index) => add('replan_condition', `replan_condition:${index}`, value));
201
+ }
202
+ return out;
203
+ }
204
+
205
+ export function listUnresolvedPlanObligations(plan: PlanContract): PlanObligation[] {
206
+ return listPlanObligations(plan, 'unresolved');
207
+ }
208
+
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
+
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
+
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
+
114
285
  function updatePlanContract(
115
286
  options: PlanContractStoreOptions,
116
287
  planId: string,
@@ -139,8 +310,10 @@ function updatePlanContract(
139
310
  function assertRequirementReference(options: PlanContractStoreOptions, requirementId: string | undefined): void {
140
311
  const normalized = requirementId?.trim();
141
312
  if (!normalized || !options.controllerHome) return;
142
- if (!readRequirement({ controllerHome: options.controllerHome, now: options.now }, normalized)) {
143
- throw new Error(`PLAN_REQUIREMENT_NOT_FOUND: ${normalized}`);
313
+ const requirement = readRequirement({ controllerHome: options.controllerHome, now: options.now }, normalized)?.value;
314
+ if (!requirement) throw new Error(`PLAN_REQUIREMENT_NOT_FOUND: ${normalized}`);
315
+ if (requirement.state === 'done' || requirement.state === 'cancelled') {
316
+ throw new Error(`PLAN_REQUIREMENT_TERMINAL: ${normalized}:${requirement.state}`);
144
317
  }
145
318
  }
146
319
 
@@ -161,6 +334,116 @@ export function planContractStorePath(location: PlanContractStoreLocation): stri
161
334
  return join(planContractRoot(location), 'index.json');
162
335
  }
163
336
 
337
+ export function planRevisionStorePath(location: PlanContractStoreLocation): string {
338
+ return join(planContractRoot(location), 'revisions.json');
339
+ }
340
+
341
+ export function listPlanRevisionRecords(
342
+ options: PlanContractStoreOptions,
343
+ planId?: string,
344
+ ): PlanRevisionRecord[] {
345
+ const normalizedPlanId = planId ? sanitizeFileComponent(planId) : undefined;
346
+ if (!sqliteBacked(options)) {
347
+ const store = readJsonFile<PlanRevisionRecordStore>(planRevisionStorePath(options), { schemaVersion: 1, updatedAt: nowIso(options), revisions: [] });
348
+ return store.revisions
349
+ .filter((record) => !normalizedPlanId || record.planId === normalizedPlanId)
350
+ .sort((left, right) => right.revision - left.revision);
351
+ }
352
+ return listControlPlaneRecords<PlanRevisionRecord>(options.controllerHome, {
353
+ namespace: 'plan_revision', scope: options.repoId, limit: 5_000,
354
+ }).map((record) => record.value)
355
+ .filter((record) => !normalizedPlanId || record.planId === normalizedPlanId)
356
+ .sort((left, right) => right.revision - left.revision);
357
+ }
358
+
359
+ function appendJsonPlanRevisionRecord(options: PlanContractStoreOptions, record: PlanRevisionRecord): void {
360
+ const path = planRevisionStorePath(options);
361
+ const store = readJsonFile<PlanRevisionRecordStore>(path, { schemaVersion: 1, updatedAt: record.recordedAt, revisions: [] });
362
+ if (store.revisions.some((existing) => existing.planId === record.planId && existing.revision === record.revision)) {
363
+ throw new Error(`PLAN_REVISION_ALREADY_EXISTS: ${record.planId}:r${record.revision}`);
364
+ }
365
+ writeJsonAtomic(path, { schemaVersion: 1, updatedAt: record.recordedAt, revisions: [record, ...store.revisions] });
366
+ }
367
+
368
+ function planRevisionRecord(plan: PlanContract, input: { recordedAt: string; reason: string; requestedRevisionLabel?: string }): PlanRevisionRecord {
369
+ return {
370
+ schemaVersion: 1, repoId: plan.repoId, planId: plan.planId, revision: currentPlanRevision(plan),
371
+ requirementId: plan.requirementId, scopeKey: plan.scopeKey, sourceRevision: plan.sourceRevision, goal: plan.goal,
372
+ nonGoals: [...plan.nonGoals], assumptions: [...plan.assumptions], resolvedDecisions: [...plan.resolvedDecisions],
373
+ stopConditions: [...plan.stopConditions], replanConditions: [...plan.replanConditions], integrationStrategy: plan.integrationStrategy,
374
+ status: plan.status, steps: structuredClone(plan.steps), evidenceRefs: structuredClone(plan.evidenceRefs),
375
+ obligationDispositions: plan.obligationDispositions ? structuredClone(plan.obligationDispositions) : undefined,
376
+ recordedAt: input.recordedAt, reason: input.reason.slice(0, 1_000), requestedRevisionLabel: input.requestedRevisionLabel,
377
+ };
378
+ }
379
+
380
+ function revisionDraftFromCandidate(candidate: PlanContract, input: { requestedRevisionLabel?: string; createdAt: string; updatedAt: string }): PlanRevisionDraft {
381
+ return {
382
+ revision: currentPlanRevision(candidate), requestedRevisionLabel: input.requestedRevisionLabel,
383
+ sourceRevision: candidate.sourceRevision, goal: candidate.goal, nonGoals: [...candidate.nonGoals], assumptions: [...candidate.assumptions],
384
+ resolvedDecisions: [...candidate.resolvedDecisions], stopConditions: [...candidate.stopConditions], replanConditions: [...candidate.replanConditions],
385
+ integrationStrategy: candidate.integrationStrategy, steps: structuredClone(candidate.steps),
386
+ obligationDispositions: candidate.obligationDispositions ? structuredClone(candidate.obligationDispositions) : undefined,
387
+ deliveryCarries: candidate.deliveryCarries ? structuredClone(candidate.deliveryCarries) : undefined,
388
+ createdAt: input.createdAt, updatedAt: input.updatedAt,
389
+ };
390
+ }
391
+
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
+
417
+ function assertRevisionScopeAuthority(current: PlanContract, allPlans: readonly PlanContract[]): void {
418
+ const conflicting = allPlans.find((plan) => plan.planId !== current.planId
419
+ && !isTerminalPlanContractStatus(plan.status) && plan.scopeKey === current.scopeKey);
420
+ if (conflicting) throw new Error(`PLAN_SCOPE_ALREADY_OWNED: ${current.scopeKey}:${conflicting.planId}`);
421
+ }
422
+
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
+
164
447
  export function emptyPlanContractStore(updatedAt: string): PlanContractStore {
165
448
  return { schemaVersion: 1, updatedAt, contracts: [] };
166
449
  }
@@ -220,6 +503,9 @@ function writePlanContractStore(options: PlanContractStoreOptions, store: PlanCo
220
503
  options.repoId,
221
504
  contract.planId,
222
505
  );
506
+ // SQLite is authoritative per Plan row. A sibling Plan appearing in an
507
+ // aggregate compatibility snapshot is not itself a mutation.
508
+ if (current && JSON.stringify(current.value) === JSON.stringify(contract)) continue;
223
509
  writeControlPlaneRecordWithinTransaction(database, {
224
510
  namespace: 'plan_contract',
225
511
  scope: options.repoId,
@@ -234,12 +520,81 @@ function writePlanContractStore(options: PlanContractStoreOptions, store: PlanCo
234
520
  return store;
235
521
  }
236
522
 
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
+
237
591
  function buildPlanContract(input: CreatePlanContractInput, at: string): PlanContract {
238
592
  const planId = sanitizeFileComponent(input.planId);
239
593
  if (!String(input.planId ?? '').trim() || planId === 'unknown') throw new Error('plan_id is required');
240
594
  return {
241
595
  schemaVersion: 1,
242
596
  planId,
597
+ revision: 1,
243
598
  repoId: input.repoId,
244
599
  requirementId: input.requirementId?.trim().slice(0, 160) || undefined,
245
600
  scopeKey: normalizeScopeKey(input.scopeKey),
@@ -254,6 +609,7 @@ function buildPlanContract(input: CreatePlanContractInput, at: string): PlanCont
254
609
  status: 'draft',
255
610
  steps: input.steps.slice(0, 30).map(normalizeStep),
256
611
  evidenceRefs: (input.evidenceRefs ?? []).slice(0, 20),
612
+ obligationDispositions: (input.obligationDispositions ?? []).slice(0, 512).map(normalizeObligationDisposition),
257
613
  createdAt: at,
258
614
  updatedAt: at,
259
615
  };
@@ -285,6 +641,176 @@ function draftContentErrors(plan: PlanContract): string[] {
285
641
  return [...new Set(errors)];
286
642
  }
287
643
 
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
+
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
+
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
+
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
+
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
+ }
813
+
288
814
  function createPlanContractUnlocked(options: PlanContractStoreOptions, input: CreatePlanContractInput): PlanContract {
289
815
  assertRequirementReference(options, input.requirementId);
290
816
  const at = nowIso(options);
@@ -308,22 +834,54 @@ function replacePlanContractUnlocked(
308
834
  const predecessorKey = sanitizeFileComponent(predecessorId);
309
835
  const predecessorIndex = store.contracts.findIndex((contract) => contract.planId === predecessorKey);
310
836
  if (predecessorIndex < 0) throw new Error(`plan contract not found: ${predecessorKey}`);
311
- const predecessor = store.contracts[predecessorIndex]!;
312
- if (isTerminalPlanContractStatus(predecessor.status)) throw new Error(`plan contract ${predecessor.planId} is terminal (${predecessor.status})`);
313
- const successor = buildPlanContract(input, at);
314
- if (successor.planId === predecessor.planId) throw new Error('PLAN_SUCCESSOR_ID_MUST_CHANGE');
315
- if (successor.repoId !== predecessor.repoId) throw new Error('PLAN_SUCCESSOR_REPOSITORY_MISMATCH');
316
- if (store.contracts.some((existing) => existing.planId === successor.planId)) {
317
- throw new Error(`plan contract already exists: ${successor.planId}`);
318
- }
319
- const conflictingScope = store.contracts.find((existing, index) => index !== predecessorIndex
320
- && !isTerminalPlanContractStatus(existing.status)
321
- && existing.scopeKey === successor.scopeKey);
322
- if (conflictingScope) throw new Error(`PLAN_SCOPE_ALREADY_OWNED: ${successor.scopeKey}:${conflictingScope.planId}`);
837
+ const current = store.contracts[predecessorIndex]!;
838
+ if (!isPlanExtensionPredecessor(current)) throw new Error(`plan contract ${current.planId} is terminal (${current.status})`);
839
+
840
+ // Before approval Plan identity is already provisional, so an extension simply
841
+ // repairs that exact draft instead of minting an r2/r3 entity.
842
+ 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
+ return repairDraftPlanContractUnlocked(options, current.planId, {
855
+ scopeKey: input.scopeKey, sourceRevision: input.sourceRevision, goal: input.goal, nonGoals: input.nonGoals, assumptions: input.assumptions,
856
+ resolvedDecisions: input.resolvedDecisions, stopConditions: input.stopConditions, replanConditions: input.replanConditions,
857
+ integrationStrategy: input.integrationStrategy, steps: input.steps, obligationDispositions: input.obligationDispositions,
858
+ });
859
+ }
860
+ if (current.pendingRevision) throw new Error(`PLAN_REVISION_DRAFT_ALREADY_EXISTS: ${current.planId}:r${current.pendingRevision.revision}`);
861
+ if (input.requirementId?.trim() !== current.requirementId?.trim()) throw new Error('PLAN_REVISION_REQUIREMENT_IDENTITY_MISMATCH');
862
+ if (normalizeScopeKey(input.scopeKey) !== current.scopeKey) throw new Error(`PLAN_REVISION_SCOPE_IDENTITY_MISMATCH: ${current.scopeKey}`);
863
+
864
+ const candidate = {
865
+ ...buildPlanContract({ ...input, planId: current.planId, repoId: current.repoId, requirementId: current.requirementId, scopeKey: current.scopeKey }, at),
866
+ revision: currentPlanRevision(current) + 1,
867
+ supersedes: current.supersedes ? [...current.supersedes] : undefined,
868
+ };
869
+ const contentErrors = draftContentErrors(candidate);
870
+ 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
+ assertRevisionScopeAuthority(current, store.contracts);
874
+ const deliveryCarries = deriveRequiredRevisionDeliveryCarries(options, current, candidate, at);
875
+ const stagedCandidate = deliveryCarries.length > 0 ? { ...candidate, deliveryCarries } : candidate;
876
+ const requestedRevisionLabel = sanitizeFileComponent(input.planId);
877
+ const pendingRevision = revisionDraftFromCandidate(stagedCandidate, {
878
+ requestedRevisionLabel: requestedRevisionLabel !== current.planId ? requestedRevisionLabel : undefined, createdAt: at, updatedAt: at,
879
+ });
880
+ const next: PlanContract = { ...current, status: 'replanning', pendingRevision, updatedAt: at };
323
881
  const contracts = [...store.contracts];
324
- contracts[predecessorIndex] = { ...predecessor, status: 'superseded', supersededBy: successor.planId, updatedAt: at };
325
- writePlanContractStore(options, { schemaVersion: 1, updatedAt: at, contracts: [successor, ...contracts] });
326
- return successor;
882
+ contracts[predecessorIndex] = next;
883
+ writePlanContractStore(options, { schemaVersion: 1, updatedAt: at, contracts });
884
+ return next;
327
885
  }
328
886
 
329
887
  function repairDraftPlanContractUnlocked(
@@ -336,9 +894,11 @@ function repairDraftPlanContractUnlocked(
336
894
  const index = store.contracts.findIndex((contract) => contract.planId === key);
337
895
  if (index < 0) throw new Error(`plan contract not found: ${key}`);
338
896
  const current = store.contracts[index]!;
339
- if (current.status !== 'draft') throw new Error(`PLAN_DRAFT_REPAIR_STATUS_INVALID: ${current.planId}:${current.status}`);
340
- if (input.expectedSourceRevision !== undefined && current.sourceRevision !== input.expectedSourceRevision) {
341
- throw new Error(`PLAN_DRAFT_REPAIR_STALE_SOURCE: ${current.planId}:expected=${input.expectedSourceRevision}:actual=${current.sourceRevision}`);
897
+ const pending = current.status === 'replanning' ? current.pendingRevision : undefined;
898
+ if (current.status !== 'draft' && !pending) throw new Error(`PLAN_DRAFT_REPAIR_STATUS_INVALID: ${current.planId}:${current.status}`);
899
+ const observedSourceRevision = pending?.sourceRevision ?? current.sourceRevision;
900
+ if (input.expectedSourceRevision !== undefined && observedSourceRevision !== input.expectedSourceRevision) {
901
+ throw new Error(`PLAN_DRAFT_REPAIR_STALE_SOURCE: ${current.planId}:expected=${input.expectedSourceRevision}:actual=${observedSourceRevision}`);
342
902
  }
343
903
  assertRequirementReference(options, current.requirementId);
344
904
  const at = nowIso(options);
@@ -349,16 +909,51 @@ function repairDraftPlanContractUnlocked(
349
909
  requirementId: current.requirementId,
350
910
  evidenceRefs: current.evidenceRefs,
351
911
  }, at);
352
- const contentErrors = draftContentErrors(candidate);
912
+ const candidateWithLineage = retireResolvedPredecessorObligationDispositions({
913
+ ...candidate,
914
+ ...(current.supersedes?.length ? { supersedes: [...current.supersedes] } : {}),
915
+ }, store.contracts);
916
+ const contentErrors = draftContentErrors(candidateWithLineage);
353
917
  if (contentErrors.length > 0) {
354
918
  throw new Error(`PLAN_DRAFT_REPAIR_INVALID: ${contentErrors.join('; ')}`);
355
919
  }
920
+ const continuityErrors = obligationContinuityErrors(candidateWithLineage, store.contracts);
921
+ if (continuityErrors.length > 0) {
922
+ throw new Error(`PLAN_DRAFT_REPAIR_INVALID: ${continuityErrors.join('; ')}`);
923
+ }
924
+ if (pending) {
925
+ if (normalizeScopeKey(input.scopeKey) !== current.scopeKey) throw new Error(`PLAN_REVISION_SCOPE_IDENTITY_MISMATCH: ${current.scopeKey}`);
926
+ assertRevisionScopeAuthority(current, store.contracts);
927
+ 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
+ const repairedPending = revisionDraftFromCandidate(revisionCandidate, {
934
+ requestedRevisionLabel: pending.requestedRevisionLabel, createdAt: pending.createdAt, updatedAt: at,
935
+ });
936
+ const repairedCurrent: PlanContract = { ...current, pendingRevision: repairedPending, updatedAt: at };
937
+ const contracts = [...store.contracts];
938
+ contracts[index] = repairedCurrent;
939
+ writePlanContractStore(options, { schemaVersion: 1, updatedAt: at, contracts });
940
+ return repairedCurrent;
941
+ }
942
+ const explicitPredecessors = new Set(candidateWithLineage.supersedes ?? []);
356
943
  const conflictingScope = store.contracts.find((existing, candidateIndex) => candidateIndex !== index
357
944
  && !isTerminalPlanContractStatus(existing.status)
358
- && existing.scopeKey === candidate.scopeKey);
359
- if (conflictingScope) throw new Error(`PLAN_SCOPE_ALREADY_OWNED: ${candidate.scopeKey}:${conflictingScope.planId}`);
945
+ && existing.scopeKey === candidateWithLineage.scopeKey
946
+ && !explicitPredecessors.has(existing.planId));
947
+ 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
+ : [];
360
954
  const repaired: PlanContract = {
361
- ...candidate,
955
+ ...candidateWithLineage,
956
+ ...(deliveryCarries.length ? { deliveryCarries } : {}),
362
957
  createdAt: current.createdAt,
363
958
  updatedAt: at,
364
959
  };
@@ -403,16 +998,16 @@ export function createPlanContract(options: PlanContractStoreOptions, input: Cre
403
998
 
404
999
  function admitPlanContractUnlocked(options: PlanContractStoreOptions, input: AdmitPlanContractInput): AdmitPlanContractResult {
405
1000
  assertRequirementReference(options, input.requirementId);
406
- const activePlans = listPlanContracts({ ...options, status: 'active', limit: 100 });
407
- const resolution = resolvePlanAdmission(activePlans, {
1001
+ const plans = listPlanContracts({ ...options, status: 'all', limit: 100 });
1002
+ const resolution = resolvePlanAdmission(plans, {
408
1003
  requirementId: input.requirementId,
409
1004
  scopeKey: input.scopeKey,
410
1005
  planRelation: input.planRelation,
411
1006
  relatedPlanId: input.relatedPlanId,
412
1007
  });
413
1008
  if (resolution.admissionDecision === 'extend_existing' && resolution.plan) {
414
- const successor = replacePlanContractUnlocked(options, resolution.plan.planId, input);
415
- return { ...resolution, admissionDecision: 'create_new', plan: successor };
1009
+ const revised = replacePlanContractUnlocked(options, resolution.plan.planId, input);
1010
+ return { ...resolution, admissionDecision: 'reuse_existing', plan: revised };
416
1011
  }
417
1012
  if (resolution.admissionDecision !== 'create_new') return resolution;
418
1013
  return { ...resolution, plan: createPlanContractUnlocked(options, input) };
@@ -430,11 +1025,15 @@ export function getPlanContract(options: PlanContractStoreOptions, planId: strin
430
1025
  return readPlanContractStore(options).contracts.find((contract) => contract.planId === sanitizeFileComponent(planId));
431
1026
  }
432
1027
 
1028
+ export function isCurrentPlanContract(contract: PlanContract): boolean {
1029
+ return !isTerminalPlanContractStatus(contract.status) && !contract.supersededBy?.trim();
1030
+ }
1031
+
433
1032
  export function listPlanContracts(options: PlanContractStoreOptions & { status?: PlanContractStatus | 'active' | 'all'; limit?: number }): PlanContract[] {
434
1033
  const status = options.status ?? 'active';
435
1034
  const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 50), 100));
436
1035
  return readPlanContractStore(options).contracts
437
- .filter((contract) => status === 'all' || (status === 'active' ? !isTerminalPlanContractStatus(contract.status) : contract.status === status))
1036
+ .filter((contract) => status === 'all' || (status === 'active' ? isCurrentPlanContract(contract) : contract.status === status))
438
1037
  .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
439
1038
  .slice(0, limit);
440
1039
  }
@@ -442,6 +1041,7 @@ export function listPlanContracts(options: PlanContractStoreOptions & { status?:
442
1041
  export function summarizePlanContract(plan: PlanContract): PlanContractSummary {
443
1042
  return {
444
1043
  planId: plan.planId,
1044
+ revision: currentPlanRevision(plan),
445
1045
  repoId: plan.repoId,
446
1046
  requirementId: plan.requirementId,
447
1047
  scopeKey: plan.scopeKey,
@@ -463,23 +1063,144 @@ function approvalErrors(plan: PlanContract, allPlans: readonly PlanContract[]):
463
1063
  'verifying',
464
1064
  'ready_to_finalize',
465
1065
  ].includes(status);
466
- if (allPlans.some((existing) => existing.planId !== plan.planId && existing.scopeKey === plan.scopeKey && scopeIsCommitted(existing.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'))) {
467
1070
  errors.push(`active plan already owns scope_key ${plan.scopeKey}`);
468
1071
  }
1072
+ errors.push(...obligationContinuityErrors(plan, allPlans));
469
1073
  return [...new Set(errors)];
470
1074
  }
471
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
+
472
1167
  function approvePlanContractUnlocked(options: PlanContractStoreOptions, planId: string): PlanContract {
473
1168
  const store = readPlanContractStore(options);
474
1169
  const index = store.contracts.findIndex((contract) => contract.planId === sanitizeFileComponent(planId));
475
1170
  if (index < 0) throw new Error(`plan contract not found: ${sanitizeFileComponent(planId)}`);
476
1171
  const current = store.contracts[index];
477
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
+ }
478
1185
  if (current.status !== 'draft' && current.status !== 'reviewing') throw new Error(`plan contract ${current.planId} cannot be approved from ${current.status}`);
479
1186
  const errors = approvalErrors(current, store.contracts);
480
1187
  if (errors.length > 0) throw new Error(`plan contract ${current.planId} cannot be approved: ${errors.join('; ')}`);
481
1188
  const at = nowIso(options);
482
- const next = { ...current, status: 'approved' as const, updatedAt: at };
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
+ }
483
1204
  const contracts = [...store.contracts];
484
1205
  contracts[index] = next;
485
1206
  writePlanContractStore(options, { schemaVersion: 1, updatedAt: at, contracts });
@@ -494,7 +1215,7 @@ export async function approvePlanContractAsync(options: PlanContractStoreOptions
494
1215
  return await withPlanAdmissionLockAsync(options, () => approvePlanContractUnlocked(options, planId));
495
1216
  }
496
1217
 
497
- export function supersedePlanContract(options: PlanContractStoreOptions, planId: string, supersededBy: string): PlanContract {
1218
+ export function supersedePlanContract(options: PlanContractStoreOptions, planId: string, supersededBy: string, reason = 'explicit_supersession'): PlanContract {
498
1219
  return withPlanAdmissionLock(options, () => {
499
1220
  const store = readPlanContractStore(options);
500
1221
  const index = store.contracts.findIndex((contract) => contract.planId === sanitizeFileComponent(planId));
@@ -506,15 +1227,202 @@ export function supersedePlanContract(options: PlanContractStoreOptions, planId:
506
1227
  if (replacement === current.planId) throw new Error('PLAN_SUCCESSOR_ID_MUST_CHANGE');
507
1228
  const successor = store.contracts.find((contract) => contract.planId === replacement);
508
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('; ')}`);
509
1233
  const at = nowIso(options);
510
- const next = { ...current, status: 'superseded' as const, supersededBy: successor.planId, updatedAt: at };
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
+ };
511
1246
  const contracts = [...store.contracts];
512
1247
  contracts[index] = next;
513
- writePlanContractStore(options, { schemaVersion: 1, updatedAt: at, contracts });
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);
514
1251
  return next;
515
1252
  });
516
1253
  }
517
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
+
518
1426
  /**
519
1427
  * Atomically reserve one eligible step for one WorkContract. This is the
520
1428
  * Plan-to-Work concurrency boundary: competing callers cannot bind the same
@@ -585,6 +1493,63 @@ export function repairDanglingPlanStepWorkBinding(
585
1493
  });
586
1494
  }
587
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
+
588
1553
  export function completePlanStepForWork(
589
1554
  options: PlanContractStoreOptions,
590
1555
  input: {
@@ -620,7 +1585,7 @@ export function completePlanStepForWork(
620
1585
  workId: delivered ? step.workId : undefined,
621
1586
  evidenceRefs: input.work.evidenceRefs.length > 0 ? input.work.evidenceRefs.slice(0, 20) : step.evidenceRefs,
622
1587
  };
623
- return { ...current, status: delivered ? 'verifying' : 'replanning', steps, updatedAt: at };
1588
+ return { ...current, status: current.pendingRevision ? 'replanning' : delivered ? 'verifying' : 'replanning', steps, updatedAt: at };
624
1589
  });
625
1590
  }
626
1591
 
@@ -644,12 +1609,13 @@ export function acceptPlanStepEvidence(
644
1609
  evidenceRefs: [{ title: 'semantic acceptance', summary: `${reviewer}: ${rationale}`, detailLevel: 'summary' as const }, ...step.evidenceRefs].slice(0, 20),
645
1610
  };
646
1611
  const allCompleted = steps.every((candidate) => candidate.status === 'completed');
1612
+ const projectedSteps = allCompleted ? steps : projectDependencyReadySteps(steps);
647
1613
  const acceptedSourceRevision = input.acceptedSourceRevision?.trim();
648
1614
  return {
649
1615
  ...current,
650
1616
  ...(acceptedSourceRevision ? { sourceRevision: acceptedSourceRevision } : {}),
651
- status: allCompleted ? 'finalized' : 'executing',
652
- steps,
1617
+ status: current.pendingRevision ? 'replanning' : allCompleted ? 'finalized' : 'executing',
1618
+ steps: projectedSteps,
653
1619
  updatedAt: nowIso(options),
654
1620
  };
655
1621
  });