@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
@@ -1,3 +1,4 @@
1
+ import { spawnSync } from 'child_process';
1
2
  import { createHash, randomUUID } from 'crypto';
2
3
  import { existsSync } from 'fs';
3
4
  import { createServer, type IncomingMessage, type ServerResponse } from 'http';
@@ -42,7 +43,6 @@ import {
42
43
  runtimeStatus,
43
44
  verifyStableRuntime,
44
45
  verifyConfiguredRuntimeReleaseSessionStaticGates,
45
- probeReleaseCutoverReadiness,
46
46
  watchdogTick,
47
47
  type WatchdogState,
48
48
  type RecoveryConfig,
@@ -64,6 +64,8 @@ import {
64
64
  } from './release';
65
65
  import { RECOVERY_MUTATION_IDENTITY_CONTRACT, RECOVERY_MUTATION_IDENTITY_FIELDS } from './mutation-identity-contract';
66
66
  import { readReleaseSession } from '../release/release-session';
67
+ import { readRuntimeOwner } from '../root/ownership';
68
+ import { observeRuntimeStatus } from '../root/status';
67
69
  import { migrateReleaseDurableState } from '../release/release-state-migration';
68
70
  import {
69
71
  advanceConfiguredRuntimeRelease,
@@ -91,6 +93,37 @@ const RECOVERY_AUTOMATIC_RELEASE_INTERVAL_MS = 15_000;
91
93
  const RECOVERY_AUTOMATIC_RELEASE_FAILURE_BACKOFF_MAX_MS = 15 * 60_000;
92
94
  const RECOVERY_AUTOMATIC_RELEASE_STEP_TIMEOUT_MS = 15 * 60_000;
93
95
 
96
+ /**
97
+ * A CLI launched from the primary Runtime's Process Runtime cannot own its
98
+ * cutover: stopping Stable A cancels that very CLI before the release
99
+ * transaction can settle. The persistent Recovery daemon/MCP is independent.
100
+ * Compare the serving Runtime's existing process owner with the actual OS
101
+ * parent chain, not a caller-supplied scope or model-provided permission flag.
102
+ */
103
+ function assertIndependentReleaseCliOwner(controllerHome: string): void {
104
+ const runtime = observeRuntimeStatus(controllerHome);
105
+ const owner = runtime.running ? readRuntimeOwner(controllerHome) : undefined;
106
+ if (!owner) return;
107
+ if (process.platform === 'win32') throw new Error('RECOVERY_CUTOVER_PROCESS_ANCESTRY_UNVERIFIED');
108
+ let pid = process.ppid;
109
+ const visited = new Set<number>();
110
+ for (let depth = 0; depth < 32 && pid > 1; depth += 1) {
111
+ if (pid === owner.pid) {
112
+ throw new Error('RECOVERY_CUTOVER_REQUIRES_INDEPENDENT_OWNER: invoke the Recovery daemon/MCP, not a command owned by the primary Runtime');
113
+ }
114
+ if (visited.has(pid)) throw new Error('RECOVERY_CUTOVER_PROCESS_ANCESTRY_UNVERIFIED');
115
+ visited.add(pid);
116
+ const inspected = spawnSync('ps', ['-o', 'ppid=', '-p', String(pid)], {
117
+ encoding: 'utf8', timeout: 2_000, maxBuffer: 4 * 1024,
118
+ });
119
+ if (inspected.status !== 0) throw new Error('RECOVERY_CUTOVER_PROCESS_ANCESTRY_UNVERIFIED');
120
+ const parent = Number.parseInt(inspected.stdout.trim(), 10);
121
+ if (!Number.isInteger(parent) || parent < 1) throw new Error('RECOVERY_CUTOVER_PROCESS_ANCESTRY_UNVERIFIED');
122
+ pid = parent;
123
+ }
124
+ if (pid > 1) throw new Error('RECOVERY_CUTOVER_PROCESS_ANCESTRY_UNVERIFIED');
125
+ }
126
+
94
127
  function option(name: string): string | undefined {
95
128
  const index = process.argv.indexOf(name);
96
129
  return index >= 0 ? process.argv[index + 1] : undefined;
@@ -194,6 +227,7 @@ async function cli(): Promise<void> {
194
227
  config,
195
228
  RECOVERY_RUNTIME_RELEASE_PROVIDER,
196
229
  `recovery-auto-release:${process.pid}:${Date.now()}`,
230
+ decision,
197
231
  );
198
232
  output({ schemaVersion: 1, ok: result.ok, attempted: result.attempted, decision, result });
199
233
  } catch (error) {
@@ -219,11 +253,12 @@ async function cli(): Promise<void> {
219
253
  }
220
254
  case 'list-releases': output(await listReleases(config)); return;
221
255
  case 'attest-known-good': output(await attestKnownGood(config)); return;
222
- case 'rollback-previous': output(await rollbackPrevious(config)); return;
223
- case 'restart-primary-runtime': output(await restartPrimaryRuntime(config, { forceHealthyRestart: true })); return;
224
- case 'restart-primary-connector': output(await restartPrimaryConnector(config)); return;
225
- case 'recover-primary-runtime': output(await recoverPrimaryRuntime(config)); return;
256
+ case 'rollback-previous': assertIndependentReleaseCliOwner(config.controllerHome); output(await rollbackPrevious(config)); return;
257
+ case 'restart-primary-runtime': assertIndependentReleaseCliOwner(config.controllerHome); output(await restartPrimaryRuntime(config, { forceHealthyRestart: true })); return;
258
+ case 'restart-primary-connector': output(await restartPrimaryConnector(config, { forceHealthyRestart: true, requestId: `recovery-cli:${process.pid}:${Date.now()}` })); return;
259
+ case 'recover-primary-runtime': assertIndependentReleaseCliOwner(config.controllerHome); output(await recoverPrimaryRuntime(config)); return;
226
260
  case 'activate-runtime-release': {
261
+ assertIndependentReleaseCliOwner(config.controllerHome);
227
262
  const releasePath = option('--release-manifest') ?? option('--release-path');
228
263
  const expectedActiveReleaseId = option('--expected-active-release');
229
264
  const expectedAuthorityRevisionRaw = option('--expected-authority-revision');
@@ -238,14 +273,18 @@ async function cli(): Promise<void> {
238
273
  }));
239
274
  return;
240
275
  }
241
- case 'stage-and-activate-runtime-release': output(await stageAndActivateConfiguredRuntimeRelease(config, {}, `recovery-cli:${process.pid}:${Date.now()}`)); return;
276
+ case 'stage-and-activate-runtime-release': assertIndependentReleaseCliOwner(config.controllerHome); output(await stageAndActivateConfiguredRuntimeRelease(config, {}, `recovery-cli:${process.pid}:${Date.now()}`)); return;
242
277
  case 'release-session-status': {
243
278
  const sessionId = option('--session-id');
244
279
  if (!sessionId) throw new Error('RECOVERY_RELEASE_SESSION_ID_REQUIRED');
245
280
  output(readReleaseSession(config.controllerHome, sessionId) ?? null);
246
281
  return;
247
282
  }
248
- case 'release-session-advance': output(await advanceConfiguredRuntimeRelease(config, RECOVERY_RUNTIME_RELEASE_PROVIDER, `recovery-cli:${process.pid}:${Date.now()}`)); return;
283
+ case 'release-session-advance': {
284
+ assertIndependentReleaseCliOwner(config.controllerHome);
285
+ output(await advanceConfiguredRuntimeRelease(config, RECOVERY_RUNTIME_RELEASE_PROVIDER, `recovery-cli:${process.pid}:${Date.now()}`));
286
+ return;
287
+ }
249
288
  case 'release-session-prepare': output(await prepareConfiguredRuntimeReleaseSession(config, {}, `recovery-cli:${process.pid}:${Date.now()}`)); return;
250
289
  case 'release-session-static-verify': {
251
290
  const sessionId = option('--session-id');
@@ -260,6 +299,7 @@ async function cli(): Promise<void> {
260
299
  return;
261
300
  }
262
301
  case 'release-session-cutover': {
302
+ assertIndependentReleaseCliOwner(config.controllerHome);
263
303
  const sessionId = option('--session-id');
264
304
  if (!sessionId) throw new Error('RECOVERY_RELEASE_SESSION_ID_REQUIRED');
265
305
  output(await cutoverConfiguredRuntimeReleaseSession(config, sessionId, `recovery-cli:${process.pid}:${Date.now()}`));
@@ -272,6 +312,7 @@ async function cli(): Promise<void> {
272
312
  return;
273
313
  }
274
314
  case 'release-session-rollback': {
315
+ assertIndependentReleaseCliOwner(config.controllerHome);
275
316
  const sessionId = option('--session-id');
276
317
  if (!sessionId) throw new Error('RECOVERY_RELEASE_SESSION_ID_REQUIRED');
277
318
  output(await rollbackConfiguredRuntimeReleaseSession(config, sessionId, {}, `recovery-cli:${process.pid}:${Date.now()}`));
@@ -439,7 +480,7 @@ export const RECOVERY_TOOLS = [
439
480
  { name: 'attest_known_good', description: 'Record the active release as known-good only after full independent verification succeeds.', inputSchema: mutationInputSchema() },
440
481
  { name: 'rollback_previous', description: 'While Canonical Runtime is stopped, atomically restore its attested previous whole-Runtime release and SQLite backup.', inputSchema: mutationInputSchema() },
441
482
  { name: 'restart_primary_runtime', description: 'Explicitly restart the installed canonical Forge Runtime service, including when currently healthy, then require whole-Runtime verification after exact Recovery machine identity matches.', inputSchema: mutationInputSchema() },
442
- { name: 'restart_primary_connector', description: 'Restart the explicitly configured primary OAuth/Connector service only after exact Recovery machine identity and local Canonical Runtime verification succeed.', inputSchema: mutationInputSchema() },
483
+ { name: 'restart_primary_connector', description: 'Explicitly restart the configured primary OAuth/Connector service, including when endpoint probes pass, only after exact Recovery machine identity and local Canonical Runtime verification succeed; verify the listener and public endpoint again afterward.', inputSchema: mutationInputSchema() },
443
484
  { name: 'recover_primary_runtime', description: 'Stop the canonical Runtime, restore the attested previous whole-Runtime release and SQLite backup, restart it, and require verification.', inputSchema: mutationInputSchema() },
444
485
  { name: 'activate_runtime_release', description: 'Activate an already staged immutable Runtime release only if machine identity and caller-observed active release/authority revision are still current. Reverse activation of current.previous is rejected; use rollback_previous/recover_primary_runtime instead.', inputSchema: mutationInputSchema({ release_path: { type: 'string', minLength: 8, maxLength: 1024, description: 'Absolute path to the staged immutable Runtime release directory.' }, expected_active_release_id: { type: 'string', minLength: 1, maxLength: 256 }, expected_authority_revision: { type: 'integer', minimum: 1 } }, ['release_path', 'expected_active_release_id', 'expected_authority_revision']) },
445
486
  { name: 'pin_runtime_release', description: 'Pin one extant legacy/home-bound immutable Runtime release so retention preserves it for explicit Runtime-only recovery activation. Portable source candidates are rejected and must use ReleaseSession.', inputSchema: mutationInputSchema({ release_path: { type: 'string', minLength: 8, maxLength: 1024, description: 'Absolute path to the immutable Runtime release directory or manifest.' } }, ['release_path']) },
@@ -781,7 +822,7 @@ export async function dispatchRecoveryTool(config: RecoveryConfig, name: string,
781
822
  case 'restart_primary_connector': {
782
823
  if (!requestId(args.request_id)) throw new Error('RECOVERY_REQUEST_ID_REQUIRED');
783
824
  assertRecoveryGatewayMutationIdentity(config, args);
784
- return mutationResponse(config, await restartPrimaryConnector(config, { requestId: `recovery-gateway:${args.request_id}` }));
825
+ return mutationResponse(config, await restartPrimaryConnector(config, { requestId: `recovery-gateway:${args.request_id}`, forceHealthyRestart: true }));
785
826
  }
786
827
  case 'recover_primary_runtime': {
787
828
  if (!requestId(args.request_id)) throw new Error('RECOVERY_REQUEST_ID_REQUIRED');
@@ -995,6 +1036,14 @@ async function runAutomaticReleaseReconciliationStep(
995
1036
  : 'automatic release reconciliation failed';
996
1037
  throw new Error(`RECOVERY_AUTOMATIC_RELEASE_STEP_FAILED: ${detail.slice(0, 500)}`);
997
1038
  }
1039
+ // An idle-window timeout is a legitimate no-cutover outcome, but not
1040
+ // forward progress. Feed it into the existing phase/revision backoff so
1041
+ // repeated drains cannot monopolize the Connector every 15 seconds.
1042
+ if (parsed.decision?.action === 'cutover'
1043
+ && typeof parsed.result?.detail === 'string'
1044
+ && parsed.result.detail.startsWith('RELEASE_SESSION_CUTOVER_DEFERRED_FOR_MCP_ACTIVITY')) {
1045
+ throw new Error(`RECOVERY_AUTOMATIC_RELEASE_CUTOVER_DEFERRED: ${parsed.result.detail.slice(0, 500)}`);
1046
+ }
998
1047
  if (parsed.decision?.action) {
999
1048
  process.stdout.write(JSON.stringify({
1000
1049
  at: new Date().toISOString(),
@@ -1017,27 +1066,10 @@ async function startAutomaticReleaseReconciliation(config: RecoveryConfig): Prom
1017
1066
  () => configuredRuntimeReleaseSourceState(config),
1018
1067
  );
1019
1068
  observed.fingerprint = decision.required ? releaseReconciliationFingerprint(decision) : undefined;
1020
- if (decision.required && decision.action === 'cutover') {
1021
- const gate = await probeReleaseCutoverReadiness(config);
1022
- if (!gate.ready) {
1023
- process.stdout.write(JSON.stringify({
1024
- at: new Date().toISOString(),
1025
- action: 'automatic_release_cutover_deferred',
1026
- reason: gate.detail,
1027
- sessionId: decision.session?.sessionId,
1028
- phase: decision.session?.phase,
1029
- activeSessions: gate.activeSessions,
1030
- activePosts: gate.activePosts,
1031
- activeStreams: gate.activeStreams,
1032
- initializing: gate.initializing,
1033
- latestActivityAgeMs: gate.latestActivityAgeMs,
1034
- }) + '\n');
1035
- } else {
1036
- await runAutomaticReleaseReconciliationStep(config, observed, decision);
1037
- }
1038
- } else {
1039
- await runAutomaticReleaseReconciliationStep(config, observed, decision);
1040
- }
1069
+ // ReleaseSession cutover owns the one Connector drain/quietness fence.
1070
+ // A read-only busy pre-gate here would prevent that drain from ever
1071
+ // starting under sustained MCP traffic, starving the verified candidate.
1072
+ if (decision.required) await runAutomaticReleaseReconciliationStep(config, observed, decision);
1041
1073
  } catch (error) {
1042
1074
  failure = error instanceof Error ? error.message : String(error);
1043
1075
  process.stderr.write(`automatic release reconciliation failed: ${failure}\n`);
@@ -24,6 +24,16 @@ export type RecoveryOperationLockAttempt =
24
24
  | { acquired: true; handle: RecoveryOperationLockHandle }
25
25
  | { acquired: false; owner: RecoveryOperationLockRecord };
26
26
 
27
+ /** Preserve the competing owner identities when lock acquisition detects a race. */
28
+ export class RecoveryOperationLockRaceError extends Error {
29
+ constructor(
30
+ readonly existing: RecoveryOperationLockRecord,
31
+ readonly latest: RecoveryOperationLockRecord | undefined,
32
+ ) {
33
+ super('RECOVERY_OPERATION_LOCK_RACE');
34
+ }
35
+ }
36
+
27
37
  function processStartTime(pid: number): string | undefined {
28
38
  const result = spawnSync('/bin/ps', ['-o', 'lstart=', '-p', String(pid)], {
29
39
  encoding: 'utf8',
@@ -123,7 +133,7 @@ export function acquireRecoveryOperationLock(input: {
123
133
  if (!latest || latest.instanceId !== existing.instanceId) {
124
134
  if (attempt === 1) {
125
135
  if (latest && recoveryOperationLockOwnerAlive(latest)) return { acquired: false, owner: latest };
126
- throw new Error('RECOVERY_OPERATION_LOCK_RACE');
136
+ throw new RecoveryOperationLockRaceError(existing, latest);
127
137
  }
128
138
  continue;
129
139
  }
@@ -1,4 +1,5 @@
1
1
  import type { RepositorySchedule } from '../../../../packages/kernel/scheduler/api/index';
2
+
2
3
  import {
3
4
  DEFAULT_CHATGPT_AUTOMATION_MODEL,
4
5
  DEFAULT_CHATGPT_AUTOMATION_REASONING,
@@ -51,6 +51,7 @@ import type {
51
51
  const execFileAsync = promisify(execFile);
52
52
  const LIVE_MAINTENANCE_OPERATION = 'runtime_maintenance_apply';
53
53
  const EXTERNAL_CONTROLLER_WAKE_OPERATION = 'external_controller_wake';
54
+ const CONTROLLER_PROGRESSION_OPERATION = 'controller_progression';
54
55
  const BROWSER_PROBE_OPERATION = 'browser_probe';
55
56
  const GITHUB_ISSUE_WATCH_OPERATION = 'github_issue_watch';
56
57
  const DEFAULT_EXTERNAL_CONTROLLER_WAKE_TIMEOUT_MS = 60_000;
@@ -60,6 +61,7 @@ function isDeterministicSchedule(schedule: RepositorySchedule): boolean {
60
61
  // Kernel executes only typed, bounded dispatch operations.
61
62
  return schedule.action.operation === LIVE_MAINTENANCE_OPERATION
62
63
  || schedule.action.operation === EXTERNAL_CONTROLLER_WAKE_OPERATION
64
+ || schedule.action.operation === CONTROLLER_PROGRESSION_OPERATION
63
65
  || schedule.action.operation === BROWSER_PROBE_OPERATION
64
66
  || schedule.action.operation === GITHUB_ISSUE_WATCH_OPERATION;
65
67
  }
@@ -87,6 +89,7 @@ async function workspaceDirty(controllerHome: string, repoId: string): Promise<b
87
89
 
88
90
  function workBoundScheduleWorkId(schedule: RepositorySchedule): string | undefined {
89
91
  if (schedule.action.operation !== EXTERNAL_CONTROLLER_WAKE_OPERATION
92
+ && schedule.action.operation !== CONTROLLER_PROGRESSION_OPERATION
90
93
  && schedule.action.operation !== BROWSER_PROBE_OPERATION) return undefined;
91
94
  const value = schedule.action.arguments?.work_id;
92
95
  return typeof value === 'string' && value.trim() ? value.trim() : undefined;
@@ -246,7 +249,7 @@ export function classifyChatgptWakeFailure(reason: string): ChatgptWakeFailureCl
246
249
  return 'ordinary_failure';
247
250
  }
248
251
 
249
- async function executeExternalControllerWake(
252
+ async function executeControllerProgression(
250
253
  controllerHome: string,
251
254
  schedule: RepositorySchedule,
252
255
  occurrence: ScheduleOccurrence,
@@ -311,12 +314,20 @@ async function executeExternalControllerWake(
311
314
  );
312
315
  }
313
316
 
317
+ if (schedule.action.operation === CONTROLLER_PROGRESSION_OPERATION
318
+ && (typeof args.controller_binding_id !== 'string' || !args.controller_binding_id.trim())) {
319
+ throw new Error(`SCHEDULE_CONTROLLER_PROGRESSION_BINDING_REQUIRED:${workId}`);
320
+ }
321
+
314
322
  const hadLegacySessionIdentity = typeof args.controller_session_id === 'string' && args.controller_session_id.trim().length > 0;
315
323
 
316
324
  // Explicit compatibility migration for schedules written before Kernel V2 B4.
317
325
  // Provider metadata is copied once into the adapter-owned binding and the
318
326
  // Schedule is rewritten to exact Work/ControllerSession/ControllerBinding ids.
319
327
  if (!bindingRecord) {
328
+ if (schedule.action.operation === CONTROLLER_PROGRESSION_OPERATION) {
329
+ throw new Error(`SCHEDULE_CONTROLLER_PROGRESSION_BINDING_NOT_FOUND:${workId}`);
330
+ }
320
331
  const binding = ensureScheduledControllerBinding(
321
332
  { controllerHome, repoId: schedule.repoId },
322
333
  { workId, session: retainedSession, scheduleName: schedule.name, args },
@@ -388,6 +399,8 @@ async function executeExternalControllerWake(
388
399
  continuationHint,
389
400
  allowSemanticWaitRecovery: controllerType === 'chatgpt',
390
401
  scheduleName: schedule.name,
402
+ scheduleId: schedule.scheduleId,
403
+ scheduleOccurrenceId: occurrence.occurrenceId,
391
404
  bindingArgs: args,
392
405
  },
393
406
  );
@@ -442,13 +455,17 @@ async function executeExternalControllerWake(
442
455
 
443
456
  const supervisorEnrollment = progression.status === 'chatgpt_enrolled'
444
457
  ? progression.supervisorEnrollment
445
- : { status: 'not_eligible' as const };
458
+ : progression.status === 'scheduled_supervisor_woken'
459
+ ? { status: 'enrolled' as const, taskId: progression.taskId, effectId: progression.effectId }
460
+ : { status: 'not_eligible' as const };
461
+ const supervisorOwnsTurn = progression.status === 'chatgpt_enrolled' || progression.status === 'scheduled_supervisor_woken';
446
462
  updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, (current) => ({
447
463
  lastTriggeredAt: timestamp,
448
464
  lastOccurrenceId: occurrence.occurrenceId,
449
465
  consecutiveFailures: 0,
450
466
  nextEligibleAt: undefined,
451
467
  ...(supervisorEnrollment.status === 'enrolled'
468
+ && schedule.action.operation !== CONTROLLER_PROGRESSION_OPERATION
452
469
  ? { enabled: false, pausedReason: 'workflow_supervisor_owns_outer_turn' }
453
470
  : current.enabled ? { pausedReason: undefined } : {}),
454
471
  }));
@@ -457,18 +474,22 @@ async function executeExternalControllerWake(
457
474
  status: 'dispatched',
458
475
  reason: progression.status === 'chatgpt_enrolled'
459
476
  ? `Workflow Supervisor reserved the exact ChatGPT effect ${progression.supervisorEnrollment.effectId ?? progression.relayScopeId}; Scheduler issued no provider send.`
460
- : progression.reused
461
- ? `Controller continuation occurrence ${occurrence.occurrenceId} was already durably dispatched; external replay suppressed and semantic round closure is still pending.`
462
- : `ControllerHost accepted durable binding ${bindingRecord.binding.bindingId} for Work ${workId}; dispatch ${progression.providerDispatchReceiptId ?? progression.relayScopeId}; semantic round closure is still pending.`,
477
+ : progression.status === 'scheduled_supervisor_woken'
478
+ ? `Scheduler occurrence ${occurrence.occurrenceId} woke the same Workflow Supervisor task and reserved effect ${progression.effectId}; Scheduler issued no provider send.`
479
+ : progression.reused
480
+ ? `Controller continuation occurrence ${occurrence.occurrenceId} was already durably dispatched; external replay suppressed and semantic round closure is still pending.`
481
+ : `ControllerHost accepted durable binding ${bindingRecord.binding.bindingId} for Work ${workId}; dispatch ${progression.providerDispatchReceiptId ?? progression.relayScopeId}; semantic round closure is still pending.`,
463
482
  });
464
483
  appendWorkEvidence(workStore, workId, {
465
484
  evidenceId: dispatchedOccurrence.occurrenceId,
466
- title: progression.status === 'chatgpt_enrolled'
485
+ title: supervisorOwnsTurn
467
486
  ? 'scheduled Workflow Supervisor effect reserved'
468
487
  : 'scheduled Controller continuation dispatched',
469
- summary: progression.status === 'chatgpt_enrolled'
470
- ? `Schedule ${schedule.scheduleId} materialized the exact ControllerRound occurrence and Supervisor effect; Supervisor owns ChatGPT delivery.`
471
- : `Schedule ${schedule.scheduleId} occurrence ${dispatchedOccurrence.occurrenceId} resumed the exact Work-bound ControllerSession through ControllerHost.`,
488
+ summary: progression.status === 'scheduled_supervisor_woken'
489
+ ? `Schedule ${schedule.scheduleId} occurrence ${dispatchedOccurrence.occurrenceId} woke the same Work-bound Supervisor task; Supervisor owns ChatGPT delivery.`
490
+ : progression.status === 'chatgpt_enrolled'
491
+ ? `Schedule ${schedule.scheduleId} materialized the exact ControllerRound occurrence and Supervisor effect; Supervisor owns ChatGPT delivery.`
492
+ : `Schedule ${schedule.scheduleId} occurrence ${dispatchedOccurrence.occurrenceId} resumed the exact Work-bound ControllerSession through ControllerHost.`,
472
493
  detailLevel: 'summary',
473
494
  });
474
495
  return dispatchedOccurrence;
@@ -724,8 +745,9 @@ export async function evaluateSchedule(
724
745
  }
725
746
 
726
747
  const args = schedule.action.arguments ?? {};
727
- if (schedule.action.operation === EXTERNAL_CONTROLLER_WAKE_OPERATION) {
728
- return executeExternalControllerWake(controllerHome, schedule, occurrence, timestamp, args);
748
+ if (schedule.action.operation === EXTERNAL_CONTROLLER_WAKE_OPERATION
749
+ || schedule.action.operation === CONTROLLER_PROGRESSION_OPERATION) {
750
+ return executeControllerProgression(controllerHome, schedule, occurrence, timestamp, args);
729
751
  }
730
752
 
731
753
  if (schedule.action.operation === GITHUB_ISSUE_WATCH_OPERATION) {
@@ -772,7 +794,7 @@ export async function evaluateSchedule(
772
794
  ? `${args.continuation_prompt}\n\nGitHub issue watcher detected: ${issueSummary}`
773
795
  : `GitHub issue watcher detected actionable issue changes: ${issueSummary}. Read current Work/Plan/evidence and process these issues through the bounded repair workflow.`,
774
796
  };
775
- const wake = await executeExternalControllerWake(controllerHome, schedule, occurrence, timestamp, wakeArgs, evidence, { transientWakeFailure: true });
797
+ const wake = await executeControllerProgression(controllerHome, schedule, occurrence, timestamp, wakeArgs, evidence, { transientWakeFailure: true });
776
798
  if (wake.status !== 'failed') persistObservation();
777
799
  return wake;
778
800
  } catch (error) {
@@ -836,7 +858,7 @@ export async function evaluateSchedule(
836
858
  : `Scheduled browser watcher ${schedule.scheduleId} detected that authentication is required (${probe.authReason ?? 'login marker'}). Inspect the external dependency and request user login only if it cannot be restored safely.`;
837
859
  if (workId) {
838
860
  const wakeArgs = { ...args, continuation_prompt: authPrompt };
839
- return executeExternalControllerWake(controllerHome, observedSchedule, occurrence, timestamp, wakeArgs, { ...observationEvidence, authReason: probe.authReason });
861
+ return executeControllerProgression(controllerHome, observedSchedule, occurrence, timestamp, wakeArgs, { ...observationEvidence, authReason: probe.authReason });
840
862
  }
841
863
  const controllerType = typeof args.controller_type === 'string' ? args.controller_type.trim() : 'chatgpt';
842
864
  if (controllerType !== 'chatgpt') {
@@ -917,7 +939,7 @@ export async function evaluateSchedule(
917
939
  ? args.continuation_prompt
918
940
  : `Scheduled browser watcher ${schedule.scheduleId} detected a changed external observation. Read the current external state, correlate it with Work ${workId}, and continue only the bounded Work objective.`,
919
941
  };
920
- return executeExternalControllerWake(controllerHome, observedSchedule, occurrence, timestamp, wakeArgs, { ...observationEvidence, observationStatus });
942
+ return executeControllerProgression(controllerHome, observedSchedule, occurrence, timestamp, wakeArgs, { ...observationEvidence, observationStatus });
921
943
  } catch (error) {
922
944
  const reason = error instanceof Error ? error.message : String(error);
923
945
  const failed = applyScheduleFailure(controllerHome, schedule.scheduleId, schedule.repoId, occurrence.occurrenceId, {
@@ -5,6 +5,7 @@ import { resolveControllerHome } from '../../../cli/repositories/controller-home
5
5
  import { getSchedule, saveSchedule } from '../../../../packages/kernel/scheduler/api/index';
6
6
 
7
7
  const EXTERNAL_CONTROLLER_WAKE_OPERATION = 'external_controller_wake';
8
+ const CONTROLLER_PROGRESSION_OPERATION = 'controller_progression';
8
9
 
9
10
  export interface PendingContinuationActivationMarker {
10
11
  schemaVersion: 1;
@@ -53,8 +54,9 @@ export function queuePendingContinuationActivation(
53
54
  scheduleId: string,
54
55
  ): PendingContinuationActivationMarker {
55
56
  const schedule = getSchedule(controllerHome, repoId, scheduleId);
56
- if (schedule.action.operation !== EXTERNAL_CONTROLLER_WAKE_OPERATION) {
57
- throw new Error('PENDING_CONTINUATION_ACTIVATION_REQUIRES_EXTERNAL_CONTROLLER_WAKE');
57
+ if (schedule.action.operation !== EXTERNAL_CONTROLLER_WAKE_OPERATION
58
+ && schedule.action.operation !== CONTROLLER_PROGRESSION_OPERATION) {
59
+ throw new Error('PENDING_CONTINUATION_ACTIVATION_REQUIRES_WORK_CONTINUATION');
58
60
  }
59
61
  const workId = schedule.action.arguments?.work_id;
60
62
  if (typeof workId !== 'string' || !workId.trim()) {
@@ -108,7 +110,8 @@ export function applyPendingContinuationActivations(controllerHome: string): Pen
108
110
  const marker = readMarker(path);
109
111
  const schedule = getSchedule(controllerHome, marker.repoId, marker.scheduleId);
110
112
  const workId = schedule.action.arguments?.work_id;
111
- if (schedule.action.operation !== EXTERNAL_CONTROLLER_WAKE_OPERATION || workId !== marker.workId) {
113
+ if ((schedule.action.operation !== EXTERNAL_CONTROLLER_WAKE_OPERATION
114
+ && schedule.action.operation !== CONTROLLER_PROGRESSION_OPERATION) || workId !== marker.workId) {
112
115
  results.push({ ...marker, status: 'failed', reason: 'Schedule no longer targets the queued Work continuation.' });
113
116
  continue;
114
117
  }
@@ -122,7 +125,11 @@ export function applyPendingContinuationActivations(controllerHome: string): Pen
122
125
  results.push({ ...marker, status: 'already_active' });
123
126
  continue;
124
127
  }
125
- saveSchedule(controllerHome, { ...schedule, policy: { ...schedule.policy, shadowMode: false } });
128
+ saveSchedule(controllerHome, {
129
+ ...schedule,
130
+ action: { ...schedule.action, operation: CONTROLLER_PROGRESSION_OPERATION },
131
+ policy: { ...schedule.policy, shadowMode: false },
132
+ });
126
133
  rmSync(path, { force: true });
127
134
  results.push({ ...marker, status: 'activated' });
128
135
  } catch (error) {
@@ -28,6 +28,13 @@ import type {
28
28
  export type ContinuationControllerType = Exclude<ControllerType, 'human'>;
29
29
  export type WorkScheduleMode = 'continuation' | 'browser_watch' | 'browser_keepalive';
30
30
 
31
+ const CONTROLLER_PROGRESSION_OPERATION = 'controller_progression';
32
+ const LEGACY_EXTERNAL_CONTROLLER_WAKE_OPERATION = 'external_controller_wake';
33
+
34
+ function isWorkContinuationOperation(operation: string): boolean {
35
+ return operation === CONTROLLER_PROGRESSION_OPERATION || operation === LEGACY_EXTERNAL_CONTROLLER_WAKE_OPERATION;
36
+ }
37
+
31
38
  export interface WorkContinuationScheduleInput {
32
39
  workId?: string;
33
40
  scheduleMode?: WorkScheduleMode;
@@ -93,7 +100,7 @@ function activeWork(controllerHome: string, repoId: string, workId: string): Wor
93
100
  }
94
101
 
95
102
  function continuationSchedule(schedule: RepositorySchedule): RepositorySchedule {
96
- if (!['external_controller_wake', 'browser_probe'].includes(schedule.action.operation)) {
103
+ if (!isWorkContinuationOperation(schedule.action.operation) && schedule.action.operation !== 'browser_probe') {
97
104
  throw new Error(`SCHEDULE_NOT_WORK_CONTINUATION: ${schedule.scheduleId}`);
98
105
  }
99
106
  return schedule;
@@ -194,7 +201,7 @@ export function createWorkContinuationSchedule(
194
201
  }
195
202
  if (!requestedWorkId && scheduleMode === 'browser_keepalive' && controllerType !== 'chatgpt') throw new Error('STANDALONE_BROWSER_KEEPALIVE_CHATGPT_REQUIRED');
196
203
  const trigger = normalizedTrigger(input);
197
- const operation = scheduleMode === 'continuation' ? 'external_controller_wake' : 'browser_probe';
204
+ const operation = scheduleMode === 'continuation' ? CONTROLLER_PROGRESSION_OPERATION : 'browser_probe';
198
205
  const name = input.scheduleName?.trim()
199
206
  || (scheduleMode === 'browser_watch' ? `Watch external state for Work ${work!.workId}`
200
207
  : scheduleMode === 'browser_keepalive'
@@ -240,7 +247,7 @@ export function createWorkContinuationSchedule(
240
247
  // Work may legitimately observe several independent external targets.
241
248
  if (scheduleMode === 'continuation') {
242
249
  const existing = listSchedules(controllerHome, repoId)
243
- .filter((candidate) => candidate.action.operation === 'external_controller_wake')
250
+ .filter((candidate) => isWorkContinuationOperation(candidate.action.operation))
244
251
  .filter((candidate) => candidate.action.arguments?.work_id === work!.workId);
245
252
  const authoritative = [...existing].reverse().find((candidate) => candidate.enabled) ?? existing.at(-1);
246
253
  if (authoritative) {
@@ -302,7 +309,7 @@ export function listWorkContinuationSchedules(
302
309
  options: WorkContinuationScheduleListOptions = {},
303
310
  ): { schedules: RepositorySchedule[]; occurrences?: ScheduleOccurrence[] } {
304
311
  const schedules = listSchedules(controllerHome, repoId)
305
- .filter((entry) => ['external_controller_wake', 'browser_probe'].includes(entry.action.operation))
312
+ .filter((entry) => isWorkContinuationOperation(entry.action.operation) || entry.action.operation === 'browser_probe')
306
313
  .filter((entry) => !options.workId || entry.action.arguments?.work_id === options.workId);
307
314
  if (!options.includeOccurrences) return { schedules };
308
315
  const scheduleIds = new Set(schedules.map((entry) => entry.scheduleId));
@@ -342,6 +349,44 @@ export function resumeWorkContinuationSchedule(controllerHome: string, repoId: s
342
349
  const schedule = continuationSchedule(getSchedule(controllerHome, repoId, scheduleId));
343
350
  const workId = typeof schedule.action.arguments?.work_id === 'string' ? schedule.action.arguments.work_id : '';
344
351
  activeWork(controllerHome, repoId, workId);
352
+ if (schedule.action.operation === LEGACY_EXTERNAL_CONTROLLER_WAKE_OPERATION) {
353
+ const trigger = schedule.trigger;
354
+ const migrated = createWorkContinuationSchedule(controllerHome, repoId, {
355
+ workId,
356
+ scheduleName: schedule.name,
357
+ controllerType: typeof schedule.action.arguments?.controller_type === 'string'
358
+ ? schedule.action.arguments.controller_type as ContinuationControllerType
359
+ : undefined,
360
+ continuationPrompt: typeof schedule.action.arguments?.continuation_hint === 'string'
361
+ ? schedule.action.arguments.continuation_hint
362
+ : typeof schedule.action.arguments?.continuation_prompt === 'string'
363
+ ? schedule.action.arguments.continuation_prompt
364
+ : undefined,
365
+ triggerType: trigger.type,
366
+ everyMinutes: trigger.everyMinutes,
367
+ cronExpression: trigger.cronExpression,
368
+ timezone: trigger.timezone,
369
+ catchUpMinutes: trigger.catchUpMinutes,
370
+ calendarAt: trigger.calendarAt,
371
+ condition: trigger.condition,
372
+ eventName: trigger.eventName,
373
+ dependencyJobIds: trigger.dependencyJobIds,
374
+ maxFailures: schedule.policy.maxFailures,
375
+ cooldownMinutes: schedule.policy.cooldownMinutes,
376
+ dailyBudgetMinutes: schedule.policy.dailyBudgetMinutes,
377
+ shadowMode: schedule.policy.shadowMode,
378
+ backoffBaseMinutes: schedule.policy.backoffBaseMinutes,
379
+ backoffMaxMinutes: schedule.policy.backoffMaxMinutes,
380
+ stopConditions: schedule.stopConditions,
381
+ }).schedule;
382
+ return saveSchedule(controllerHome, {
383
+ ...migrated,
384
+ enabled: true,
385
+ pausedReason: undefined,
386
+ consecutiveFailures: 0,
387
+ nextEligibleAt: undefined,
388
+ });
389
+ }
345
390
  return saveSchedule(controllerHome, {
346
391
  ...schedule,
347
392
  enabled: true,
@@ -378,7 +423,7 @@ export async function triggerWorkContinuationRepositoryEvent(
378
423
  ): Promise<Array<{ scheduleId: string; occurrenceId?: string; status?: string }>> {
379
424
  const schedules = listWorkContinuationSchedules(controllerHome, repoId, { workId: input.workId }).schedules
380
425
  .filter((schedule) => schedule.enabled)
381
- .filter((schedule) => schedule.action.operation === 'external_controller_wake')
426
+ .filter((schedule) => isWorkContinuationOperation(schedule.action.operation))
382
427
  .filter((schedule) => schedule.trigger.type === 'repository-event' && schedule.trigger.eventName === eventName);
383
428
  const triggered: Array<{ scheduleId: string; occurrenceId?: string; status?: string }> = [];
384
429
  for (const schedule of schedules) {
@@ -404,7 +449,7 @@ export function eventDrivenContinuationSchedule(
404
449
  ): RepositorySchedule {
405
450
  const workSchedules = listWorkContinuationSchedules(controllerHome, repoId, { workId: input.workId }).schedules;
406
451
  const existing = workSchedules
407
- .filter((schedule) => schedule.action.operation === 'external_controller_wake')
452
+ .filter((schedule) => isWorkContinuationOperation(schedule.action.operation))
408
453
  .reverse()
409
454
  .find((schedule) => schedule.enabled)
410
455
  ?? workSchedules.at(-1);
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { createConnection } from 'node:net';
3
3
  import { workflowSupervisorSocketPath } from './paths';
4
- import type { WorkflowSupervisorAutomationStatus, WorkflowSupervisorConsumerStatus, WorkflowSupervisorContinuationProof, WorkflowSupervisorDiscoveredConversation, WorkflowSupervisorEffect, WorkflowSupervisorTask, WorkflowSupervisorTaskInput, WorkflowSupervisorTaskStall, WorkflowSupervisorTerminalState } from './types';
4
+ import type { WorkflowSupervisorConsumerStatus, WorkflowSupervisorContinuationProof, WorkflowSupervisorDiscoveredConversation, WorkflowSupervisorEffect, WorkflowSupervisorTask, WorkflowSupervisorTaskInput, WorkflowSupervisorTaskStall, WorkflowSupervisorTerminalState } from './types';
5
5
 
6
6
  interface RpcResponse<T> { id: string; ok: boolean; result?: T; error?: { code?: string; message?: string } }
7
7
 
@@ -63,12 +63,16 @@ export async function getWorkflowSupervisorTask(forgeHome: string, taskId: strin
63
63
 
64
64
  export async function reviseWorkflowSupervisorTaskObjective(forgeHome: string, input: {
65
65
  taskId: string; objective: string; expectedObjectiveSha256: string; requestId: string; authorizedBy?: string;
66
- }): Promise<{ task: WorkflowSupervisorTask; changed: boolean; deduplicated: boolean; objectiveSha256: string; refreshedEffect?: WorkflowSupervisorEffect }> {
66
+ workId?: string; requirementId?: string; expectedCompletionContractSha256?: string;
67
+ }): Promise<{ task: WorkflowSupervisorTask; changed: boolean; deduplicated: boolean; objectiveSha256: string; completionContractSha256: string; semanticBindingChanged: boolean; refreshedEffect?: WorkflowSupervisorEffect }> {
67
68
  return await rpc(forgeHome, 'task_revise_objective', {
68
69
  task_id: input.taskId,
69
70
  objective: input.objective,
70
71
  expected_objective_sha256: input.expectedObjectiveSha256,
71
72
  request_id: input.requestId,
73
+ ...(input.workId?.trim() ? { work_id: input.workId.trim() } : {}),
74
+ ...(input.requirementId?.trim() ? { requirement_id: input.requirementId.trim() } : {}),
75
+ ...(input.expectedCompletionContractSha256?.trim() ? { expected_completion_contract_sha256: input.expectedCompletionContractSha256.trim() } : {}),
72
76
  ...(input.authorizedBy?.trim() ? { authorized_by: input.authorizedBy.trim() } : {}),
73
77
  }, SUPERVISOR_RPC_MUTATION_TIMEOUT_MS);
74
78
  }
@@ -89,6 +93,13 @@ export async function listWorkflowSupervisorTasks(forgeHome: string, activeOnly
89
93
  const result = await rpc<{ tasks: WorkflowSupervisorTask[] }>(forgeHome, 'task_list', { active_only: activeOnly });
90
94
  return result.tasks;
91
95
  }
96
+ /** Explicit read-only terminal history. Native ChatGPT archive is not inferred. */
97
+ export async function listWorkflowSupervisorTerminalTasks(forgeHome: string): Promise<Array<{ task: WorkflowSupervisorTask; terminal: WorkflowSupervisorTerminalState }>> {
98
+ const result = await rpc<{ terminalTasks: Array<{ task: WorkflowSupervisorTask; terminal: WorkflowSupervisorTerminalState }> }>(
99
+ forgeHome, 'task_list', { terminal_only: true },
100
+ );
101
+ return result.terminalTasks;
102
+ }
92
103
  /** Operator-only recovery; never exposed to the browser Native Messaging host. */
93
104
  export async function recoverWorkflowSupervisorTask(forgeHome: string, input: { taskId: string; sourceEffectId?: string; requestId: string; reason: string; supersedeUnknown?: boolean; authorizedBy?: string }): Promise<{ recoveryEffect: WorkflowSupervisorEffect; action: 'retry_authorized' | 'recovery_reserved' | 'unknown_superseded' }> {
94
105
  return await rpc(forgeHome, 'task_recover', {
@@ -106,6 +117,24 @@ export async function getWorkflowSupervisorTaskStall(forgeHome: string, taskId:
106
117
  return await rpc<WorkflowSupervisorTaskStall>(forgeHome, 'task_stall', { task_id: taskId });
107
118
  }
108
119
 
120
+ /** One-way binding of a Work-owned Supervisor task to its canonical Scheduler lane. */
121
+ export async function bindWorkflowSupervisorScheduledWake(forgeHome: string, input: {
122
+ taskId: string; repoId: string; workId: string; scheduleId: string;
123
+ }): Promise<{ task: WorkflowSupervisorTask; changed: boolean; deduplicated: boolean }> {
124
+ return await rpc(forgeHome, 'task_bind_scheduled_wake', {
125
+ task_id: input.taskId, repo_id: input.repoId, work_id: input.workId, schedule_id: input.scheduleId,
126
+ }, SUPERVISOR_RPC_MUTATION_TIMEOUT_MS);
127
+ }
128
+
129
+ /** Scheduler occurrence is the idempotency identity; an unknown send outcome is never replayed. */
130
+ export async function wakeWorkflowSupervisorScheduledTask(forgeHome: string, input: {
131
+ taskId: string; scheduleId: string; occurrenceId: string;
132
+ }): Promise<{ effect: WorkflowSupervisorEffect; deduplicated: boolean }> {
133
+ return await rpc(forgeHome, 'task_wake_scheduled', {
134
+ task_id: input.taskId, schedule_id: input.scheduleId, occurrence_id: input.occurrenceId,
135
+ }, SUPERVISOR_RPC_MUTATION_TIMEOUT_MS);
136
+ }
137
+
109
138
  /**
110
139
  * Operator-only conversation migration for a standalone Supervisor task whose
111
140
  * exact conversation became permanently unusable. It is explicit and bounded by
@@ -163,18 +192,6 @@ export async function reserveWorkflowSupervisorEnrollment(forgeHome: string, tas
163
192
  }, SUPERVISOR_RPC_MUTATION_TIMEOUT_MS);
164
193
  }
165
194
 
166
- export async function recordWorkflowSupervisorAutomationReceipt(
167
- forgeHome: string,
168
- input: { taskId: string; conversationId: string; status: WorkflowSupervisorAutomationStatus; receiptId: string },
169
- ): Promise<unknown> {
170
- return await rpc(forgeHome, 'automation_receipt', {
171
- task_id: input.taskId,
172
- conversation_id: input.conversationId,
173
- automation_status: input.status,
174
- receipt_id: input.receiptId,
175
- }, SUPERVISOR_RPC_MUTATION_TIMEOUT_MS);
176
- }
177
-
178
195
  export interface WorkflowSupervisorEffectDispatchBudget {
179
196
  effectId: string;
180
197
  generations: number;