@moretea-labs/forge 1.8.1 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (328) hide show
  1. package/AGENTS.md +5 -2
  2. package/CHANGELOG.md +20 -0
  3. package/README.en.md +24 -3
  4. package/README.md +10 -6
  5. package/README.zh-CN.md +5 -3
  6. package/THIRD_PARTY_NOTICES.md +1 -2
  7. package/adapters/chatgpt/browser-delivery-runtime.ts +5 -3
  8. package/adapters/chatgpt/provider-delivery.ts +1 -1
  9. package/adapters/chatgpt/work-conversation-binding-store.ts +20 -1
  10. package/adapters/computer/chatgpt-extension-target.ts +295 -50
  11. package/adapters/computer/chrome-extension/background.js +227 -71
  12. package/adapters/computer/chrome-extension/content.js +181 -157
  13. package/adapters/computer/chrome-extension/manifest.json +3 -1
  14. package/adapters/computer/desktop-operator-provider.ts +18 -6
  15. package/adapters/computer/interaction-target-authority.ts +39 -9
  16. package/adapters/mcp/access-mode.ts +18 -39
  17. package/adapters/mcp/auth.ts +2 -6
  18. package/adapters/mcp/instructions.ts +2 -2
  19. package/adapters/mcp/multi-repository.ts +7 -28
  20. package/adapters/mcp/runtime-gateway/context-adapter.ts +79 -40
  21. package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +7 -15
  22. package/adapters/mcp/runtime-gateway/execution-tools.ts +7 -13
  23. package/adapters/mcp/runtime-gateway/plugin-adapter.ts +16 -3
  24. package/adapters/mcp/runtime-gateway/process-tools.ts +3 -21
  25. package/adapters/mcp/runtime-gateway/router.ts +3 -5
  26. package/adapters/mcp/runtime-gateway/routing-policy.ts +17 -10
  27. package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1 -1
  28. package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +14 -9
  29. package/adapters/mcp/runtime-gateway/runtime-tools.ts +1 -5
  30. package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +36 -1
  31. package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +58 -4
  32. package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +7 -3
  33. package/adapters/mcp/runtime-gateway/work-adapter.ts +16 -16
  34. package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +0 -17
  35. package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +69 -12
  36. package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +26 -2
  37. package/adapters/mcp/server.ts +24 -3
  38. package/adapters/mcp/setup.ts +4 -10
  39. package/adapters/mcp/tool-mapping/access-tools.ts +0 -6
  40. package/adapters/mcp/tool-mapping/legacy-tool-service.ts +672 -2935
  41. package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +1 -1
  42. package/adapters/mcp/tool-mapping/repository-tools.ts +52 -86
  43. package/adapters/mcp/toolset-names.ts +11 -42
  44. package/adapters/mcp/toolset.ts +16 -90
  45. package/{src/cli → adapters}/mcp/transports/AGENTS.md +5 -5
  46. package/{src/cli → adapters}/mcp/transports/CLAUDE.md +5 -5
  47. package/adapters/mcp/transports/http-observation.ts +18 -6
  48. package/adapters/mcp/transports/http-post-lifecycle.ts +19 -0
  49. package/adapters/mcp/transports/http.ts +54 -14
  50. package/adapters/mcp/types.ts +0 -6
  51. package/assets/hooks/lib/session-state.sh +0 -38
  52. package/assets/hooks/post-bash.sh +1 -1
  53. package/assets/hooks/post-tool-observer.sh +0 -5
  54. package/assets/hooks/prompt-guard.sh +0 -55
  55. package/assets/partials/04-project-structure.partial.md +1 -1
  56. package/assets/partials/07-footer.partial.md +1 -1
  57. package/assets/partials-agents/02-operating-mode.partial.md +1 -1
  58. package/assets/partials-agents/08-deep-docs.partial.md +1 -1
  59. package/assets/reference-configs/agentic-development-flow.md +1 -1
  60. package/assets/reference-configs/external-tooling.md +0 -76
  61. package/assets/reference-configs/release-deploy.md +1 -1
  62. package/assets/templates/helpers/architecture-queue.sh +1 -1
  63. package/assets/templates/helpers/check-agent-tooling.sh +1 -327
  64. package/assets/templates/helpers/check-task-workflow.sh +1 -1
  65. package/assets/templates/helpers/ensure-task-workflow.sh +0 -14
  66. package/assets/templates/helpers/migrate-project-template.sh +2 -2
  67. package/assets/workflow-contract.v1.json +0 -2
  68. package/docs/README.md +5 -2
  69. package/docs/architecture/CURRENT.md +114 -30
  70. package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +42 -0
  71. package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +15 -2
  72. package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +4 -2
  73. package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +109 -4
  74. package/docs/architecture/decisions/20261009-exact-code-context-discovery.md +19 -0
  75. package/docs/architecture/decisions/20261009-generated-xcode-cache-retention.md +37 -0
  76. package/docs/architecture/decisions/20261011-autonomous-continuation-convergence.md +597 -0
  77. package/docs/architecture/decisions/20261017-edit-session-finalization-reconciliation.md +17 -0
  78. package/docs/architecture/decisions/20261019-retire-host-skills.md +16 -0
  79. package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +1 -1
  80. package/docs/architecture/domains/public-surface.md +3 -5
  81. package/docs/architecture/domains/verification.md +5 -5
  82. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +2 -2
  83. package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +4 -4
  84. package/docs/architecture/reviews/20261015-code-search-runtime-acceptance.md +75 -0
  85. package/docs/forge-plugin-management.md +14 -2
  86. package/docs/operations/controller-browser-plugin.md +23 -0
  87. package/docs/operations/features.md +6 -7
  88. package/docs/operations/features.zh-CN.md +1 -1
  89. package/docs/operations/github-repository.md +4 -4
  90. package/docs/operations/github-semantic-release-projection.md +20 -0
  91. package/docs/operations/homebrew.md +2 -2
  92. package/docs/operations/mcp-tool-exposure.md +13 -54
  93. package/docs/operations/runtime-performance-diagnostics.md +8 -0
  94. package/docs/operations/standalone-disaster-recovery.md +1 -1
  95. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  96. package/docs/tutorials/03-first-repository-task.md +2 -0
  97. package/docs/wiki/Computer-and-Plugins.md +78 -0
  98. package/docs/wiki/Computer-and-Plugins.zh-CN.md +88 -0
  99. package/docs/wiki/Home.md +16 -2
  100. package/docs/wiki/Implementation.md +24 -12
  101. package/docs/wiki/Installation.md +28 -32
  102. package/docs/wiki/Integrations.md +4 -0
  103. package/docs/wiki/Quick-Start.md +35 -22
  104. package/docs/wiki/Releases-and-Upgrades.md +15 -25
  105. package/docs/wiki/_Sidebar.md +5 -1
  106. package/package.json +1 -12
  107. package/packages/kernel/controller/infrastructure/controller-session-store.ts +63 -38
  108. package/packages/kernel/identity/infrastructure/activity-projection-store.ts +10 -0
  109. package/packages/kernel/scheduler/application/eligibility.ts +4 -1
  110. package/packages/kernel/scheduler/infrastructure/schedule-store.ts +13 -10
  111. package/packages/kernel/work/application/work-service.ts +1 -0
  112. package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
  113. package/packages/kernel/work/domain/state-machine.ts +9 -2
  114. package/packages/kernel/work/infrastructure/work-contract-store.ts +28 -1
  115. package/packages/plugin-runtime/browser/provider-registry.ts +12 -36
  116. package/packages/plugin-runtime/computer/conversation-target.ts +17 -7
  117. package/scripts/assemble-template.ts +11 -72
  118. package/scripts/check-mcp-compatibility.ts +55 -74
  119. package/scripts/check-package-identity.mjs +0 -5
  120. package/scripts/check-runtime-architecture.mjs +305 -28
  121. package/scripts/init-project.sh +0 -46
  122. package/scripts/install-ios-hid-toolchain.sh +60 -0
  123. package/scripts/install-standalone-recovery.ts +1 -1
  124. package/scripts/lib/project-init-lib.sh +3 -3
  125. package/scripts/migrate-windows-wsl-controller-home.ts +2 -0
  126. package/scripts/open-source-audit-allowlist.txt +0 -1
  127. package/scripts/run-governed-gate.ts +2 -0
  128. package/scripts/smoke-ios-physical-device.ts +38 -10
  129. package/scripts/smoke-mcp-http-runtime.ts +7 -8
  130. package/scripts/smoke-mcp-tool-surface.ts +27 -41
  131. package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +6 -3
  132. package/scripts/sync-generated-authority.mjs +0 -1
  133. package/src/cli/agent-jobs/executor-health.ts +1 -1
  134. package/src/cli/agent-jobs/integration.ts +2 -2
  135. package/src/cli/chatgpt-browser/file-policy.ts +2 -2
  136. package/src/cli/commands/doctor.ts +1 -143
  137. package/src/cli/commands/global-runtime.ts +3 -196
  138. package/src/cli/commands/init-hook.ts +6 -23
  139. package/src/cli/commands/init.ts +3 -252
  140. package/src/cli/commands/mcp.ts +0 -5
  141. package/src/cli/commands/prompt-guard-decision.ts +0 -2
  142. package/src/cli/commands/recovery.ts +2 -14
  143. package/src/cli/commands/repository.ts +17 -1
  144. package/src/cli/commands/setup-execution.ts +41 -0
  145. package/src/cli/controller/completion-orchestrator.ts +1 -1
  146. package/src/cli/controller/composite-operations.ts +1 -1
  147. package/src/cli/controller/context/instruction-context.ts +1 -1
  148. package/src/cli/controller/context/known-paths.ts +2 -2
  149. package/src/cli/controller/context/query-planning.ts +11 -4
  150. package/src/cli/controller/context/rule-resolution.ts +2 -2
  151. package/src/cli/controller/context/source-materializer.ts +2 -2
  152. package/src/cli/controller/context/structural-provider.ts +84 -0
  153. package/src/cli/controller/context/structural.ts +6 -6
  154. package/src/cli/controller/context/types.ts +8 -9
  155. package/src/cli/controller/context-pack.ts +97 -76
  156. package/src/cli/controller/operational-plan.ts +1 -1
  157. package/src/cli/controller/systemd-user.ts +4 -10
  158. package/src/cli/editing/edit-session.ts +17 -5
  159. package/src/cli/github/github.ts +4 -4
  160. package/src/cli/github/semantic-projection.ts +174 -0
  161. package/src/cli/hook/prompt-intents.ts +0 -9
  162. package/src/cli/index.ts +1 -43
  163. package/src/cli/local-bridge/connector-freshness.ts +1 -12
  164. package/src/cli/local-bridge/console-automations.ts +1 -1
  165. package/src/cli/local-bridge/console-view-models.ts +1 -1
  166. package/src/cli/local-bridge/facade-api.ts +0 -2
  167. package/src/cli/repositories/command-classifier.ts +0 -9
  168. package/src/cli/repositories/command-scope.ts +2 -2
  169. package/src/cli/repositories/locks.ts +20 -0
  170. package/src/cli/repositories/registry.ts +2 -1
  171. package/src/cli/repositories/safe-patch.ts +2 -12
  172. package/src/cli/repositories/structured-git.ts +39 -3
  173. package/src/cli/repository/inspector.ts +185 -343
  174. package/src/effects/process-runner.ts +17 -1
  175. package/src/runtime/context/generic-lsp-provider.ts +1 -1
  176. package/src/runtime/context/semantic-navigation-contract.ts +7 -0
  177. package/src/runtime/context/semantic-navigation.ts +1 -1
  178. package/src/runtime/control-plane/cleanup-artifact-retention.ts +4 -4
  179. package/src/runtime/control-plane/execution/work-delivery-service.ts +150 -31
  180. package/src/runtime/control-plane/execution/work-handle-authority.ts +12 -4
  181. package/src/runtime/control-plane/execution/work-handle-store.ts +53 -1
  182. package/src/runtime/control-plane/execution/work-operation-service.ts +20 -10
  183. package/src/runtime/control-plane/execution/work-preparation-service.ts +108 -4
  184. package/src/runtime/control-plane/execution/work-process-evidence.ts +2 -2
  185. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +28 -13
  186. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +5 -1
  187. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +1 -1
  188. package/src/runtime/control-plane/facade/capability-registry.ts +2 -0
  189. package/src/runtime/control-plane/facade/work-continuation.ts +1 -1
  190. package/src/runtime/control-plane/generated-cache-retention.ts +114 -63
  191. package/src/runtime/control-plane/global-scheduler/maintenance.ts +20 -8
  192. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +1 -11
  193. package/src/runtime/control-plane/governance/external-effects.ts +1 -1
  194. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +1 -0
  195. package/src/runtime/control-plane/runtime-cleanup.ts +12 -93
  196. package/src/runtime/evidence/sensitive-output.ts +22 -2
  197. package/src/runtime/execution/jobs/child-reference.ts +11 -0
  198. package/src/runtime/execution/jobs/receipt-recovery.ts +1 -11
  199. package/src/runtime/execution/process-runtime/command-facade.ts +3 -3
  200. package/src/runtime/execution/process-runtime/runtime.ts +20 -16
  201. package/src/runtime/execution/thin-harness/execution-router.ts +16 -16
  202. package/src/runtime/execution/thin-harness/fast-executor.ts +2 -2
  203. package/src/runtime/execution/thin-harness/search-worker.ts +2 -2
  204. package/src/runtime/execution/workers/executor.ts +1 -2
  205. package/src/runtime/maintenance/cleanup.ts +41 -5
  206. package/src/runtime/plugins/action-application.ts +12 -1
  207. package/src/runtime/plugins/app-store-connect-adapter.ts +252 -4
  208. package/src/runtime/plugins/browser-adapter.ts +381 -313
  209. package/src/runtime/plugins/browser-macos-bridge.ts +114 -112
  210. package/src/runtime/plugins/browser-manifest-surface.ts +2 -1
  211. package/src/runtime/plugins/browser-registration.ts +2 -0
  212. package/src/runtime/plugins/browser-runtime.ts +26 -35
  213. package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +11 -2
  214. package/src/runtime/plugins/computer-registration.ts +76 -9
  215. package/src/runtime/plugins/github-adapter.ts +30 -3
  216. package/src/runtime/plugins/gmail-adapter.ts +166 -1
  217. package/src/runtime/plugins/google-shared.ts +10 -5
  218. package/src/runtime/plugins/ios/remote-xpc-hid.ts +70 -30
  219. package/src/runtime/plugins/ios-adapter.ts +45 -40
  220. package/src/runtime/plugins/ios-agent-device.ts +54 -4
  221. package/src/runtime/plugins/ios-device-ownership.ts +103 -0
  222. package/src/runtime/plugins/ios-physical-device.ts +497 -79
  223. package/src/runtime/plugins/local-system-adapter.ts +1 -99
  224. package/src/runtime/plugins/store.ts +80 -7
  225. package/src/runtime/plugins/types.ts +4 -0
  226. package/src/runtime/projections/git-status-sampler.ts +2 -19
  227. package/src/runtime/recovery/actions.ts +1 -1
  228. package/src/runtime/recovery/lifecycle-inventory.ts +1 -11
  229. package/src/runtime/recovery/probe.ts +2 -2
  230. package/src/runtime/release/release-coordinator.ts +20 -4
  231. package/src/runtime/root/controller-progression-composition.ts +67 -5
  232. package/src/runtime/root/package-connector-service.ts +123 -11
  233. package/src/runtime/root/package-runtime-release.ts +6 -18
  234. package/src/runtime/root/package-runtime-service.ts +7 -13
  235. package/src/runtime/root/release-execution-canary.ts +18 -5
  236. package/src/runtime/root/release-manifest.ts +0 -34
  237. package/src/runtime/root/release-materialize.ts +0 -78
  238. package/src/runtime/root/runtime.ts +1 -2
  239. package/src/runtime/root/scheduled-controller-composition.ts +2 -1
  240. package/src/runtime/root/service.ts +90 -11
  241. package/src/runtime/root/types.ts +0 -6
  242. package/src/runtime/root/workflow-supervisor-composition.ts +207 -19
  243. package/src/runtime/root/workflow-supervisor-runtime.ts +365 -10
  244. package/src/runtime/safe-tooling/google-credential-store.ts +1 -11
  245. package/src/runtime/safe-tooling/ios-development.ts +22 -8
  246. package/src/runtime/safe-tooling/job-summary.ts +1 -1
  247. package/src/runtime/safe-tooling/workspace-auth.ts +4 -1
  248. package/src/runtime/shared/atomic-text-file.ts +21 -0
  249. package/src/runtime/shared/json-files.ts +8 -3
  250. package/src/runtime/standalone-recovery/core.ts +189 -174
  251. package/src/runtime/standalone-recovery/entry.ts +62 -30
  252. package/src/runtime/standalone-recovery/operation-lock.ts +11 -1
  253. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +1 -0
  254. package/src/runtime/workflow/schedules/engine.ts +36 -14
  255. package/src/runtime/workflow/schedules/pending-activation.ts +11 -4
  256. package/src/runtime/workflow/schedules/work-continuation.ts +51 -6
  257. package/supervisor/client.ts +31 -14
  258. package/supervisor/control-plane.ts +210 -115
  259. package/supervisor/forge-validators.ts +88 -21
  260. package/supervisor/native-browser-adapter.ts +393 -28
  261. package/supervisor/native-messaging/host.ts +21 -2
  262. package/supervisor/protocol.ts +74 -26
  263. package/supervisor/server.ts +16 -8
  264. package/supervisor/store.ts +476 -57
  265. package/supervisor/types.ts +19 -13
  266. package/SKILL.md +0 -427
  267. package/adapters/computer/chatgpt-target-router.ts +0 -103
  268. package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +0 -87
  269. package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +0 -1
  270. package/adapters/mcp/toolset-selection.ts +0 -41
  271. package/agents/openai.yaml +0 -4
  272. package/assets/skill-hooks.json +0 -41
  273. package/assets/skill-version.json +0 -189
  274. package/assets/skills/claude-review/SKILL.md +0 -231
  275. package/assets/skills/codex-review/SKILL.md +0 -103
  276. package/assets/templates/helpers/check-skill-version.ts +0 -193
  277. package/scripts/check-skill-version.ts +0 -5
  278. package/scripts/ensure-codegraph.sh +0 -16
  279. package/scripts/run-skill-hook.ts +0 -244
  280. package/scripts/sync-codex-installed-copies.sh +0 -182
  281. package/src/cli/commands/tools.ts +0 -167
  282. package/src/cli/mcp/access-mode.ts +0 -2
  283. package/src/cli/mcp/access-tools.ts +0 -2
  284. package/src/cli/mcp/audit.ts +0 -2
  285. package/src/cli/mcp/auth.ts +0 -2
  286. package/src/cli/mcp/instructions.ts +0 -2
  287. package/src/cli/mcp/legacy-context.ts +0 -2
  288. package/src/cli/mcp/legacy-tool-service.ts +0 -2
  289. package/src/cli/mcp/multi-repository.ts +0 -2
  290. package/src/cli/mcp/oauth.ts +0 -2
  291. package/src/cli/mcp/openai-secure-tunnel.ts +0 -2
  292. package/src/cli/mcp/paths.ts +0 -2
  293. package/src/cli/mcp/policy.ts +0 -2
  294. package/src/cli/mcp/post-finalize-work-attribution.ts +0 -2
  295. package/src/cli/mcp/proxy-env.ts +0 -2
  296. package/src/cli/mcp/readiness-projection.ts +0 -2
  297. package/src/cli/mcp/redaction.ts +0 -2
  298. package/src/cli/mcp/repo.ts +0 -2
  299. package/src/cli/mcp/repository-tools.ts +0 -2
  300. package/src/cli/mcp/server.ts +0 -2
  301. package/src/cli/mcp/setup.ts +0 -2
  302. package/src/cli/mcp/tool-capability-cache.ts +0 -2
  303. package/src/cli/mcp/tool-contract.ts +0 -2
  304. package/src/cli/mcp/tools.ts +0 -2
  305. package/src/cli/mcp/toolset-names.ts +0 -2
  306. package/src/cli/mcp/toolset-selection.ts +0 -2
  307. package/src/cli/mcp/toolset.ts +0 -2
  308. package/src/cli/mcp/transports/http.ts +0 -2
  309. package/src/cli/mcp/transports/session-registry.ts +0 -2
  310. package/src/cli/mcp/transports/stdio.ts +0 -2
  311. package/src/cli/mcp/types.ts +0 -2
  312. package/src/cli/mcp/umbrella-tools.ts +0 -2
  313. package/src/cli/tools/codegraph.ts +0 -695
  314. package/src/core/adoption/helper-wrapper-plan.ts +0 -159
  315. package/src/runtime/context/codegraph-cache-boundary.ts +0 -255
  316. package/src/runtime/context/codegraph-read-provider.ts +0 -611
  317. package/src/runtime/context/codegraph-sidecar.cjs +0 -248
  318. package/src/runtime/control-plane/codegraph-cache-retention.ts +0 -164
  319. package/src/runtime/gateway/mcp/execution-tools.ts +0 -2
  320. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +0 -2
  321. package/src/runtime/gateway/mcp/persisted-check-process.ts +0 -2
  322. package/src/runtime/gateway/mcp/process-tools.ts +0 -2
  323. package/src/runtime/gateway/mcp/router.ts +0 -2
  324. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +0 -2
  325. package/src/runtime/gateway/mcp/runtime-tools.ts +0 -2
  326. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +0 -2
  327. package/src/runtime/plugins/browser-provider-registry.ts +0 -2
  328. package/src/runtime/plugins/computer-chatgpt-macos-target.ts +0 -891
@@ -746,45 +746,70 @@ export function bindControllerSessionToCurrentRuntime(
746
746
  },
747
747
  ): ControllerSession {
748
748
  assertIdentity(input);
749
- const current = getControllerSession(options, input.workId);
750
- if (!current) {
751
- if (input.allowClaimIfMissing !== true) throw new Error(`WORK_CONTROLLER_OWNER_REQUIRED: ${input.workId}`);
752
- return claimControllerSession(options, { ...input, expectedClaimGeneration: 0 });
753
- }
754
- const currentPrincipal = controllerSessionPrincipalId(current);
755
- if (current.controllerId !== input.controllerId) {
756
- throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${input.workId} is owned by ${current.controllerId}`);
757
- }
758
- if (current.controllerType !== input.controllerType) {
759
- throw new Error(`WORK_CONTROLLER_TYPE_MISMATCH: ${input.workId} is owned by ${current.controllerType}`);
760
- }
761
- if (currentPrincipal !== input.principalId.trim()) {
762
- throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${input.workId}`);
763
- }
764
- const ownerInstanceId = current.controllerInstanceId?.trim() || '';
765
749
  const requestedInstanceId = input.controllerInstanceId.trim();
766
- if (ownerInstanceId !== requestedInstanceId) {
767
- const currentRuntimeInstanceId = input.currentRuntimeInstanceId?.trim() || '';
768
- if (!currentRuntimeInstanceId || currentRuntimeInstanceId !== requestedInstanceId) {
769
- throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${input.workId}`);
770
- }
771
- }
772
- const requestedAuthorityDigest = input.authorityDigest?.trim() || '';
773
- const currentAuthorityDigest = current.authorityDigest?.trim() || '';
774
- if (
775
- ownerInstanceId === requestedInstanceId
776
- && current.sessionId === input.sessionId
777
- && (!requestedAuthorityDigest || requestedAuthorityDigest === currentAuthorityDigest)
778
- ) return current;
779
- // The opaque Work capability belongs to durable ownership, not to one
780
- // transport or Runtime process. A positively-current Runtime migration keeps
781
- // the same digest while claim generation fences the new controller epoch.
782
- const preservedAuthorityDigest = input.authorityDigest?.trim() || current.authorityDigest?.trim() || undefined;
783
- return resumeControllerSession(options, {
784
- ...input,
785
- ...(preservedAuthorityDigest ? { authorityDigest: preservedAuthorityDigest } : {}),
786
- expectedClaimGeneration: current.claimGeneration,
787
- });
750
+ const currentRuntimeInstanceId = input.currentRuntimeInstanceId?.trim() || '';
751
+ return withControllerSessionMutationLock(
752
+ options,
753
+ input.workId,
754
+ `controller-bind-current-runtime:${input.controllerId}:${input.sessionId}`,
755
+ () => {
756
+ assertControllerWorkClaimable(options, input.workId);
757
+ const store = read(options);
758
+ const current = activeSession(store, input.workId, optionsNowMs(options));
759
+ const retained = current ? undefined : getRetainedControllerSession(options, input.workId);
760
+ const retainedMatchesIdentity = Boolean(
761
+ retained
762
+ && retained.controllerId === input.controllerId
763
+ && retained.controllerType === input.controllerType
764
+ && controllerSessionPrincipalId(retained) === input.principalId.trim(),
765
+ );
766
+ const previous = current ?? (input.allowClaimIfMissing === true && retainedMatchesIdentity ? retained : undefined);
767
+
768
+ if (!current && !previous && input.allowClaimIfMissing !== true) {
769
+ throw new Error(`WORK_CONTROLLER_OWNER_REQUIRED: ${input.workId}`);
770
+ }
771
+ if (!current && retainedMatchesIdentity) {
772
+ if (!currentRuntimeInstanceId || currentRuntimeInstanceId !== requestedInstanceId) {
773
+ throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${input.workId}`);
774
+ }
775
+ }
776
+
777
+ if (current) {
778
+ const currentPrincipal = controllerSessionPrincipalId(current);
779
+ if (current.controllerId !== input.controllerId) {
780
+ throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${input.workId} is owned by ${current.controllerId}`);
781
+ }
782
+ if (current.controllerType !== input.controllerType) {
783
+ throw new Error(`WORK_CONTROLLER_TYPE_MISMATCH: ${input.workId} is owned by ${current.controllerType}`);
784
+ }
785
+ if (currentPrincipal !== input.principalId.trim()) {
786
+ throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${input.workId}`);
787
+ }
788
+ const ownerInstanceId = current.controllerInstanceId?.trim() || '';
789
+ if (ownerInstanceId !== requestedInstanceId) {
790
+ if (!currentRuntimeInstanceId || currentRuntimeInstanceId !== requestedInstanceId) {
791
+ throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${input.workId}`);
792
+ }
793
+ }
794
+ const requestedAuthorityDigest = input.authorityDigest?.trim() || '';
795
+ const currentAuthorityDigest = current.authorityDigest?.trim() || '';
796
+ if (
797
+ ownerInstanceId === requestedInstanceId
798
+ && current.sessionId === input.sessionId
799
+ && (!requestedAuthorityDigest || requestedAuthorityDigest === currentAuthorityDigest)
800
+ ) return current;
801
+ }
802
+
803
+ // The opaque Work capability belongs to durable ownership, not to one
804
+ // transport or Runtime process. A positively-current Runtime migration keeps
805
+ // the same digest while claim generation fences the new controller epoch.
806
+ const preservedAuthorityDigest = input.authorityDigest?.trim() || previous?.authorityDigest?.trim() || undefined;
807
+ return persistClaim(options, store, {
808
+ ...input,
809
+ ...(preservedAuthorityDigest ? { authorityDigest: preservedAuthorityDigest } : {}),
810
+ }, previous);
811
+ },
812
+ );
788
813
  }
789
814
 
790
815
  export function releaseControllerSession(options: ControllerSessionStoreOptions, workId: string, controllerId: string): void {
@@ -36,6 +36,16 @@ export function recordDirectActivity(
36
36
  return item;
37
37
  }
38
38
 
39
+ /** Read one existing activity by its stable identity, independent of list-window truncation. */
40
+ export function getDirectActivity(controllerHome: string, activityId: string): CapabilityActivityItem | undefined {
41
+ try {
42
+ const item = JSON.parse(readFileSync(activityFilePath(controllerHome, activityId), 'utf8')) as CapabilityActivityItem;
43
+ return item.activityId === activityId ? item : undefined;
44
+ } catch {
45
+ return undefined;
46
+ }
47
+ }
48
+
39
49
  export function listDirectActivities(controllerHome: string, limit = 50): CapabilityActivityItem[] {
40
50
  const dir = activityDir(controllerHome);
41
51
  const files = readdirSync(dir).filter((f) => f.endsWith('.json'));
@@ -122,7 +122,10 @@ function cronFieldMatches(value: number, field: string, min: number, max: number
122
122
  const trimmed = part.trim();
123
123
  if (trimmed === '*') return true;
124
124
  const stepMatch = trimmed.match(/^\*\/(\d+)$/);
125
- if (stepMatch) return value % Math.max(1, Number(stepMatch[1])) === 0;
125
+ if (stepMatch) {
126
+ const step = Math.max(1, Number(stepMatch[1]));
127
+ return value >= min && value <= max && (value - min) % step === 0;
128
+ }
126
129
  const rangeMatch = trimmed.match(/^(\d+)-(\d+)(?:\/(\d+))?$/);
127
130
  if (rangeMatch) {
128
131
  const start = Math.max(min, Number(rangeMatch[1]));
@@ -101,9 +101,9 @@ function requestPath(controllerHome: string, targetKey: string, requestId: strin
101
101
  export const SCHEDULE_OCCURRENCE_RECENT_LIMIT = 5_000;
102
102
  export const SCHEDULE_CREATED_OCCURRENCE_STALE_MS = 5 * 60 * 1_000;
103
103
 
104
- const EXTERNAL_CONTROLLER_WAKE_MAX_FAILURES = 3;
105
- const EXTERNAL_CONTROLLER_WAKE_DAILY_BUDGET_MINUTES = 60;
106
- const EXTERNAL_CONTROLLER_WAKE_MIN_COOLDOWN_MINUTES = 10;
104
+ const WORK_CONTINUATION_MAX_FAILURES = 3;
105
+ const WORK_CONTINUATION_DAILY_BUDGET_MINUTES = 60;
106
+ const WORK_CONTINUATION_MIN_COOLDOWN_MINUTES = 10;
107
107
 
108
108
  function boundedSchedulePolicy(schedule: RepositorySchedule): RepositorySchedule['policy'] {
109
109
  const policy = {
@@ -111,17 +111,18 @@ function boundedSchedulePolicy(schedule: RepositorySchedule): RepositorySchedule
111
111
  backoffBaseMinutes: Math.max(1, schedule.policy.backoffBaseMinutes ?? schedule.policy.cooldownMinutes ?? 1),
112
112
  backoffMaxMinutes: Math.max(1, schedule.policy.backoffMaxMinutes ?? 24 * 60),
113
113
  };
114
- if (schedule.action.operation !== 'external_controller_wake') return policy;
115
- // A Work-bound controller wake is expensive and can affect the user's
114
+ if (schedule.action.operation !== 'external_controller_wake' && schedule.action.operation !== 'controller_progression') return policy;
115
+ // A Work-bound continuation is expensive and can affect the user's
116
116
  // provider. No schedule payload may convert a transient tool outage into an
117
117
  // all-day retry loop. Larger work needs an explicit future user action after
118
118
  // the bounded lane records useful failure evidence.
119
119
  return {
120
120
  ...policy,
121
- maxFailures: Math.max(1, Math.min(policy.maxFailures, EXTERNAL_CONTROLLER_WAKE_MAX_FAILURES)),
122
- dailyBudgetMinutes: Math.max(1, Math.min(policy.dailyBudgetMinutes, EXTERNAL_CONTROLLER_WAKE_DAILY_BUDGET_MINUTES)),
123
- cooldownMinutes: Math.max(policy.cooldownMinutes, EXTERNAL_CONTROLLER_WAKE_MIN_COOLDOWN_MINUTES),
124
- backoffBaseMinutes: Math.max(policy.backoffBaseMinutes, EXTERNAL_CONTROLLER_WAKE_MIN_COOLDOWN_MINUTES),
121
+ maxFailures: Math.max(1, Math.min(policy.maxFailures, WORK_CONTINUATION_MAX_FAILURES)),
122
+ dailyBudgetMinutes: Math.max(1, Math.min(policy.dailyBudgetMinutes, WORK_CONTINUATION_DAILY_BUDGET_MINUTES)),
123
+ cooldownMinutes: Math.max(policy.cooldownMinutes, WORK_CONTINUATION_MIN_COOLDOWN_MINUTES),
124
+ backoffBaseMinutes: Math.max(policy.backoffBaseMinutes, WORK_CONTINUATION_MIN_COOLDOWN_MINUTES),
125
+ backoffMaxMinutes: Math.max(policy.backoffMaxMinutes, WORK_CONTINUATION_MIN_COOLDOWN_MINUTES, policy.backoffBaseMinutes),
125
126
  };
126
127
  }
127
128
  function canonical(value: unknown): unknown {
@@ -175,7 +176,7 @@ export function createSchedule(controllerHome: string, input: CreateScheduleInpu
175
176
  return getSchedule(controllerHome, input.repoId, record.scheduleId);
176
177
  }
177
178
  const timestamp = new Date().toISOString();
178
- const schedule: RepositorySchedule = {
179
+ const candidate: RepositorySchedule = {
179
180
  ...input,
180
181
  requestId,
181
182
  schemaVersion: 1,
@@ -185,6 +186,7 @@ export function createSchedule(controllerHome: string, input: CreateScheduleInpu
185
186
  updatedAt: timestamp,
186
187
  consecutiveFailures: 0,
187
188
  };
189
+ const schedule: RepositorySchedule = { ...candidate, policy: boundedSchedulePolicy(candidate) };
188
190
  writeJsonAtomic(schedulePath(controllerHome, schedule.repoId, schedule.scheduleId), schedule);
189
191
  writeJsonAtomic(requestRecordPath, { schemaVersion: 1, requestId, semanticKey, scheduleId: schedule.scheduleId, repoId: schedule.repoId, createdAt: timestamp } satisfies ScheduleRequestRecord);
190
192
  appendRuntimeEvent(controllerHome, { repoId: schedule.repoId, entityType: 'schedule', entityId: schedule.scheduleId, eventType: 'schedule_created', requestId, revision: schedule.revision, data: { name: schedule.name, shadowMode: schedule.policy.shadowMode } });
@@ -228,6 +230,7 @@ function persistScheduleRevision(
228
230
  requestId: current.requestId,
229
231
  createdAt: current.createdAt,
230
232
  updatedAt: new Date().toISOString(),
233
+ policy: boundedSchedulePolicy(candidate),
231
234
  };
232
235
  writeJsonAtomic(schedulePath(controllerHome, next.repoId, next.scheduleId), next);
233
236
  appendRuntimeEvent(controllerHome, { repoId: next.repoId, entityType: 'schedule', entityId: next.scheduleId, eventType: 'schedule_updated', requestId: next.requestId, revision: next.revision, data: { enabled: next.enabled, pausedReason: next.pausedReason } });
@@ -90,6 +90,7 @@ export {
90
90
  reviseWorkSemanticContext,
91
91
  summarizeWorkContract,
92
92
  updateWorkContract,
93
+ updateWorkContractPlacementAtomically,
93
94
  promoteWorkToRepositoryChange,
94
95
  recordWorkEvidenceState,
95
96
  activateWorkContract,
@@ -55,7 +55,7 @@ export interface RepositoryCompletionReceipt {
55
55
  delivery: {
56
56
  kind: CompletionReceiptDeliveryKind;
57
57
  status: CompletionReceiptDeliveryStatus;
58
- strategy: 'edit_session_commit' | 'work_fast_forward' | 'already_integrated' | 'no_change' | 'remote';
58
+ strategy: 'edit_session_commit' | 'work_fast_forward' | 'work_merge_commit' | 'already_integrated' | 'no_change' | 'remote';
59
59
  reachable: boolean;
60
60
  recordedAt: string;
61
61
  };
@@ -135,8 +135,15 @@ export function validateWorkSemantics(contract: WorkContract): WorkContract {
135
135
  if (outcome === 'completed_changed' && contract.workKind !== 'repository_change') {
136
136
  throw new Error('WORK_SEMANTICS_INVALID: completed_changed requires repository_change WorkKind');
137
137
  }
138
- if (outcome === 'completed_no_change' && !['completed_no_change', 'read_only_review'].includes(contract.workKind)) {
139
- throw new Error('WORK_SEMANTICS_INVALID: completed_no_change requires completed_no_change or read_only_review WorkKind');
138
+ // An empty repository delivery is evidence, not authority to rewrite WorkKind.
139
+ const emptyRepositoryDelivery = contract.workKind === 'repository_change'
140
+ && isRepositoryCompletionReceipt(contract.completionReceipt)
141
+ && contract.completionReceipt.delivery.kind === 'no_change'
142
+ && contract.completionReceipt.changedPaths.length === 0;
143
+ if (outcome === 'completed_no_change'
144
+ && !['completed_no_change', 'read_only_review'].includes(contract.workKind)
145
+ && !emptyRepositoryDelivery) {
146
+ throw new Error('WORK_SEMANTICS_INVALID: completed_no_change requires a no-change WorkKind or proven empty repository delivery');
140
147
  }
141
148
  if (outcome === 'completed_no_change' && contract.workKind === 'read_only_review' && contract.completionReceipt?.source !== 'read_only_review') {
142
149
  throw new Error('WORK_SEMANTICS_INVALID: read_only_review completed_no_change requires a read_only_review completion receipt');
@@ -14,6 +14,7 @@ import {
14
14
  readControlPlaneRecordWithinTransaction,
15
15
  withControlPlaneTransaction,
16
16
  writeControlPlaneRecordWithinTransaction,
17
+ type SqliteDatabase,
17
18
  } from '../../../../src/runtime/control-plane/persistence/sqlite-store';
18
19
  import {
19
20
  MAX_IMPLEMENTATION_REVIEW_HISTORY,
@@ -97,6 +98,8 @@ export type CreateWorkContractInput = Omit<
97
98
  export interface ListWorkContractOptions extends WorkContractStoreOptions {
98
99
  state?: SemanticWorkState | 'active' | 'all';
99
100
  limit?: number;
101
+ /** Explicit pagination for complete internal historical inventory. */
102
+ offset?: number;
100
103
  detailLevel?: 'summary' | 'detail' | 'raw';
101
104
  }
102
105
 
@@ -121,6 +124,9 @@ export interface WorkContractLineageSnapshot {
121
124
  contracts: WorkContract[];
122
125
  invalid: Array<{
123
126
  workId: string;
127
+ requirementId?: string;
128
+ semanticParentWorkId?: string;
129
+ dependsOnWorkIds?: string[];
124
130
  parentWorkId?: string;
125
131
  predecessorWorkId?: string;
126
132
  supersedes: string[];
@@ -640,6 +646,9 @@ function lineageInvalidWork(
640
646
  const clean = (id: unknown): string | undefined => typeof id === 'string' && id.trim() ? id.trim() : undefined;
641
647
  return {
642
648
  workId: clean(raw.workId) ?? recordKey ?? '',
649
+ ...(clean(raw.requirementId) ? { requirementId: clean(raw.requirementId) } : {}),
650
+ ...(clean(raw.semanticParentWorkId) ? { semanticParentWorkId: clean(raw.semanticParentWorkId) } : {}),
651
+ dependsOnWorkIds: Array.isArray(raw.dependsOnWorkIds) ? raw.dependsOnWorkIds.map(clean).filter((id): id is string => Boolean(id)) : [],
643
652
  ...(clean(raw.parentWorkId) ? { parentWorkId: clean(raw.parentWorkId) } : {}),
644
653
  ...(clean(raw.predecessorWorkId) ? { predecessorWorkId: clean(raw.predecessorWorkId) } : {}),
645
654
  supersedes: Array.isArray(raw.supersedes) ? raw.supersedes.map(clean).filter((id): id is string => Boolean(id)) : [],
@@ -990,6 +999,8 @@ export function isCurrentWorkContract(contract: WorkContract): boolean {
990
999
  export function listWorkContracts(options: ListWorkContractOptions): WorkContract[] {
991
1000
  const state = options.state ?? 'active';
992
1001
  const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 50), 100));
1002
+ const offset = Math.max(0, Math.trunc(options.offset ?? 0));
1003
+ if (state === 'active' && offset > 0) throw new Error('WORK_ACTIVE_PAGINATION_UNSUPPORTED');
993
1004
  if (state === 'active' && sqliteBacked(options)) {
994
1005
  const active = readActiveWorkCandidates({ ...options, limit });
995
1006
  if (active.invalid.length > 0) {
@@ -1008,7 +1019,7 @@ export function listWorkContracts(options: ListWorkContractOptions): WorkContrac
1008
1019
  return semanticWorkState(contract) === state;
1009
1020
  })
1010
1021
  .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
1011
- .slice(0, limit);
1022
+ .slice(offset, offset + limit);
1012
1023
  }
1013
1024
 
1014
1025
  function workExecutionSemanticScopeKeys(contract: WorkContract): string[] {
@@ -1414,6 +1425,7 @@ function updateWorkContractInternal(
1414
1425
  allowLifecycleWrite = false,
1415
1426
  allowImplementationReviewWrite = false,
1416
1427
  allowPhaseRegression = false,
1428
+ atomicPlacementEffect?: (database: SqliteDatabase, contract: WorkContract) => void,
1417
1429
  ): WorkContract {
1418
1430
  return withExactWorkContractWrite(options, workId, () => {
1419
1431
  const authoritativeOptions = authoritativeExistingWorkStoreOptions(options, workId);
@@ -1517,9 +1529,11 @@ function updateWorkContractInternal(
1517
1529
  action: 'work_contract_updated',
1518
1530
  expectedRevision: exact.revision,
1519
1531
  });
1532
+ atomicPlacementEffect?.(database, next);
1520
1533
  });
1521
1534
  return next;
1522
1535
  }
1536
+ if (atomicPlacementEffect) throw new Error('WORK_PLACEMENT_ATOMIC_SQLITE_REQUIRED');
1523
1537
  const contracts = [...store!.contracts];
1524
1538
  contracts[index] = next;
1525
1539
  writeWorkContractStore(authoritativeOptions, { schemaVersion: 4, updatedAt: at, contracts });
@@ -1604,6 +1618,19 @@ export function updateWorkContract(
1604
1618
  ): WorkContract {
1605
1619
  return updateWorkContractInternal(options, workId, patch, false);
1606
1620
  }
1621
+ export function updateWorkContractPlacementAtomically(
1622
+ options: WorkContractStoreOptions,
1623
+ workId: string,
1624
+ patch: Pick<WorkContractMetadataPatch, 'checkoutId' | 'worktreeRef' | 'executionPlacement' | 'worktreePolicy'>,
1625
+ effect: (database: SqliteDatabase, contract: WorkContract) => void,
1626
+ ): WorkContract {
1627
+ // The WorkContract writer remains the sole semantic metadata authority.
1628
+ // An execution WorkHandle is committed inside the same SQLite transaction,
1629
+ // never as a second step that could leave conflicting checkout ownership.
1630
+ return updateWorkContractInternal(options, workId, patch, false, false, false, false, effect);
1631
+ }
1632
+
1633
+
1607
1634
 
1608
1635
  /** Explicit execution-classification transition when an open Work begins governed repository mutation. */
1609
1636
  export function promoteWorkToRepositoryChange(
@@ -6,8 +6,6 @@ import {
6
6
  } from './runtime-contract';
7
7
 
8
8
  export interface BrowserRuntimeProvider<TInput, TResult> extends BrowserProviderCapabilities {
9
- /** Lower values are preferred when multiple providers satisfy the same capability contract. */
10
- priority: number;
11
9
  /** Availability is decided before execution; ordinary provider failures are never fallback signals. */
12
10
  supportsInput(input: TInput): boolean;
13
11
  execute(input: TInput): Promise<TResult>;
@@ -37,22 +35,6 @@ export class BrowserProviderSelectionError extends Error {
37
35
  }
38
36
  }
39
37
 
40
- /**
41
- * A typed pre-action rejection. Runtime may select another provider because this proves the action
42
- * was not attempted. Arbitrary provider errors are intentionally not mapped to this signal.
43
- */
44
- export class BrowserProviderUnavailableBeforeActionError extends Error {
45
- readonly providerId: string;
46
- readonly reason: string;
47
-
48
- constructor(providerId: string, reason: string) {
49
- super(`${providerId} unavailable before action: ${reason}`);
50
- this.name = 'BrowserProviderUnavailableBeforeActionError';
51
- this.providerId = providerId;
52
- this.reason = reason;
53
- }
54
- }
55
-
56
38
  function foregroundCompatible(
57
39
  provider: BrowserProviderCapabilities,
58
40
  requested: BrowserForegroundRequirement,
@@ -61,24 +43,23 @@ function foregroundCompatible(
61
43
  }
62
44
 
63
45
  export class BrowserProviderRegistry<TInput, TResult> {
64
- private readonly providers = new Map<string, BrowserProviderState<TInput, TResult>>();
46
+ private state?: BrowserProviderState<TInput, TResult>;
65
47
 
66
48
  register(provider: BrowserRuntimeProvider<TInput, TResult>): void {
67
- const current = this.providers.get(provider.providerId);
68
- if (current) {
69
- current.provider = provider;
49
+ if (this.state?.provider.providerId === provider.providerId) {
50
+ this.state.provider = provider;
70
51
  return;
71
52
  }
72
- this.providers.set(provider.providerId, { provider, generation: 1 });
53
+ // A configuration change replaces the binding; it never accumulates candidates.
54
+ this.state = { provider, generation: 1 };
73
55
  }
74
56
 
75
57
  invalidate(providerId: string, reason: string): void {
76
- const state = this.providers.get(providerId);
77
- if (state) state.invalidatedReason = reason;
58
+ if (this.state?.provider.providerId === providerId) this.state.invalidatedReason = reason;
78
59
  }
79
60
 
80
61
  invalidateAll(reason: string): void {
81
- for (const state of this.providers.values()) state.invalidatedReason = reason;
62
+ if (this.state) this.state.invalidatedReason = reason;
82
63
  }
83
64
 
84
65
  private async prepare(state: BrowserProviderState<TInput, TResult>): Promise<void> {
@@ -92,17 +73,12 @@ export class BrowserProviderRegistry<TInput, TResult> {
92
73
  input: TInput;
93
74
  requiredCapabilities: readonly BrowserProviderCapability[];
94
75
  foreground: BrowserForegroundRequirement;
95
- excludedProviderIds?: ReadonlySet<string>;
96
76
  }): Promise<BrowserProviderSelection<TInput, TResult>> {
97
- const candidates = [...this.providers.values()]
98
- .filter(({ provider }) => !options.excludedProviderIds?.has(provider.providerId))
99
- .filter(({ provider }) => provider.supportsInput(options.input))
100
- .filter(({ provider }) => providerSupports(provider, options.requiredCapabilities))
101
- .filter(({ provider }) => foregroundCompatible(provider, options.foreground))
102
- .sort((left, right) => left.provider.priority - right.provider.priority
103
- || left.provider.providerId.localeCompare(right.provider.providerId));
104
-
105
- const selected = candidates[0];
77
+ const selected = this.state
78
+ && this.state.provider.supportsInput(options.input)
79
+ && providerSupports(this.state.provider, options.requiredCapabilities)
80
+ && foregroundCompatible(this.state.provider, options.foreground)
81
+ ? this.state : undefined;
106
82
  if (!selected) throw new BrowserProviderSelectionError(options.requiredCapabilities);
107
83
  await this.prepare(selected);
108
84
  return { provider: selected.provider, generation: selected.generation };
@@ -22,6 +22,10 @@ export interface ComputerChatgptConversationObservation {
22
22
  userMessages?: string[];
23
23
  assistantMessages?: string[];
24
24
  composerText?: string;
25
+ /** Read-only content-script selector attestation; undefined on older loaded scripts. */
26
+ composerSelectorVersion?: 'visible-single-v2';
27
+ /** Exact loaded content-script contract; file projection/version on disk is not a live browser attestation. */
28
+ composerReplacementContract?: 'exact-backed-up-project-draft-v1';
25
29
  providerActivityText: string;
26
30
  providerFailureText: string;
27
31
  pageText?: string;
@@ -44,13 +48,13 @@ export interface ComputerChatgptConversationTarget {
44
48
  identity: ComputerChatgptTargetIdentity;
45
49
  observe(options?: ComputerChatgptConversationObservationOptions): Promise<ComputerChatgptConversationObservation>;
46
50
  /** UI-only preparation. Must never click Send or create a provider turn. */
47
- prepare(prompt: string, options?: { mode?: 'send' | 'resume' | 'recover'; reasoning?: 'medium' | 'high' | 'xhigh' }): Promise<{ ready: true } | { ready: false; reasonCode: string }>;
51
+ prepare(prompt: string, options?: { mode?: 'send' | 'resume' | 'recover' }): Promise<{ ready: true } | { ready: false; reasonCode: string }>;
48
52
  dispatch(
49
53
  prompt: string,
50
- options?: { mode?: 'send' | 'resume' | 'recover'; reasoning?: 'medium' | 'high' | 'xhigh' },
54
+ options?: { mode?: 'send' | 'resume' | 'recover' },
51
55
  ): Promise<
52
56
  | { mutation: 'not_attempted'; reasonCode: string }
53
- | { mutation: 'attempted'; confirmed?: boolean; reasoningVerified?: 'medium' | 'high' | 'xhigh' }
57
+ | { mutation: 'attempted'; confirmed?: boolean }
54
58
  >;
55
59
  }
56
60
 
@@ -79,7 +83,10 @@ export interface ComputerObservedChatgptConversation {
79
83
 
80
84
  export interface ComputerChatgptConversationInventory {
81
85
  conversations: ComputerObservedChatgptConversation[];
86
+ /** Full coverage of all supported browser families, not only active providers. */
82
87
  complete: boolean;
88
+ /** At least one provider completed its own inventory; not exact-target/send proof. */
89
+ providerInventoryReady?: boolean;
83
90
  unavailableProviders: string[];
84
91
  }
85
92
 
@@ -91,7 +98,7 @@ export interface ComputerChatgptConversationInventory {
91
98
  export type ComputerChatgptExtensionCommand =
92
99
  | { commandId: string; kind: 'ensure'; identity: ComputerChatgptTargetIdentity; providerBinding?: ComputerSurfaceProviderBinding }
93
100
  | { commandId: string; kind: 'observe'; identity: ComputerChatgptTargetIdentity; providerBinding?: ComputerSurfaceProviderBinding; options?: ComputerChatgptConversationObservationOptions }
94
- | { commandId: string; kind: 'dispatch' | 'prepare'; identity: ComputerChatgptTargetIdentity; providerBinding?: ComputerSurfaceProviderBinding; prompt: string; mode?: 'send' | 'resume' | 'recover'; reasoning?: 'medium' | 'high' | 'xhigh' }
101
+ | { commandId: string; kind: 'dispatch' | 'prepare'; identity: ComputerChatgptTargetIdentity; providerBinding?: ComputerSurfaceProviderBinding; prompt: string; mode?: 'send' | 'resume' | 'recover' }
95
102
  | { commandId: string; kind: 'find_marker'; marker: string; bootstrapKey?: string }
96
103
  | { commandId: string; kind: 'close'; identity: ComputerChatgptTargetIdentity; providerBinding?: ComputerSurfaceProviderBinding };
97
104
  export type ComputerChatgptExtensionCommandInput = ComputerChatgptExtensionCommand extends infer Command
@@ -107,10 +114,12 @@ export type ComputerChatgptExtensionCommandResult =
107
114
  | { kind: 'prepared'; ready: true }
108
115
  | { kind: 'prepared'; ready: false; reasonCode: string }
109
116
  | { kind: 'dispatch'; mutation: 'not_attempted'; reasonCode: string }
110
- | { kind: 'dispatch'; mutation: 'attempted'; confirmed?: boolean; reasoningVerified?: 'medium' | 'high' | 'xhigh'; observation?: ComputerChatgptConversationObservation }
117
+ | { kind: 'dispatch'; mutation: 'attempted'; confirmed?: boolean; observation?: ComputerChatgptConversationObservation }
111
118
  | { kind: 'marker_matches'; matches: Array<{ identity: ComputerChatgptConversationIdentity; providerBinding?: ComputerSurfaceProviderBinding; observation: ComputerChatgptConversationObservation }> }
112
119
  | { kind: 'closed' }
113
- | { kind: 'failed'; code: string; retryable?: boolean; failoverSafe?: boolean; humanAction?: 'login' | 'grant_permission' };
120
+ | { kind: 'failed'; code: string; retryable?: boolean; failoverSafe?: boolean; humanAction?: 'login' | 'grant_permission';
121
+ /** Exact attachment retained after ensure hydration failed; never Send/readiness authority. */
122
+ providerBinding?: ComputerSurfaceProviderBinding };
114
123
 
115
124
  export interface ComputerChatgptExtensionHeartbeat {
116
125
  providerId: string;
@@ -128,7 +137,8 @@ export interface ComputerChatgptExtensionBrokerRpc {
128
137
 
129
138
  export interface ComputerChatgptConversationTargetPort {
130
139
  inventory(): Promise<ComputerChatgptConversationInventory>;
131
- ensureExact(identity: ComputerChatgptConversationIdentity): Promise<ComputerChatgptTargetResult>;
140
+ /** Read-only reconciliation cannot create a missing tab. Only a separately authorized new send may opt in to exact conversation creation. */
141
+ ensureExact(identity: ComputerChatgptConversationIdentity, options?: { createIfMissing?: boolean }): Promise<ComputerChatgptTargetResult>;
132
142
  openBootstrap(projectUrl: string, bootstrapKey: string): Promise<ComputerChatgptTargetResult>;
133
143
  findBySubmittedMarker(
134
144
  marker: string,
@@ -9,7 +9,6 @@
9
9
  import { readFileSync, existsSync, readdirSync } from "fs";
10
10
  import { join, dirname } from "path";
11
11
  import { fileURLToPath } from "url";
12
- import { runHooks } from "./run-skill-hook";
13
12
 
14
13
  // ============================================================================
15
14
  // Types
@@ -64,23 +63,6 @@ export interface PlanMap {
64
63
  plans: Record<string, PlanConfig>;
65
64
  }
66
65
 
67
- export interface SkillVersionManifest {
68
- version: string;
69
- templateVersion: string;
70
- compatibility: {
71
- minClaudeCodeVersion: string;
72
- minBunVersion: string;
73
- };
74
- breakingChanges: Array<{
75
- version: string;
76
- description: string;
77
- }>;
78
- generatedProjectStamp: {
79
- format: string;
80
- location: string;
81
- };
82
- }
83
-
84
66
  interface RuntimeProfileConfig {
85
67
  label: string;
86
68
  claudePolicy: string;
@@ -134,7 +116,6 @@ const ASSETS_DIR = join(REPO_ROOT, "assets");
134
116
  const PARTIALS_DIR = join(ASSETS_DIR, "partials");
135
117
  const PARTIALS_AGENTS_DIR = join(ASSETS_DIR, "partials-agents");
136
118
  const VERSIONS_FILE = join(ASSETS_DIR, "versions.json");
137
- const SKILL_VERSION_FILE = join(ASSETS_DIR, "skill-version.json");
138
119
  const PLAN_MAP_FILE = join(ASSETS_DIR, "plan-map.json");
139
120
  const QUESTION_PACK_FILE = join(ASSETS_DIR, "initializer-question-pack.v4.json");
140
121
 
@@ -276,31 +257,14 @@ export function loadVersions(versionsFilePath: string = VERSIONS_FILE): Record<s
276
257
  return versions;
277
258
  }
278
259
 
279
- /**
280
- * Load skill version manifest from skill-version.json.
281
- */
282
- export function loadSkillVersion(
283
- skillVersionFilePath: string = SKILL_VERSION_FILE
284
- ): SkillVersionManifest {
285
- if (!existsSync(skillVersionFilePath)) {
286
- throw new Error(`skill-version.json not found at ${skillVersionFilePath}`);
287
- }
288
-
289
- let parsed: unknown;
290
- try {
291
- parsed = JSON.parse(readFileSync(skillVersionFilePath, "utf-8"));
292
- } catch (error) {
293
- throw new Error(
294
- `Failed to parse skill-version.json: ${error instanceof Error ? error.message : String(error)}`
295
- );
296
- }
297
-
298
- const manifest = parsed as SkillVersionManifest;
299
- if (!manifest.version || !manifest.templateVersion) {
300
- throw new Error("skill-version.json missing required version or templateVersion field");
260
+ /** Read the Forge release version from its single canonical package manifest. */
261
+ export function loadForgeVersion(): string {
262
+ const pkgPath = join(ASSETS_DIR, "..", "package.json");
263
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as { version?: string };
264
+ if (typeof pkg.version !== "string" || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(pkg.version)) {
265
+ throw new Error("Invalid Forge package version in " + pkgPath);
301
266
  }
302
-
303
- return manifest;
267
+ return pkg.version;
304
268
  }
305
269
 
306
270
  /**
@@ -935,7 +899,7 @@ export function assembleTemplate(options: AssemblyOptions): string {
935
899
 
936
900
  // Load version variables
937
901
  const versions = loadVersions();
938
- const skillVersion = loadSkillVersion();
902
+ const forgeVersion = loadForgeVersion();
939
903
 
940
904
  // Merge all variables (user variables take precedence)
941
905
  const mergedVariables: Record<string, string> = {
@@ -943,8 +907,8 @@ export function assembleTemplate(options: AssemblyOptions): string {
943
907
  ...getDefaultTemplateVariables(resolvedPlanType, planMap, quickMode),
944
908
  ...variables,
945
909
  PLAN_TYPE: resolvedPlanType,
946
- SKILL_VERSION: skillVersion.version,
947
- TEMPLATE_VERSION: skillVersion.templateVersion,
910
+ FORGE_VERSION: forgeVersion,
911
+ TEMPLATE_VERSION: forgeVersion,
948
912
  };
949
913
 
950
914
  const webappRenderingModel = getWebappRenderingTemplateVariables(
@@ -1017,31 +981,6 @@ export function assembleTemplate(options: AssemblyOptions): string {
1017
981
  return assembled;
1018
982
  }
1019
983
 
1020
- /**
1021
- * Async wrapper that runs lifecycle hooks around assembleTemplate().
1022
- * Use this from CLI or scripts that support async. Tests can use sync assembleTemplate().
1023
- */
1024
- export async function assembleTemplateWithHooks(options: AssemblyOptions): Promise<string> {
1025
- const hookContext = {
1026
- planType: options.planType,
1027
- target: options.target ?? "claude",
1028
- quickMode: options.quickMode ?? false,
1029
- };
1030
-
1031
- // Pre-assemble hook (sync — failure aborts)
1032
- const preResult = await runHooks("pre-assemble", hookContext);
1033
- if (!preResult.success) {
1034
- throw new Error("pre-assemble hook failed, aborting assembly");
1035
- }
1036
-
1037
- const output = assembleTemplate(options);
1038
-
1039
- // Post-assemble hook (advisory — failure is warning only)
1040
- await runHooks("post-assemble", { ...hookContext, outputLength: output.length });
1041
-
1042
- return output;
1043
- }
1044
-
1045
984
  // ============================================================================
1046
985
  // CLI
1047
986
  // ============================================================================
@@ -1163,7 +1102,7 @@ if (import.meta.main) {
1163
1102
  cloudflareNative: parsed.cloudflare,
1164
1103
  };
1165
1104
 
1166
- const output = await assembleTemplateWithHooks(options);
1105
+ const output = assembleTemplate(options);
1167
1106
  console.log(output);
1168
1107
  } catch (error) {
1169
1108
  console.error("Error:", error instanceof Error ? error.message : error);