@moretea-labs/forge 1.8.0 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (695) hide show
  1. package/AGENTS.md +73 -7
  2. package/CHANGELOG.md +34 -0
  3. package/CLAUDE.md +17 -2
  4. package/CONTRIBUTING.md +3 -2
  5. package/README.en.md +24 -3
  6. package/README.md +10 -6
  7. package/README.zh-CN.md +5 -3
  8. package/THIRD_PARTY_NOTICES.md +1 -2
  9. package/adapters/chatgpt/browser-delivery-host.ts +7 -1
  10. package/adapters/chatgpt/browser-delivery-runtime.ts +531 -56
  11. package/adapters/chatgpt/controller-binding-store.ts +101 -22
  12. package/adapters/chatgpt/controller-round-host.ts +15 -25
  13. package/adapters/chatgpt/controller-round-settlement-store.ts +58 -4
  14. package/adapters/chatgpt/provider-delivery.ts +229 -2
  15. package/adapters/chatgpt/work-conversation-binding-store.ts +99 -54
  16. package/adapters/computer/chatgpt-extension-target.ts +708 -0
  17. package/adapters/computer/chrome-extension/background.js +541 -0
  18. package/adapters/computer/chrome-extension/content.js +402 -0
  19. package/adapters/computer/chrome-extension/core.js +24 -0
  20. package/adapters/computer/chrome-extension/manifest.json +32 -0
  21. package/adapters/computer/desktop-operator-discovery.ts +11 -31
  22. package/adapters/computer/desktop-operator-provider.ts +18 -6
  23. package/adapters/computer/interaction-target-authority.ts +178 -24
  24. package/adapters/mcp/access-mode.ts +18 -39
  25. package/adapters/mcp/auth.ts +2 -7
  26. package/adapters/mcp/instructions.ts +6 -5
  27. package/adapters/mcp/multi-repository.ts +10 -47
  28. package/adapters/mcp/oauth.ts +73 -0
  29. package/adapters/mcp/paths.ts +4 -1
  30. package/adapters/mcp/runtime-gateway/bounded-result-runtime.ts +62 -0
  31. package/adapters/mcp/runtime-gateway/cognition-settlement.ts +201 -0
  32. package/adapters/mcp/runtime-gateway/context-adapter.ts +490 -113
  33. package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +83 -0
  34. package/adapters/mcp/runtime-gateway/controller-authority-adapter.ts +20 -5
  35. package/adapters/mcp/runtime-gateway/core-capability-adapter.ts +258 -0
  36. package/adapters/mcp/runtime-gateway/engineering-preconditions.ts +10 -27
  37. package/adapters/mcp/runtime-gateway/engineering-tool-contract.ts +15 -0
  38. package/adapters/mcp/runtime-gateway/execution-tools.ts +14 -54
  39. package/adapters/mcp/runtime-gateway/gateway-execution-adapter.ts +2 -1
  40. package/adapters/mcp/runtime-gateway/plugin-adapter.ts +132 -47
  41. package/adapters/mcp/runtime-gateway/process-tools.ts +173 -23
  42. package/adapters/mcp/runtime-gateway/protected-computer-adapter.ts +16 -4
  43. package/adapters/mcp/runtime-gateway/recovery-adapter.ts +69 -52
  44. package/adapters/mcp/runtime-gateway/repository-compat-adapter.ts +4 -35
  45. package/adapters/mcp/runtime-gateway/result-adapter.ts +10 -4
  46. package/adapters/mcp/runtime-gateway/router.ts +19 -507
  47. package/adapters/mcp/runtime-gateway/routing-policy.ts +506 -0
  48. package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +15 -66
  49. package/adapters/mcp/runtime-gateway/runtime-readiness-observation.ts +218 -0
  50. package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +123 -178
  51. package/adapters/mcp/runtime-gateway/runtime-tools.ts +11 -5
  52. package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +37 -1
  53. package/adapters/mcp/runtime-gateway/shared-adapter.ts +20 -0
  54. package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +416 -270
  55. package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +140 -0
  56. package/adapters/mcp/runtime-gateway/work-adapter.ts +262 -3123
  57. package/adapters/mcp/runtime-gateway/work-controller-operations.ts +257 -0
  58. package/adapters/mcp/runtime-gateway/work-detail-projection.ts +60 -0
  59. package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +229 -0
  60. package/adapters/mcp/runtime-gateway/work-learning-operations.ts +145 -0
  61. package/adapters/mcp/runtime-gateway/work-list-projection.ts +38 -0
  62. package/adapters/mcp/runtime-gateway/work-plan-operations.ts +382 -0
  63. package/adapters/mcp/runtime-gateway/work-repair-adapter.ts +308 -0
  64. package/adapters/mcp/runtime-gateway/work-requirement-operations.ts +148 -0
  65. package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +320 -0
  66. package/adapters/mcp/runtime-gateway/work-workflow-operations.ts +113 -0
  67. package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +180 -0
  68. package/adapters/mcp/server.ts +91 -26
  69. package/adapters/mcp/setup.ts +5 -13
  70. package/adapters/mcp/tool-mapping/access-tools.ts +0 -6
  71. package/adapters/mcp/tool-mapping/legacy-tool-service.ts +703 -3092
  72. package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +5 -10
  73. package/adapters/mcp/tool-mapping/repository-tools.ts +170 -213
  74. package/adapters/mcp/toolset-names.ts +11 -37
  75. package/adapters/mcp/toolset.ts +19 -92
  76. package/{src/cli → adapters}/mcp/transports/AGENTS.md +5 -5
  77. package/{src/cli → adapters}/mcp/transports/CLAUDE.md +5 -5
  78. package/adapters/mcp/transports/http-observation.ts +377 -0
  79. package/adapters/mcp/transports/http-post-lifecycle.ts +19 -0
  80. package/adapters/mcp/transports/http.ts +200 -683
  81. package/adapters/mcp/transports/oauth-http.ts +322 -0
  82. package/adapters/mcp/transports/session-registry.ts +6 -0
  83. package/adapters/mcp/tunnels/openai-secure-tunnel.ts +89 -5
  84. package/adapters/mcp/types.ts +0 -6
  85. package/assets/hooks/lib/session-state.sh +0 -38
  86. package/assets/hooks/post-bash.sh +1 -1
  87. package/assets/hooks/post-tool-observer.sh +0 -5
  88. package/assets/hooks/prompt-guard.sh +0 -55
  89. package/assets/partials/04-project-structure.partial.md +1 -1
  90. package/assets/partials/07-footer.partial.md +1 -1
  91. package/assets/partials-agents/02-operating-mode.partial.md +1 -1
  92. package/assets/partials-agents/08-deep-docs.partial.md +1 -1
  93. package/assets/plugin-registry.v1.json +6 -4
  94. package/assets/reference-configs/agentic-development-flow.md +26 -1
  95. package/assets/reference-configs/external-tooling.md +0 -76
  96. package/assets/reference-configs/global-working-rules.md +19 -0
  97. package/assets/reference-configs/release-deploy.md +1 -1
  98. package/assets/templates/helpers/architecture-queue.sh +1 -1
  99. package/assets/templates/helpers/check-agent-tooling.sh +1 -327
  100. package/assets/templates/helpers/check-task-workflow.sh +1 -1
  101. package/assets/templates/helpers/ensure-task-workflow.sh +0 -14
  102. package/assets/templates/helpers/migrate-project-template.sh +2 -2
  103. package/assets/workflow-contract.v1.json +0 -2
  104. package/docs/README.md +5 -2
  105. package/docs/architecture/CURRENT.md +205 -51
  106. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +14 -5
  107. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +6 -3
  108. package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +42 -0
  109. package/docs/architecture/decisions/20260907-assistant-learning-loop.md +2 -0
  110. package/docs/architecture/decisions/20260907-controller-round-transition-policy.md +19 -4
  111. package/docs/architecture/decisions/20260916-workflow-supervisor-authority.md +249 -0
  112. package/docs/architecture/decisions/20260917-cognitive-memory-foundation.md +82 -0
  113. package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +234 -0
  114. package/docs/architecture/decisions/20260919-autonomous-work-liveness.md +62 -0
  115. package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +74 -0
  116. package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +342 -0
  117. package/docs/architecture/decisions/20261005-forge-v3-desktop-client-boundary.md +77 -0
  118. package/docs/architecture/decisions/20261009-exact-code-context-discovery.md +19 -0
  119. package/docs/architecture/decisions/20261009-generated-xcode-cache-retention.md +37 -0
  120. package/docs/architecture/decisions/20261011-autonomous-continuation-convergence.md +597 -0
  121. package/docs/architecture/decisions/20261017-edit-session-finalization-reconciliation.md +17 -0
  122. package/docs/architecture/decisions/20261019-retire-host-skills.md +16 -0
  123. package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +1 -1
  124. package/docs/architecture/domains/public-surface.md +3 -5
  125. package/docs/architecture/domains/verification.md +6 -5
  126. package/docs/architecture/domains/workflow-engine.md +6 -0
  127. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +3 -3
  128. package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +4 -4
  129. package/docs/architecture/reviews/20261015-code-search-runtime-acceptance.md +75 -0
  130. package/docs/forge-plugin-management.md +16 -4
  131. package/docs/operations/controller-browser-plugin.md +23 -0
  132. package/docs/operations/controller-performance-and-502.md +17 -0
  133. package/docs/operations/controller-reliability-runbook.md +2 -2
  134. package/docs/operations/features.md +6 -7
  135. package/docs/operations/features.zh-CN.md +1 -1
  136. package/docs/operations/github-repository.md +4 -4
  137. package/docs/operations/github-semantic-release-projection.md +20 -0
  138. package/docs/operations/homebrew.md +2 -2
  139. package/docs/operations/mcp-tool-exposure.md +13 -52
  140. package/docs/operations/releasing.md +2 -0
  141. package/docs/operations/runtime-performance-diagnostics.md +8 -0
  142. package/docs/operations/standalone-disaster-recovery.md +15 -7
  143. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  144. package/docs/tutorials/03-first-repository-task.md +2 -0
  145. package/docs/wiki/Computer-and-Plugins.md +78 -0
  146. package/docs/wiki/Computer-and-Plugins.zh-CN.md +88 -0
  147. package/docs/wiki/Home.md +16 -2
  148. package/docs/wiki/Implementation.md +25 -13
  149. package/docs/wiki/Installation.md +28 -32
  150. package/docs/wiki/Integrations.md +4 -0
  151. package/docs/wiki/Quick-Start.md +35 -22
  152. package/docs/wiki/Releases-and-Upgrades.md +15 -25
  153. package/docs/wiki/_Sidebar.md +5 -1
  154. package/package.json +25 -25
  155. package/packages/kernel/cognition/api/index.ts +6 -0
  156. package/packages/kernel/cognition/application/activation-service.ts +451 -0
  157. package/packages/kernel/cognition/application/consolidation-service.ts +185 -0
  158. package/packages/kernel/cognition/application/legacy-experience-adapter.ts +38 -0
  159. package/packages/kernel/cognition/application/memory-service.ts +64 -0
  160. package/packages/kernel/cognition/domain/learning.ts +106 -0
  161. package/packages/kernel/cognition/domain/memory.ts +254 -0
  162. package/packages/kernel/controller/api/index.ts +1 -0
  163. package/packages/kernel/controller/application/continuation-service.ts +281 -0
  164. package/packages/kernel/controller/application/controller-round-service.ts +12 -0
  165. package/packages/kernel/controller/domain/controller-round-transition-policy.ts +242 -21
  166. package/packages/kernel/controller/domain/controller-round.ts +4 -1
  167. package/packages/kernel/controller/domain/execution-quality.ts +3 -1
  168. package/packages/kernel/controller/infrastructure/controller-round-store.ts +689 -103
  169. package/packages/kernel/controller/infrastructure/controller-session-store.ts +348 -97
  170. package/packages/kernel/controller/ports/controller-host.ts +2 -0
  171. package/packages/kernel/identity/api/index.ts +10 -0
  172. package/packages/kernel/identity/domain/activity-projection.ts +43 -0
  173. package/packages/kernel/identity/domain/capability-broker.ts +60 -0
  174. package/packages/kernel/identity/domain/grant.ts +72 -0
  175. package/packages/kernel/identity/domain/owned-resource.ts +116 -0
  176. package/packages/kernel/identity/domain/user-request.ts +93 -0
  177. package/packages/kernel/identity/infrastructure/activity-projection-store.ts +87 -0
  178. package/packages/kernel/identity/infrastructure/capability-broker.ts +81 -0
  179. package/packages/kernel/identity/infrastructure/grant-store.ts +284 -0
  180. package/packages/kernel/identity/infrastructure/owned-resource-store.ts +259 -0
  181. package/packages/kernel/identity/infrastructure/user-request-store.ts +182 -0
  182. package/packages/kernel/scheduler/api/index.ts +0 -2
  183. package/packages/kernel/scheduler/application/eligibility.ts +4 -1
  184. package/packages/kernel/scheduler/application/schedule-service.ts +3 -0
  185. package/packages/kernel/scheduler/application/settlement.ts +0 -15
  186. package/packages/kernel/scheduler/domain/schedule.ts +87 -0
  187. package/packages/kernel/scheduler/infrastructure/schedule-store.ts +166 -143
  188. package/packages/kernel/work/api/index.ts +3 -2
  189. package/packages/kernel/work/application/work-service.ts +18 -42
  190. package/packages/kernel/work/domain/current-task-lineage.ts +104 -0
  191. package/packages/kernel/work/domain/engineering-design.ts +10 -5
  192. package/packages/kernel/work/domain/engineering-profile.ts +8 -12
  193. package/packages/kernel/work/domain/evaluation-promotion-receipt.ts +186 -0
  194. package/packages/kernel/work/domain/execution-concurrency.ts +19 -26
  195. package/packages/kernel/work/domain/execution-snapshot.ts +0 -31
  196. package/packages/kernel/work/domain/implementation-review.ts +14 -331
  197. package/packages/kernel/work/domain/objective-graph.ts +119 -0
  198. package/packages/kernel/work/domain/project-engineering-contract.ts +23 -1
  199. package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
  200. package/packages/kernel/work/domain/state-machine.ts +34 -65
  201. package/packages/kernel/work/domain/types.ts +57 -51
  202. package/packages/kernel/work/infrastructure/work-contract-store.ts +847 -729
  203. package/packages/kernel/work/ports/work-contract-store.ts +5 -2
  204. package/packages/plugin-runtime/browser/provider-registry.ts +12 -36
  205. package/packages/plugin-runtime/browser/session-authority.ts +2 -1
  206. package/packages/plugin-runtime/computer/chatgpt-identity.ts +24 -0
  207. package/packages/plugin-runtime/computer/conversation-target.ts +153 -0
  208. package/packages/plugin-runtime/computer/index.ts +2 -0
  209. package/packages/plugin-runtime/computer/target-authority.ts +26 -1
  210. package/packages/plugin-runtime/external/unix-jsonl-transport.ts +142 -18
  211. package/packages/protocols/computer/contract.ts +27 -1
  212. package/scripts/activate-source-baseline.ts +7 -9
  213. package/scripts/assemble-template.ts +11 -72
  214. package/scripts/canary-controller-semantic-wait.ts +12 -24
  215. package/scripts/check-mcp-compatibility.ts +94 -201
  216. package/scripts/check-package-identity.mjs +0 -5
  217. package/scripts/check-runtime-architecture.mjs +921 -239
  218. package/scripts/forge-local-recovery-helper.mjs +93 -15
  219. package/scripts/init-project.sh +0 -46
  220. package/scripts/install-ios-hid-toolchain.sh +60 -0
  221. package/scripts/install-standalone-recovery.ts +1 -1
  222. package/scripts/lib/project-init-lib.sh +6 -3
  223. package/scripts/migrate-windows-wsl-controller-home.ts +2 -0
  224. package/scripts/open-source-audit-allowlist.txt +1 -3
  225. package/scripts/run-bounded-command.mjs +5 -0
  226. package/scripts/run-governed-gate.ts +2 -1
  227. package/scripts/run-mobile-monitor-bridge.ts +0 -1
  228. package/scripts/smoke-control-plane-failure-domain.ts +94 -0
  229. package/scripts/smoke-ios-physical-device.ts +38 -10
  230. package/scripts/smoke-mcp-http-runtime.ts +12 -8
  231. package/scripts/smoke-mcp-tool-surface.ts +27 -41
  232. package/scripts/smoke-runtime-recovery.ts +12 -43
  233. package/scripts/smoke-schedule-engine.ts +23 -15
  234. package/scripts/smoke-unattended-transport-recovery.ts +223 -0
  235. package/scripts/smoke-user-request-continuation-restart.ts +232 -0
  236. package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +95 -0
  237. package/scripts/smoke-workflow-supervisor-process-restart.ts +276 -0
  238. package/scripts/smoke-workflow-supervisor-restart-recovery.ts +150 -0
  239. package/scripts/smoke-workflow-supervisor.ts +103 -0
  240. package/scripts/sync-generated-authority.mjs +0 -1
  241. package/scripts/verify-forge-runtime.sh +2 -1
  242. package/src/cli/agent-jobs/executor-health.ts +12 -35
  243. package/src/cli/agent-jobs/integration.ts +20 -51
  244. package/src/cli/agent-jobs/job-manager.ts +7 -3
  245. package/src/cli/chatgpt-browser/file-policy.ts +2 -2
  246. package/src/cli/commands/brain-assistant.ts +27 -13
  247. package/src/cli/commands/brain.ts +13 -15
  248. package/src/cli/commands/chatgpt.ts +1 -163
  249. package/src/cli/commands/doctor.ts +1 -143
  250. package/src/cli/commands/global-runtime.ts +3 -196
  251. package/src/cli/commands/init-hook.ts +6 -23
  252. package/src/cli/commands/init.ts +3 -252
  253. package/src/cli/commands/mcp.ts +0 -5
  254. package/src/cli/commands/prompt-guard-decision.ts +0 -2
  255. package/src/cli/commands/recovery.ts +137 -135
  256. package/src/cli/commands/repository.ts +17 -1
  257. package/src/cli/commands/runtime.ts +3 -3
  258. package/src/cli/commands/setup-execution.ts +41 -0
  259. package/src/cli/commands/setup-profile.ts +8 -0
  260. package/src/cli/controller/check-runner.ts +13 -2
  261. package/src/cli/controller/completion-backlog.ts +3 -3
  262. package/src/cli/controller/completion-orchestrator.ts +5 -44
  263. package/src/cli/controller/completion-target.ts +3 -72
  264. package/src/cli/controller/composite-operations.ts +1 -1
  265. package/src/cli/controller/context/instruction-context.ts +1 -1
  266. package/src/cli/controller/context/known-paths.ts +2 -2
  267. package/src/cli/controller/context/query-planning.ts +11 -4
  268. package/src/cli/controller/context/rule-resolution.ts +2 -2
  269. package/src/cli/controller/context/source-materializer.ts +83 -102
  270. package/src/cli/controller/context/structural-provider.ts +84 -0
  271. package/src/cli/controller/context/structural.ts +6 -6
  272. package/src/cli/controller/context/types.ts +8 -9
  273. package/src/cli/controller/context-pack.ts +107 -84
  274. package/src/cli/controller/execution-completion.ts +4 -7
  275. package/src/cli/controller/execution-policy.ts +36 -153
  276. package/src/cli/controller/governance.ts +2 -3
  277. package/src/cli/controller/issue-store.ts +4 -99
  278. package/src/cli/controller/launch-agents.ts +59 -18
  279. package/src/cli/controller/operational-plan.ts +25 -52
  280. package/src/cli/controller/runtime-config.ts +5 -1
  281. package/src/cli/controller/stuck-state-migration.ts +2 -3
  282. package/src/cli/controller/systemd-user.ts +42 -10
  283. package/src/cli/editing/edit-session.ts +18 -12
  284. package/src/cli/github/github.ts +4 -4
  285. package/src/cli/github/semantic-projection.ts +174 -0
  286. package/src/cli/hook/prompt-intents.ts +0 -9
  287. package/src/cli/index.ts +1 -43
  288. package/src/cli/local-bridge/connector-freshness.ts +1 -12
  289. package/src/cli/local-bridge/console-automations.ts +19 -14
  290. package/src/cli/local-bridge/console-view-models.ts +4 -13
  291. package/src/cli/local-bridge/facade-api.ts +64 -193
  292. package/src/cli/local-bridge/server.ts +24 -147
  293. package/src/cli/local-bridge/types.ts +0 -1
  294. package/src/cli/mcp/oauth-client-retirement.ts +74 -0
  295. package/src/cli/repositories/command-classifier.ts +0 -9
  296. package/src/cli/repositories/command-executor.ts +29 -23
  297. package/src/cli/repositories/command-scope.ts +37 -18
  298. package/src/cli/repositories/controller-home.ts +48 -0
  299. package/src/cli/repositories/ephemeral-workspace.ts +20 -30
  300. package/src/cli/repositories/git-command-executor.ts +2 -2
  301. package/src/cli/repositories/locks.ts +20 -0
  302. package/src/cli/repositories/registry.ts +79 -12
  303. package/src/cli/repositories/safe-patch.ts +2 -12
  304. package/src/cli/repositories/structured-git.ts +101 -5
  305. package/src/cli/repository/inspector.ts +185 -343
  306. package/src/cli/repository/session-cache.ts +92 -6
  307. package/src/effects/process-runner.ts +17 -1
  308. package/src/runtime/context/assistant-context.ts +20 -7
  309. package/src/runtime/context/assistant-work-context.ts +89 -27
  310. package/src/runtime/context/automatic-learning.ts +565 -0
  311. package/src/runtime/context/cognitive-attention.ts +173 -0
  312. package/src/runtime/context/cognitive-skill-canary.ts +158 -0
  313. package/src/runtime/context/context-closure.ts +46 -9
  314. package/src/runtime/context/context-pack-process.ts +103 -0
  315. package/src/runtime/context/context-plane.ts +1 -9
  316. package/src/runtime/context/direct-controller-learning.ts +298 -0
  317. package/src/runtime/context/generic-lsp-provider.ts +1 -1
  318. package/src/runtime/context/semantic-navigation-contract.ts +16 -0
  319. package/src/runtime/context/semantic-navigation.ts +7 -35
  320. package/src/runtime/context/typescript-navigation-process.ts +145 -0
  321. package/src/runtime/context/typescript-navigation.ts +89 -3
  322. package/src/runtime/control-plane/cleanup-artifact-retention.ts +215 -12
  323. package/src/runtime/control-plane/concurrency/work-execution-concurrency.ts +9 -47
  324. package/src/runtime/control-plane/execution/controller-authority-recovery.ts +34 -4
  325. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +29 -20
  326. package/src/runtime/control-plane/execution/execution-identity.ts +23 -3
  327. package/src/runtime/control-plane/execution/repository-work-attribution.ts +7 -30
  328. package/src/runtime/control-plane/execution/retained-work-resume.ts +206 -199
  329. package/src/runtime/control-plane/execution/session-store.ts +31 -1
  330. package/src/runtime/control-plane/execution/validation.ts +6 -8
  331. package/src/runtime/control-plane/execution/work-authority-reconciler.ts +13 -42
  332. package/src/runtime/control-plane/execution/work-check-evidence.ts +137 -0
  333. package/src/runtime/control-plane/execution/work-completion-authority.ts +30 -85
  334. package/src/runtime/control-plane/execution/work-delivery-service.ts +616 -0
  335. package/src/runtime/control-plane/execution/work-evidence-policy.ts +12 -50
  336. package/src/runtime/control-plane/execution/work-execution-support.ts +50 -149
  337. package/src/runtime/control-plane/execution/work-handle-authority.ts +197 -110
  338. package/src/runtime/control-plane/execution/work-handle-store.ts +72 -44
  339. package/src/runtime/control-plane/execution/work-head-settlement.ts +53 -20
  340. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +52 -29
  341. package/src/runtime/control-plane/execution/work-operation-service.ts +60 -34
  342. package/src/runtime/control-plane/execution/work-preparation-service.ts +231 -267
  343. package/src/runtime/control-plane/execution/work-process-evidence.ts +2 -2
  344. package/src/runtime/control-plane/execution/work-remote-delivery.ts +2 -3
  345. package/src/runtime/control-plane/execution/work-revision-diff.ts +39 -0
  346. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +201 -199
  347. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +33 -85
  348. package/src/runtime/control-plane/execution/work-verification-service.ts +406 -50
  349. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +6 -2
  350. package/src/runtime/control-plane/facade/capability-registry.ts +364 -37
  351. package/src/runtime/control-plane/facade/check-normalization.ts +3 -9
  352. package/src/runtime/control-plane/facade/codex-delegation.ts +10 -11
  353. package/src/runtime/control-plane/facade/controller-connection.ts +135 -0
  354. package/src/runtime/control-plane/facade/goal-workloop-access.ts +1 -20
  355. package/src/runtime/control-plane/facade/goal-workloop.ts +281 -1753
  356. package/src/runtime/control-plane/facade/handoff-inbox-application.ts +12 -25
  357. package/src/runtime/control-plane/facade/handoff-inbox-store.ts +256 -52
  358. package/src/runtime/control-plane/facade/index.ts +0 -1
  359. package/src/runtime/control-plane/facade/operation-digest.ts +12 -15
  360. package/src/runtime/control-plane/facade/plan-contract-store.ts +451 -991
  361. package/src/runtime/control-plane/facade/policy-gate.ts +0 -23
  362. package/src/runtime/control-plane/facade/repository-work-admission.ts +12 -22
  363. package/src/runtime/control-plane/facade/requirement-authority.ts +102 -199
  364. package/src/runtime/control-plane/facade/requirement-board.ts +3 -3
  365. package/src/runtime/control-plane/facade/rh-work-operation-contract.ts +51 -14
  366. package/src/runtime/control-plane/facade/self-healing-loop.ts +18 -53
  367. package/src/runtime/control-plane/facade/suggested-actions.ts +15 -18
  368. package/src/runtime/control-plane/facade/types.ts +49 -108
  369. package/src/runtime/control-plane/facade/work-continuation.ts +9 -9
  370. package/src/runtime/control-plane/generated-cache-retention.ts +206 -59
  371. package/src/runtime/control-plane/global-scheduler/autonomous-continuation.ts +248 -0
  372. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +1 -1
  373. package/src/runtime/control-plane/global-scheduler/maintenance.ts +256 -117
  374. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -12
  375. package/src/runtime/control-plane/global-scheduler/scheduler.ts +66 -40
  376. package/src/runtime/control-plane/global-scheduler/wake-signal.ts +42 -2
  377. package/src/runtime/control-plane/governance/access-policy.ts +19 -2
  378. package/src/runtime/control-plane/governance/authorization.ts +20 -3
  379. package/src/runtime/control-plane/governance/external-effects.ts +1 -1
  380. package/src/runtime/control-plane/launcher/chatgpt-round-continuation.ts +63 -300
  381. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +29 -300
  382. package/src/runtime/control-plane/launcher/local-conversation-controller.ts +238 -0
  383. package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +5 -2
  384. package/src/runtime/control-plane/launcher/thin-launcher.ts +96 -240
  385. package/src/runtime/control-plane/persistence/cognition-store.ts +820 -0
  386. package/src/runtime/control-plane/persistence/experience-store.ts +162 -29
  387. package/src/runtime/control-plane/persistence/requirement-store.ts +216 -74
  388. package/src/runtime/control-plane/persistence/sqlite-store.ts +113 -6
  389. package/src/runtime/control-plane/release-retention.ts +135 -47
  390. package/src/runtime/control-plane/repository-namespace-retention.ts +2 -2
  391. package/src/runtime/control-plane/runtime-cleanup.ts +295 -107
  392. package/src/runtime/control-plane/workspace/project-onboarding.ts +80 -0
  393. package/src/runtime/control-plane/workspace/semantic-sync-service.ts +19 -5
  394. package/src/runtime/control-plane/workspace/workspace-store.ts +169 -0
  395. package/src/runtime/diagnostics/incident-repair.ts +313 -146
  396. package/src/runtime/diagnostics/mcp-timing.ts +114 -0
  397. package/src/runtime/evidence/artifact-store.ts +158 -40
  398. package/src/runtime/evidence/evidence-store.ts +112 -8
  399. package/src/runtime/evidence/output-handle-index.ts +225 -0
  400. package/src/runtime/evidence/result-store.ts +158 -44
  401. package/src/runtime/evidence/sensitive-output.ts +22 -2
  402. package/src/runtime/execution/jobs/child-reference.ts +11 -0
  403. package/src/runtime/execution/jobs/receipt-recovery.ts +1 -11
  404. package/src/runtime/execution/jobs/store.ts +3 -4
  405. package/src/runtime/execution/managed-workspace.ts +57 -0
  406. package/src/runtime/execution/process-runtime/check-facade.ts +1 -1
  407. package/src/runtime/execution/process-runtime/check-result-retention.ts +2 -2
  408. package/src/runtime/execution/process-runtime/check-result.ts +105 -1
  409. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +18 -8
  410. package/src/runtime/execution/process-runtime/command-facade.ts +82 -30
  411. package/src/runtime/execution/process-runtime/gc.ts +15 -2
  412. package/src/runtime/execution/process-runtime/handle-index.ts +176 -0
  413. package/src/runtime/execution/process-runtime/host-command-lane.ts +139 -0
  414. package/src/runtime/execution/process-runtime/index.ts +3 -0
  415. package/src/runtime/execution/process-runtime/lightweight-managed.ts +251 -72
  416. package/src/runtime/execution/process-runtime/persisted-check.ts +3 -3
  417. package/src/runtime/execution/process-runtime/process-runner-entry.ts +47 -5
  418. package/src/runtime/execution/process-runtime/process-scope.ts +160 -0
  419. package/src/runtime/execution/process-runtime/resource-claims.ts +65 -61
  420. package/src/runtime/execution/process-runtime/runtime.ts +55 -18
  421. package/src/runtime/execution/process-runtime/store.ts +30 -2
  422. package/src/runtime/execution/process-runtime/types.ts +6 -1
  423. package/src/runtime/execution/thin-harness/execution-router.ts +16 -16
  424. package/src/runtime/execution/thin-harness/fast-executor.ts +2 -2
  425. package/src/runtime/execution/thin-harness/search-worker.ts +2 -2
  426. package/src/runtime/execution/workers/executor.ts +3 -8
  427. package/src/runtime/maintenance/cleanup.ts +41 -5
  428. package/src/runtime/platform/service-inventory.ts +130 -0
  429. package/src/runtime/platform/service-manager.ts +8 -0
  430. package/src/runtime/plugins/action-application.ts +97 -29
  431. package/src/runtime/plugins/app-store-connect-adapter.ts +290 -18
  432. package/src/runtime/plugins/browser-adapter.ts +1124 -333
  433. package/src/runtime/plugins/browser-automation-service.ts +23 -3
  434. package/src/runtime/plugins/browser-macos-bridge.ts +431 -137
  435. package/src/runtime/plugins/browser-manifest-surface.ts +47 -12
  436. package/src/runtime/plugins/browser-native-session-service.ts +63 -13
  437. package/src/runtime/plugins/browser-registration.ts +18 -0
  438. package/src/runtime/plugins/browser-runtime.ts +40 -36
  439. package/src/runtime/plugins/browser-session-authority.ts +2 -2
  440. package/src/runtime/plugins/browser-session-store.ts +46 -76
  441. package/src/runtime/plugins/capability-authorization-grants.ts +258 -62
  442. package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +228 -0
  443. package/src/runtime/plugins/computer-registration.ts +298 -20
  444. package/src/runtime/plugins/desktop-operator-external-policy.ts +36 -4
  445. package/src/runtime/plugins/desktop-operator-registration.ts +12 -4
  446. package/src/runtime/plugins/external-adapter.ts +42 -6
  447. package/src/runtime/plugins/external-registration.ts +21 -1
  448. package/src/runtime/plugins/github-adapter.ts +30 -3
  449. package/src/runtime/plugins/gmail-adapter.ts +189 -3
  450. package/src/runtime/plugins/google-calendar-adapter.ts +2 -2
  451. package/src/runtime/plugins/google-shared.ts +24 -12
  452. package/src/runtime/plugins/google-tasks-adapter.ts +2 -2
  453. package/src/runtime/plugins/ios/remote-xpc-hid.ts +70 -30
  454. package/src/runtime/plugins/ios-adapter.ts +46 -41
  455. package/src/runtime/plugins/ios-agent-device.ts +54 -4
  456. package/src/runtime/plugins/ios-device-ownership.ts +103 -0
  457. package/src/runtime/plugins/ios-physical-device.ts +497 -79
  458. package/src/runtime/plugins/lightweight-action.ts +208 -2
  459. package/src/runtime/plugins/local-system-adapter.ts +12 -100
  460. package/src/runtime/plugins/managed-process-adapter.ts +12 -5
  461. package/src/runtime/plugins/plugin-action-sidecar.ts +17 -13
  462. package/src/runtime/plugins/plugin-management-adapter.ts +1 -1
  463. package/src/runtime/plugins/store.ts +682 -435
  464. package/src/runtime/plugins/types.ts +24 -0
  465. package/src/runtime/plugins/xiaohongshu-publish.ts +16 -0
  466. package/src/runtime/projections/git-status-sampler.ts +2 -19
  467. package/src/runtime/projections/invalidation.ts +7 -3
  468. package/src/runtime/projections/materialized-view.ts +80 -36
  469. package/src/runtime/recovery/actions.ts +4 -4
  470. package/src/runtime/recovery/capability-action-application.ts +46 -45
  471. package/src/runtime/recovery/lifecycle-inventory.ts +30 -40
  472. package/src/runtime/recovery/maintenance-executor.ts +374 -123
  473. package/src/runtime/recovery/probe.ts +2 -2
  474. package/src/runtime/recovery/store.ts +29 -11
  475. package/src/runtime/release/release-coordinator.ts +312 -0
  476. package/src/runtime/release/release-session.ts +716 -0
  477. package/src/runtime/release/release-state-migration.ts +25 -0
  478. package/src/runtime/resources/leases/store.ts +56 -25
  479. package/src/runtime/root/computer-composition.ts +191 -6
  480. package/src/runtime/root/controller-progression-composition.ts +254 -0
  481. package/src/runtime/root/controller-round-composition.ts +20 -1
  482. package/src/runtime/root/deployment-topology.ts +106 -0
  483. package/src/runtime/root/entry.ts +4 -3
  484. package/src/runtime/root/known-good-recovery.ts +184 -0
  485. package/src/runtime/root/local-bridge.ts +0 -1
  486. package/src/runtime/root/mcp-transport.ts +111 -18
  487. package/src/runtime/root/ownership.ts +261 -9
  488. package/src/runtime/root/package-connector-service.ts +227 -29
  489. package/src/runtime/root/package-runtime-release.ts +117 -30
  490. package/src/runtime/root/package-runtime-service.ts +24 -24
  491. package/src/runtime/root/release-execution-canary.ts +57 -12
  492. package/src/runtime/root/release-loader.ts +90 -8
  493. package/src/runtime/root/release-manifest.ts +80 -40
  494. package/src/runtime/root/release-materialize.ts +372 -106
  495. package/src/runtime/root/release-store.ts +395 -36
  496. package/src/runtime/root/runtime-lane.ts +191 -0
  497. package/src/runtime/root/runtime.ts +160 -29
  498. package/src/runtime/root/scheduled-controller-composition.ts +29 -3
  499. package/src/runtime/root/service-runner.ts +2 -2
  500. package/src/runtime/root/service.ts +126 -44
  501. package/src/runtime/root/status.ts +48 -0
  502. package/src/runtime/root/types.ts +32 -9
  503. package/src/runtime/root/workflow-supervisor-composition.ts +681 -0
  504. package/src/runtime/root/workflow-supervisor-runtime.ts +564 -0
  505. package/src/runtime/root/write-fence.ts +50 -2
  506. package/src/runtime/safe-tooling/google-credential-store.ts +67 -22
  507. package/src/runtime/safe-tooling/google-oauth-broker.ts +25 -7
  508. package/src/runtime/safe-tooling/ios-development.ts +22 -8
  509. package/src/runtime/safe-tooling/job-summary.ts +1 -1
  510. package/src/runtime/safe-tooling/workspace-auth.ts +5 -2
  511. package/src/runtime/shared/atomic-text-file.ts +21 -0
  512. package/src/runtime/shared/bounded-child-supervisor.ts +9 -1
  513. package/src/runtime/shared/json-files.ts +8 -3
  514. package/src/runtime/shared/process-environment.ts +20 -9
  515. package/src/runtime/shared/response-budget.ts +1 -3
  516. package/src/runtime/standalone-recovery/controller-home-migration.ts +16 -24
  517. package/src/runtime/standalone-recovery/core.ts +3792 -417
  518. package/src/runtime/standalone-recovery/entry.ts +507 -45
  519. package/src/runtime/standalone-recovery/installer.ts +262 -148
  520. package/src/runtime/standalone-recovery/mcp-server.ts +29 -193
  521. package/src/runtime/standalone-recovery/operation-lock.ts +12 -2
  522. package/src/runtime/standalone-recovery/performance.ts +54 -16
  523. package/src/runtime/standalone-recovery/release.ts +53 -5
  524. package/src/runtime/standalone-recovery/service-labels.ts +5 -2
  525. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +5 -2
  526. package/src/runtime/workflow/schedules/browser-probe.ts +17 -6
  527. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +11 -3
  528. package/src/runtime/workflow/schedules/engine.ts +166 -178
  529. package/src/runtime/workflow/schedules/pending-activation.ts +11 -4
  530. package/src/runtime/workflow/schedules/work-continuation.ts +95 -11
  531. package/src/runtime/workflows/runtime.ts +15 -19
  532. package/src/testing/test-governance.ts +5 -1
  533. package/supervisor/chatgpt-conversation.ts +23 -0
  534. package/supervisor/client.ts +218 -0
  535. package/supervisor/control-plane.ts +963 -0
  536. package/supervisor/entry.ts +15 -0
  537. package/supervisor/forge-validators.ts +147 -0
  538. package/supervisor/index.ts +5 -0
  539. package/supervisor/native-browser-adapter.ts +1151 -0
  540. package/supervisor/native-messaging/host.ts +58 -0
  541. package/supervisor/native-messaging/manifest.ts +26 -0
  542. package/supervisor/native-messaging/protocol.ts +27 -0
  543. package/supervisor/paths.ts +18 -0
  544. package/supervisor/protocol.ts +250 -0
  545. package/supervisor/server.ts +412 -0
  546. package/supervisor/store.ts +1870 -0
  547. package/supervisor/tsconfig.json +8 -0
  548. package/supervisor/types.ts +261 -0
  549. package/SKILL.md +0 -427
  550. package/adapters/chatgpt/controller-host.ts +0 -119
  551. package/adapters/mcp/controller-round-compatibility.ts +0 -184
  552. package/adapters/mcp/frozen-client-semantic-compatibility.ts +0 -292
  553. package/adapters/mcp/runtime-gateway/work-compat-adapter.ts +0 -502
  554. package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +0 -1
  555. package/adapters/mcp/toolset-selection.ts +0 -41
  556. package/agents/openai.yaml +0 -4
  557. package/assets/skill-hooks.json +0 -41
  558. package/assets/skill-version.json +0 -189
  559. package/assets/skills/claude-review/SKILL.md +0 -231
  560. package/assets/skills/codex-review/SKILL.md +0 -103
  561. package/assets/templates/helpers/check-skill-version.ts +0 -193
  562. package/packages/kernel/progression/api/index.ts +0 -2
  563. package/packages/kernel/progression/application/projector.ts +0 -211
  564. package/packages/kernel/progression/domain/types.ts +0 -116
  565. package/packages/kernel/progression/index.ts +0 -1
  566. package/packages/kernel/scheduler/application/continuation-service.ts +0 -212
  567. package/packages/kernel/scheduler/domain/continuation.ts +0 -38
  568. package/packages/kernel/scheduler/infrastructure/continuation-dispatch-store.ts +0 -38
  569. package/packages/kernel/work/domain/admission-policy.ts +0 -34
  570. package/scripts/benchmark-background-check-overlap.ts +0 -228
  571. package/scripts/benchmark-computer-hotpath.ts +0 -255
  572. package/scripts/benchmark-controller-hotpath.ts +0 -212
  573. package/scripts/benchmark-controller-round2.ts +0 -191
  574. package/scripts/benchmark-ios-agent-device.ts +0 -511
  575. package/scripts/benchmark-mcp-host.sh +0 -117
  576. package/scripts/benchmark-mcp-transport.ts +0 -169
  577. package/scripts/benchmark-operational-memory-activation.ts +0 -235
  578. package/scripts/benchmark-operational-shadow.ts +0 -28
  579. package/scripts/benchmark-quality-harness.ts +0 -146
  580. package/scripts/benchmark-route-session-concurrency.ts +0 -915
  581. package/scripts/benchmark-thin-harness-gateway-ab.ts +0 -632
  582. package/scripts/benchmark-thin-harness-mcp-e2e.ts +0 -498
  583. package/scripts/benchmark-thin-harness.ts +0 -251
  584. package/scripts/benchmark-typescript-navigation.ts +0 -186
  585. package/scripts/benchmark-work-efficiency.ts +0 -334
  586. package/scripts/build-controller-ui.ts +0 -28
  587. package/scripts/check-npm-release.sh +0 -32
  588. package/scripts/check-public-release-surface.sh +0 -19
  589. package/scripts/check-release-published.sh +0 -57
  590. package/scripts/check-release-readiness.sh +0 -37
  591. package/scripts/check-release-version.mjs +0 -74
  592. package/scripts/check-skill-version.ts +0 -5
  593. package/scripts/check-tarball-install-smoke.sh +0 -150
  594. package/scripts/ensure-codegraph.sh +0 -16
  595. package/scripts/package-source-archive.sh +0 -147
  596. package/scripts/public-release-files.txt +0 -39
  597. package/scripts/public-release.ts +0 -174
  598. package/scripts/publish-release-tarball.sh +0 -27
  599. package/scripts/route-nl-vs-ts-eval.ts +0 -679
  600. package/scripts/run-skill-hook.ts +0 -244
  601. package/scripts/stage-runtime-release.ts +0 -44
  602. package/scripts/sync-codex-installed-copies.sh +0 -182
  603. package/src/cli/commands/tools.ts +0 -167
  604. package/src/cli/controller/progress.ts +0 -656
  605. package/src/cli/controller/work-mode.ts +0 -181
  606. package/src/cli/local-bridge/console-assets.ts +0 -17
  607. package/src/cli/local-bridge/dashboard.ts +0 -17
  608. package/src/cli/local-bridge/frontend/src/App.tsx +0 -10
  609. package/src/cli/local-bridge/frontend/src/components/Button.tsx +0 -1
  610. package/src/cli/local-bridge/frontend/src/components/DetailPane.tsx +0 -3
  611. package/src/cli/local-bridge/frontend/src/components/SearchField.tsx +0 -1
  612. package/src/cli/local-bridge/frontend/src/components/SectionHeader.tsx +0 -1
  613. package/src/cli/local-bridge/frontend/src/components/Segmented.tsx +0 -1
  614. package/src/cli/local-bridge/frontend/src/components/StatusText.tsx +0 -3
  615. package/src/cli/local-bridge/frontend/src/design/icons.tsx +0 -11
  616. package/src/cli/local-bridge/frontend/src/design/tokens.css +0 -8
  617. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +0 -153
  618. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +0 -37
  619. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +0 -183
  620. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +0 -2
  621. package/src/cli/local-bridge/frontend/src/features/system/SystemView.tsx +0 -2
  622. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +0 -4
  623. package/src/cli/local-bridge/frontend/src/lib/api.ts +0 -30
  624. package/src/cli/local-bridge/frontend/src/lib/format.ts +0 -4
  625. package/src/cli/local-bridge/frontend/src/lib/routes.ts +0 -7
  626. package/src/cli/local-bridge/frontend/src/main.tsx +0 -1
  627. package/src/cli/local-bridge/frontend/src/shell/AppShell.tsx +0 -1
  628. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +0 -2
  629. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +0 -4
  630. package/src/cli/local-bridge/frontend/src/styles.css +0 -28
  631. package/src/cli/local-bridge/frontend/src/types.ts +0 -15
  632. package/src/cli/local-bridge/frontend/src/vite-env.d.ts +0 -1
  633. package/src/cli/local-bridge/frontend/vite.config.ts +0 -27
  634. package/src/cli/local-bridge/ui-dist/app.css +0 -1
  635. package/src/cli/local-bridge/ui-dist/app.js +0 -11228
  636. package/src/cli/mcp/access-mode.ts +0 -2
  637. package/src/cli/mcp/access-tools.ts +0 -2
  638. package/src/cli/mcp/audit.ts +0 -2
  639. package/src/cli/mcp/auth.ts +0 -2
  640. package/src/cli/mcp/instructions.ts +0 -2
  641. package/src/cli/mcp/legacy-context.ts +0 -2
  642. package/src/cli/mcp/legacy-tool-service.ts +0 -2
  643. package/src/cli/mcp/multi-repository.ts +0 -2
  644. package/src/cli/mcp/oauth.ts +0 -2
  645. package/src/cli/mcp/openai-secure-tunnel.ts +0 -2
  646. package/src/cli/mcp/paths.ts +0 -2
  647. package/src/cli/mcp/policy.ts +0 -2
  648. package/src/cli/mcp/post-finalize-work-attribution.ts +0 -2
  649. package/src/cli/mcp/proxy-env.ts +0 -2
  650. package/src/cli/mcp/readiness-projection.ts +0 -2
  651. package/src/cli/mcp/redaction.ts +0 -2
  652. package/src/cli/mcp/repo.ts +0 -2
  653. package/src/cli/mcp/repository-tools.ts +0 -2
  654. package/src/cli/mcp/server.ts +0 -2
  655. package/src/cli/mcp/setup.ts +0 -2
  656. package/src/cli/mcp/tool-capability-cache.ts +0 -2
  657. package/src/cli/mcp/tool-contract.ts +0 -2
  658. package/src/cli/mcp/tools.ts +0 -2
  659. package/src/cli/mcp/toolset-names.ts +0 -2
  660. package/src/cli/mcp/toolset-selection.ts +0 -2
  661. package/src/cli/mcp/toolset.ts +0 -2
  662. package/src/cli/mcp/transports/http.ts +0 -2
  663. package/src/cli/mcp/transports/session-registry.ts +0 -2
  664. package/src/cli/mcp/transports/stdio.ts +0 -2
  665. package/src/cli/mcp/types.ts +0 -2
  666. package/src/cli/mcp/umbrella-tools.ts +0 -2
  667. package/src/cli/tools/codegraph.ts +0 -695
  668. package/src/core/adoption/helper-wrapper-plan.ts +0 -159
  669. package/src/runtime/context/codegraph-cache-boundary.ts +0 -255
  670. package/src/runtime/context/codegraph-read-provider.ts +0 -611
  671. package/src/runtime/context/codegraph-sidecar.cjs +0 -248
  672. package/src/runtime/control-plane/codegraph-cache-retention.ts +0 -164
  673. package/src/runtime/control-plane/execution/content-equivalent-commit-authority.ts +0 -169
  674. package/src/runtime/control-plane/execution/direct-canonical-work-reconciliation.ts +0 -85
  675. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +0 -655
  676. package/src/runtime/control-plane/execution/work-finalization-service.ts +0 -2850
  677. package/src/runtime/control-plane/execution/work-path-scope.ts +0 -37
  678. package/src/runtime/control-plane/execution/work-task-receipt.ts +0 -468
  679. package/src/runtime/control-plane/facade/controller-round-relay.ts +0 -3
  680. package/src/runtime/control-plane/facade/work-admission-policy.ts +0 -142
  681. package/src/runtime/control-plane/facade/work-implementation-review.ts +0 -2
  682. package/src/runtime/control-plane/facade/work-state-machine.ts +0 -2
  683. package/src/runtime/control-plane/facade/work-task-projection.ts +0 -66
  684. package/src/runtime/control-plane/routing/index.ts +0 -1
  685. package/src/runtime/control-plane/routing/route-policy.ts +0 -401
  686. package/src/runtime/control-plane/routing/workspace-admission.ts +0 -70
  687. package/src/runtime/gateway/mcp/execution-tools.ts +0 -2
  688. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +0 -2
  689. package/src/runtime/gateway/mcp/persisted-check-process.ts +0 -2
  690. package/src/runtime/gateway/mcp/process-tools.ts +0 -2
  691. package/src/runtime/gateway/mcp/router.ts +0 -2
  692. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +0 -2
  693. package/src/runtime/gateway/mcp/runtime-tools.ts +0 -2
  694. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +0 -2
  695. package/src/runtime/plugins/browser-provider-registry.ts +0 -2
@@ -1,10 +1,11 @@
1
- import { createHash } from 'crypto';
1
+ import { createHash, randomUUID } from 'crypto';
2
2
  import { join } from 'path';
3
3
  import type { RepositoryRecord } from '../../cli/repositories/types';
4
- import { CONTROLLER_SCOPE_REPO_ID, controllerSystemRoot, ensureControllerHome, repositoryControllerRoot } from '../../cli/repositories/controller-home';
5
- import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, rmSync } from 'fs';
4
+ import { CONTROLLER_SCOPE_REPO_ID, FORGE_INSTANCE_SCOPE_KEY, controllerSystemRoot, ensureControllerHome, repositoryControllerRoot } from '../../cli/repositories/controller-home';
5
+ import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'fs';
6
6
  import type { ExecutionJob, ResourceClaimSpec } from '../execution/jobs/types';
7
7
  import { appendRuntimeEvent } from '../evidence/event-ledger';
8
+ import { redactSensitiveText, redactSensitiveValue } from '../evidence/sensitive-output';
8
9
  import { readJsonFile, sanitizeFileComponent, writeJsonAtomic } from '../shared/json-files';
9
10
  import { createFirstPartyPluginAdapterMap } from './first-party-registry';
10
11
  import { reconcileFirstPartyExternalPluginRegistration, reconcileFirstPartyExternalPluginRegistrations } from './first-party-external-registration';
@@ -20,17 +21,14 @@ import {
20
21
  } from './capability-authorization-grants';
21
22
  import { markControllerContextProjectionDirty } from '../projections/controller-context';
22
23
  import { acquireExecutionLeases, releaseExactExecutionLeases } from '../resources/leases/store';
23
- import { classifyRepositoryCommand } from '../../cli/repositories/command-classifier';
24
+ import { isRuntimeWriteFenceError } from '../root/write-fence';
24
25
  import {
25
- acceptSubmittedWorkContract,
26
- activateWorkContract,
27
26
  appendWorkEvidence,
28
- failWorkContract,
29
27
  getWorkContract,
30
28
  recordWorkCompletionReceipt,
31
29
  updateWorkContract,
32
30
  } from '../../../packages/kernel/work/api/index';
33
- import { isTerminalWorkContractStatus, type LocalEffectCompletionReceipt, type RemoteEffectCompletionReceipt, type WorkContract, type WorkRisk } from '../control-plane/facade/types';
31
+ import { isTerminalSemanticWorkState, type RemoteEffectCompletionReceipt, type WorkContract } from '../control-plane/facade/types';
34
32
  import type {
35
33
  AssistantPluginAdapter,
36
34
  AssistantPluginActionDescriptor,
@@ -75,65 +73,191 @@ function now(): string {
75
73
  return new Date().toISOString();
76
74
  }
77
75
 
78
- export function controllerPluginRepository(controllerHome: string): RepositoryRecord {
76
+ const CONTROLLER_PLUGIN_CACHE_SCOPE = 'controller';
77
+ const CONTROLLER_PLUGIN_GRANT_SCOPE = 'controller:global';
78
+ const CONTROLLER_PLUGIN_SCOPE_KEY = FORGE_INSTANCE_SCOPE_KEY;
79
+
80
+ interface PluginActionScopeContext {
81
+ kind: 'controller' | 'repository';
82
+ scopeKey: string;
83
+ grantRepoId: string;
84
+ root: string;
85
+ checkoutId?: string;
86
+ repository?: RepositoryRecord;
87
+ }
88
+
89
+ function repositoryPluginActionScope(repository: RepositoryRecord): PluginActionScopeContext {
90
+ return {
91
+ kind: 'repository',
92
+ scopeKey: repository.repoId,
93
+ grantRepoId: repository.repoId,
94
+ root: repository.canonicalRoot,
95
+ checkoutId: repository.activeCheckoutId,
96
+ repository,
97
+ };
98
+ }
99
+
100
+ function controllerPluginActionScope(controllerHome: string): PluginActionScopeContext {
101
+ return {
102
+ kind: 'controller',
103
+ scopeKey: CONTROLLER_PLUGIN_SCOPE_KEY,
104
+ grantRepoId: CONTROLLER_PLUGIN_GRANT_SCOPE,
105
+ root: controllerSystemRoot(controllerHome),
106
+ };
107
+ }
108
+
109
+ function pluginActionExecutionFields(scope: PluginActionScopeContext): { repoId: string; repoRoot: string } {
110
+ // Adapter fields are a compatibility projection. controller:global identifies
111
+ // the Grant/config domain; it is not a Repository Registry identity.
112
+ return { repoId: scope.grantRepoId, repoRoot: scope.root };
113
+ }
114
+
115
+ function controllerPluginsRoot(controllerHome: string): string {
116
+ return join(controllerSystemRoot(controllerHome), 'plugins');
117
+ }
118
+
119
+ function controllerManifestPath(controllerHome: string, pluginId: string): string {
120
+ return join(controllerPluginsRoot(controllerHome), 'manifests', `${sanitizeFileComponent(pluginId)}.json`);
121
+ }
122
+
123
+ function controllerIndexPath(controllerHome: string): string {
124
+ return join(controllerPluginsRoot(controllerHome), 'index.json');
125
+ }
126
+
127
+ function adapterMatchesController(adapter: AssistantPluginAdapter): boolean {
128
+ const scope = adapter.scope ?? 'repository';
129
+ return scope === 'controller' || scope === 'controller_with_repository_overlay';
130
+ }
131
+
132
+ function adapterMatchesRepository(adapter: AssistantPluginAdapter, _repository: RepositoryRecord): boolean {
133
+ const scope = adapter.scope ?? 'repository';
134
+ return scope === 'repository' || scope === 'controller_with_repository_overlay';
135
+ }
136
+
137
+ function computeControllerManifest(
138
+ controllerHome: string,
139
+ pluginId: string,
140
+ resolvedAdapter?: AssistantPluginAdapter,
141
+ ): AssistantPluginManifest {
142
+ const adapter = resolvedAdapter ?? resolvePluginAdapter(controllerHome, pluginId);
143
+ if (!adapter || !adapterMatchesController(adapter)) throw new Error(`PLUGIN_NOT_FOUND: ${pluginId}`);
144
+ const previous = readControllerStoredPluginManifest(controllerHome, pluginId);
79
145
  const root = controllerSystemRoot(controllerHome);
80
- const timestamp = now();
146
+ const built = adapter.buildManifest(previous?.revision ?? 0, previous?.updatedAt, root, {
147
+ controllerHome,
148
+ repoId: CONTROLLER_PLUGIN_GRANT_SCOPE,
149
+ repoRoot: root,
150
+ controllerScoped: true,
151
+ });
152
+ const changed = !previous || fingerprintManifest(previous) !== fingerprintManifest(built);
81
153
  return {
82
- schemaVersion: 1,
83
- repoId: CONTROLLER_SCOPE_REPO_ID,
84
- displayName: 'Controller local system',
85
- canonicalRoot: root,
86
- localRoot: root,
87
- activeCheckoutId: 'controller',
88
- checkouts: [],
89
- defaultBranch: 'none',
90
- repositoryType: 'local-git',
91
- enabled: true,
92
- createdAt: timestamp,
93
- updatedAt: timestamp,
94
- lastSeenAt: timestamp,
95
- configurationPath: join(root, 'local-system', 'targets.json'),
96
- stateStorageStrategy: 'controller-home',
154
+ ...built,
155
+ revision: previous ? (changed ? previous.revision + 1 : previous.revision) : 1,
156
+ updatedAt: changed ? now() : previous?.updatedAt ?? built.updatedAt,
97
157
  };
98
158
  }
99
159
 
100
- /**
101
- * Controller-scoped product facade. The synthetic RepositoryRecord remains an
102
- * internal storage/projection compatibility shape only; callers need no repo.
103
- */
104
160
  export function readControllerStoredPluginManifest(controllerHome: string, pluginId: string): AssistantPluginManifest | undefined {
105
- return readStoredAssistantPluginManifest(controllerHome, controllerPluginRepository(controllerHome), pluginId);
161
+ try {
162
+ const canonicalPath = controllerManifestPath(controllerHome, pluginId);
163
+ if (existsSync(canonicalPath)) return readJsonFile<AssistantPluginManifest>(canonicalPath);
164
+ const legacyPath = join(legacyControllerPluginsRoot(controllerHome), 'manifests', `${sanitizeFileComponent(pluginId)}.json`);
165
+ return existsSync(legacyPath) ? readJsonFile<AssistantPluginManifest>(legacyPath) : undefined;
166
+ } catch {
167
+ return undefined;
168
+ }
169
+ }
170
+
171
+ function listControllerPluginIds(controllerHome: string): string[] {
172
+ return listPluginAdapters(controllerHome).filter(adapterMatchesController).map((adapter) => adapter.pluginId).sort();
106
173
  }
107
174
 
108
175
  export function listControllerPluginManifests(
109
176
  controllerHome: string,
110
177
  options: ListAssistantPluginManifestsOptions = {},
111
178
  ): AssistantPluginManifest[] {
112
- return listAssistantPluginManifests(controllerHome, controllerPluginRepository(controllerHome), options);
179
+ const preferStored = options.preferStored === true && options.forceRefresh !== true;
180
+ const fallbackToLive = options.fallbackToLive !== false || !preferStored;
181
+ const cacheKey = listCacheKey(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, preferStored, fallbackToLive);
182
+ if (options.forceRefresh !== true) {
183
+ const cached = readPluginManifestCache(pluginManifestListCache, cacheKey, preferStored ? PLUGIN_MANIFEST_STORED_CACHE_TTL_MS : PLUGIN_MANIFEST_LIVE_CACHE_TTL_MS);
184
+ if (cached) return cached;
185
+ }
186
+ const manifests = listControllerPluginIds(controllerHome).map((pluginId) => {
187
+ if (preferStored) {
188
+ const stored = readControllerStoredPluginManifest(controllerHome, pluginId);
189
+ if (stored) return stored;
190
+ if (!fallbackToLive) return undefined;
191
+ }
192
+ return computeControllerManifest(controllerHome, pluginId);
193
+ }).filter((manifest): manifest is AssistantPluginManifest => Boolean(manifest)).sort((left, right) => left.pluginId.localeCompare(right.pluginId));
194
+ primePluginManifestItemCache(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, manifests, preferStored);
195
+ return writePluginManifestCache(pluginManifestListCache, cacheKey, manifests);
196
+ }
197
+
198
+ function getControllerPluginManifestForExecution(
199
+ controllerHome: string,
200
+ pluginId: string,
201
+ adapter: AssistantPluginAdapter,
202
+ ): { manifest: AssistantPluginManifest; providerIdentityPrevalidated: boolean } {
203
+ const cacheKey = itemCacheKey(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, pluginId, false);
204
+ const cached = readPluginManifestCache(pluginManifestItemCache, cacheKey);
205
+ const wrap = (manifest: AssistantPluginManifest) => ({
206
+ manifest,
207
+ providerIdentityPrevalidated: manifest.enabled && manifest.health.probed && manifest.health.ready,
208
+ });
209
+ if (cached) return wrap(cached);
210
+ return wrap(writePluginManifestCache(pluginManifestItemCache, cacheKey, computeControllerManifest(controllerHome, pluginId, adapter)));
113
211
  }
114
212
 
115
213
  export function getControllerPluginManifest(controllerHome: string, pluginId: string): AssistantPluginManifest {
116
- return getAssistantPluginManifest(controllerHome, controllerPluginRepository(controllerHome), pluginId);
214
+ const adapter = resolvePluginAdapter(controllerHome, pluginId);
215
+ if (!adapter || !adapterMatchesController(adapter)) throw new Error(`PLUGIN_NOT_FOUND: ${pluginId}`);
216
+ return getControllerPluginManifestForExecution(controllerHome, pluginId, adapter).manifest;
217
+ }
218
+
219
+ function writeControllerRegistry(controllerHome: string, manifests: AssistantPluginManifest[]): AssistantPluginRegistryIndex {
220
+ const index: AssistantPluginRegistryIndex = {
221
+ schemaVersion: 1,
222
+ updatedAt: now(),
223
+ plugins: manifests.map((manifest) => ({
224
+ ...pluginIndexEntry(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, manifest),
225
+ manifestPath: controllerManifestPath(controllerHome, manifest.pluginId),
226
+ })).sort((left, right) => left.pluginId.localeCompare(right.pluginId)),
227
+ };
228
+ writeJsonAtomic(controllerIndexPath(controllerHome), index);
229
+ return index;
117
230
  }
118
231
 
119
232
  export function syncControllerPluginRegistry(controllerHome: string) {
120
233
  reconcileFirstPartyExternalPluginRegistrations(controllerHome);
121
- return syncAssistantPluginRegistry(controllerHome, controllerPluginRepository(controllerHome));
234
+ invalidateAssistantPluginManifestCache(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE);
235
+ const manifests = listControllerPluginManifests(controllerHome, { forceRefresh: true });
236
+ for (const manifest of manifests) writeJsonAtomic(controllerManifestPath(controllerHome, manifest.pluginId), manifest);
237
+ return { manifests, index: writeControllerRegistry(controllerHome, manifests) };
122
238
  }
123
239
 
124
240
  export function syncControllerPluginManifest(controllerHome: string, pluginId: string) {
125
241
  reconcileFirstPartyExternalPluginRegistration(controllerHome, pluginId);
126
- return syncAssistantPluginManifest(controllerHome, controllerPluginRepository(controllerHome), pluginId);
242
+ invalidateAssistantPluginManifestCache(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, pluginId);
243
+ const manifest = computeControllerManifest(controllerHome, pluginId);
244
+ writeJsonAtomic(controllerManifestPath(controllerHome, pluginId), manifest);
245
+ cacheAssistantPluginManifest(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, manifest, false);
246
+ cacheAssistantPluginManifest(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, manifest, true);
247
+ const manifests = listControllerPluginIds(controllerHome).map((candidate) => candidate === pluginId
248
+ ? manifest
249
+ : readControllerStoredPluginManifest(controllerHome, candidate) ?? cachedManifestForRepository(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, candidate))
250
+ .filter((entry): entry is AssistantPluginManifest => Boolean(entry)).sort((left, right) => left.pluginId.localeCompare(right.pluginId));
251
+ return { manifest, index: writeControllerRegistry(controllerHome, manifests) };
127
252
  }
128
253
 
129
254
  export function removeControllerPluginManifestProjection(controllerHome: string, pluginId: string) {
130
- return removeAssistantPluginManifestProjection(controllerHome, controllerPluginRepository(controllerHome), pluginId);
131
- }
132
-
133
- function adapterMatchesRepository(adapter: AssistantPluginAdapter, repository: RepositoryRecord): boolean {
134
- const scope = adapter.scope ?? 'repository';
135
- if (scope === 'controller_with_repository_overlay') return true;
136
- return repository.repoId === CONTROLLER_SCOPE_REPO_ID ? scope === 'controller' : scope === 'repository';
255
+ invalidateAssistantPluginManifestCache(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, pluginId);
256
+ rmSync(controllerManifestPath(controllerHome, pluginId), { force: true });
257
+ const current = readJsonFile<AssistantPluginRegistryIndex>(controllerIndexPath(controllerHome), { schemaVersion: 1, updatedAt: now(), plugins: [] });
258
+ const next = { ...current, updatedAt: now(), plugins: current.plugins.filter((entry) => entry.pluginId !== pluginId) };
259
+ writeJsonAtomic(controllerIndexPath(controllerHome), next);
260
+ return next;
137
261
  }
138
262
 
139
263
  function cloneCacheValue<T>(value: T): T {
@@ -226,7 +350,13 @@ export function clearAssistantPluginManifestCacheForTest(): void {
226
350
  }
227
351
 
228
352
  function pluginsRoot(controllerHome: string, repoId: string): string {
229
- return join(repositoryControllerRoot(controllerHome, repoId), 'plugins');
353
+ return repoId === CONTROLLER_SCOPE_REPO_ID
354
+ ? join(controllerSystemRoot(controllerHome), 'plugins')
355
+ : join(repositoryControllerRoot(controllerHome, repoId), 'plugins');
356
+ }
357
+
358
+ function legacyControllerPluginsRoot(controllerHome: string): string {
359
+ return join(repositoryControllerRoot(controllerHome, CONTROLLER_SCOPE_REPO_ID), 'plugins');
230
360
  }
231
361
 
232
362
  function manifestPath(controllerHome: string, repoId: string, pluginId: string): string {
@@ -271,8 +401,14 @@ function pluginIndexEntry(controllerHome: string, repoId: string, manifest: Assi
271
401
  }
272
402
 
273
403
  function readStoredManifest(controllerHome: string, repoId: string, pluginId: string): AssistantPluginManifest | undefined {
404
+ const canonicalPath = manifestPath(controllerHome, repoId, pluginId);
274
405
  try {
275
- return readJsonFile<AssistantPluginManifest>(manifestPath(controllerHome, repoId, pluginId));
406
+ if (existsSync(canonicalPath)) return readJsonFile<AssistantPluginManifest>(canonicalPath);
407
+ if (repoId === CONTROLLER_SCOPE_REPO_ID) {
408
+ const legacyPath = join(legacyControllerPluginsRoot(controllerHome), 'manifests', `${sanitizeFileComponent(pluginId)}.json`);
409
+ if (existsSync(legacyPath)) return readJsonFile<AssistantPluginManifest>(legacyPath);
410
+ }
411
+ return undefined;
276
412
  } catch {
277
413
  return undefined;
278
414
  }
@@ -309,6 +445,20 @@ export function migrateRepositoryPluginConfigLegacyFiles(
309
445
  join(repository.canonicalRoot, '.repo-harness', 'plugins'),
310
446
  ];
311
447
  mkdirSync(targetRoot, { recursive: true });
448
+ // Upgrade an already-adopted v1/v2 Controller Home config in place. The
449
+ // adapter owns schema semantics; this store remains the sole file writer.
450
+ const browserTarget = repositoryPluginConfigPath({ controllerHome, repoId: repository.repoId }, 'browser');
451
+ const browserNormalizer = PLUGIN_ADAPTERS.get('browser')?.normalizeLegacyConfig;
452
+ if (!browserNormalizer) throw new Error('PLUGIN_CONFIG_MIGRATION_INVALID: browser schema owner missing');
453
+ if (existsSync(browserTarget)) {
454
+ const stat = lstatSync(browserTarget);
455
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('PLUGIN_CONFIG_AUTHORITY_PATH_INVALID: browser config');
456
+ let current: unknown;
457
+ try { current = JSON.parse(readFileSync(browserTarget, 'utf8')); }
458
+ catch { throw new Error('PLUGIN_CONFIG_AUTHORITY_INVALID: browser config JSON'); }
459
+ const normalized = browserNormalizer(current);
460
+ if ((current as { schemaVersion?: unknown }).schemaVersion !== 3) writeJsonAtomic(browserTarget, normalized);
461
+ }
312
462
  for (const legacyRoot of legacyRoots) {
313
463
  if (!existsSync(legacyRoot)) continue;
314
464
  const stat = lstatSync(legacyRoot);
@@ -326,7 +476,16 @@ export function migrateRepositoryPluginConfigLegacyFiles(
326
476
  const sourceStat = lstatSync(source);
327
477
  if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) continue;
328
478
  const target = repositoryPluginConfigPath({ controllerHome, repoId: repository.repoId }, pluginId);
329
- if (!existsSync(target)) cpSync(source, target, { force: false, errorOnExist: false });
479
+ if (!existsSync(target)) {
480
+ if (pluginId === 'browser') {
481
+ let parsed: unknown;
482
+ try { parsed = JSON.parse(readFileSync(source, 'utf8')); }
483
+ catch { throw new Error('PLUGIN_CONFIG_MIGRATION_INVALID: legacy browser JSON'); }
484
+ writeJsonAtomic(target, browserNormalizer(parsed));
485
+ } else {
486
+ cpSync(source, target, { force: false, errorOnExist: false });
487
+ }
488
+ }
330
489
  rmSync(source, { force: true });
331
490
  }
332
491
  if (readdirSync(legacyRoot).length === 0) rmSync(legacyRoot, { recursive: true, force: true });
@@ -340,7 +499,6 @@ function computeManifest(
340
499
  pluginId: string,
341
500
  resolvedAdapter?: AssistantPluginAdapter,
342
501
  ): AssistantPluginManifest {
343
- migrateRepositoryPluginConfigLegacyFiles(controllerHome, repository);
344
502
  const adapter = resolvedAdapter ?? resolvePluginAdapter(controllerHome, pluginId);
345
503
  if (!adapter || !adapterMatchesRepository(adapter, repository)) throw new Error(`PLUGIN_NOT_FOUND: ${pluginId}`);
346
504
  const previous = readStoredManifest(controllerHome, repository.repoId, pluginId);
@@ -348,7 +506,7 @@ function computeManifest(
348
506
  controllerHome,
349
507
  repoId: repository.repoId,
350
508
  repoRoot: repository.canonicalRoot,
351
- controllerScoped: repository.repoId === CONTROLLER_SCOPE_REPO_ID,
509
+ controllerScoped: false,
352
510
  });
353
511
  const changed = !previous || fingerprintManifest(previous) !== fingerprintManifest(built);
354
512
  return {
@@ -370,59 +528,76 @@ function writeRegistry(controllerHome: string, repoId: string, manifests: Assist
370
528
  return index;
371
529
  }
372
530
 
373
- export function claimsForAssistantPluginAction(
531
+ function claimsForPluginActionScope(
374
532
  action: AssistantPluginActionDescriptor,
375
- repository: RepositoryRecord,
533
+ scope: PluginActionScopeContext,
376
534
  trustedPluginId: string,
377
535
  ): ResourceClaimSpec[] {
378
- const controllerScoped = repository.repoId === CONTROLLER_SCOPE_REPO_ID;
379
536
  const pluginId = trustedPluginId.trim().replace(/[^a-zA-Z0-9._-]+/g, '-');
380
537
  if (!pluginId) throw new Error('PLUGIN_ID_REQUIRED_FOR_RESOURCE_CLAIMS');
381
538
  return action.resourceClaims.map((claim) => ({
382
539
  resourceKey: claim.resource === 'remote' || claim.resource === 'provider-state'
383
- // The manifest/plugin id was resolved from the trusted registration; a
384
- // provider never supplies arbitrary resource-key text.
385
540
  ? `provider-state:${pluginId}`
386
- : controllerScoped
541
+ : scope.kind === 'controller'
387
542
  ? `controller-system:${claim.resource}`
388
543
  : claim.resource === 'workspace'
389
- ? `workspace:${repository.activeCheckoutId}`
544
+ ? `workspace:${scope.checkoutId}`
390
545
  : claim.resource === 'git-refs'
391
- ? `git-refs:${repository.repoId}`
392
- : `repo-state:${repository.repoId}`,
546
+ ? `git-refs:${scope.scopeKey}`
547
+ : `repo-state:${scope.scopeKey}`,
393
548
  mode: claim.mode,
394
549
  }));
395
550
  }
396
551
 
552
+ export function claimsForAssistantPluginAction(
553
+ action: AssistantPluginActionDescriptor,
554
+ repository: RepositoryRecord,
555
+ trustedPluginId: string,
556
+ ): ResourceClaimSpec[] {
557
+ return claimsForPluginActionScope(action, repositoryPluginActionScope(repository), trustedPluginId);
558
+ }
559
+
397
560
  async function withAssistantPluginResourceLeases<T>(
398
561
  controllerHome: string,
399
- repository: RepositoryRecord,
562
+ scope: PluginActionScopeContext,
400
563
  action: AssistantPluginActionDescriptor,
401
564
  request: AssistantPluginActionRequest,
402
565
  operation: () => Promise<T>,
403
566
  ): Promise<T> {
404
- const claims = claimsForAssistantPluginAction(action, repository, request.pluginId).map((claim) => ({
567
+ const claims = claimsForPluginActionScope(action, scope, request.pluginId).map((claim) => ({
405
568
  ...claim,
406
- repoId: repository.repoId,
407
- checkoutId: repository.activeCheckoutId,
569
+ repoId: scope.scopeKey,
570
+ ...(scope.checkoutId ? { checkoutId: scope.checkoutId } : {}),
408
571
  }));
409
572
  if (claims.length === 0) return operation();
410
573
 
411
- const ownerJobId = `plugin:${request.requestId}`;
574
+ // requestId is the semantic idempotency key, but before its receipt/index is
575
+ // persisted two concurrent replays can enter this function. Give each live
576
+ // invocation a distinct lease owner so the later replay contends on the
577
+ // first invocation's declared resource instead of silently accumulating a
578
+ // second same-owner lease and then failing exact release with LEASE_SET_MISMATCH.
579
+ const ownerJobId = `plugin:${request.requestId}:${randomUUID()}`;
412
580
  const timeoutMs = Math.max(5_000, Math.min(10 * 60_000, request.timeoutMs ?? action.defaultTimeoutMs));
413
- const acquisition = acquireExecutionLeases(controllerHome, repository.repoId, ownerJobId, claims, {
581
+ const acquisition = acquireExecutionLeases(controllerHome, scope.scopeKey, ownerJobId, claims, {
414
582
  ttlMs: timeoutMs + 60_000,
583
+ // This lease exists only to fence concurrent invocations of one external
584
+ // effect. It is not Scheduler/recovery authority, so derived projection,
585
+ // event-ledger, and wake side effects must never participate in the effect
586
+ // outcome. The lease file + fencing token remain cross-process durable for
587
+ // the bounded invocation and expire if its process dies.
588
+ visibility: 'ephemeral',
415
589
  ownerIdentity: {
416
- repositoryId: repository.repoId,
417
- checkoutId: repository.activeCheckoutId,
418
- worktreeId: repository.activeCheckoutId,
590
+ repositoryId: scope.scopeKey,
591
+ checkoutId: scope.checkoutId ?? CONTROLLER_PLUGIN_SCOPE_KEY,
592
+ worktreeId: scope.checkoutId ?? CONTROLLER_PLUGIN_SCOPE_KEY,
419
593
  branch: 'workflow-plugin-action',
420
594
  principalId: request.origin.actor?.trim() || 'workflow-plugin-action',
421
595
  controllerInstanceId: process.env.FORGE_RUNTIME_INSTANCE_ID?.trim()
422
596
  || process.env.FORGE_WRITER_INSTANCE_ID?.trim()
423
597
  || process.env.FORGE_DAEMON_INSTANCE_ID?.trim()
424
598
  || `process:${process.pid}`,
425
- controllerGeneration: process.env.FORGE_WRITER_GENERATION?.trim()
599
+ controllerGeneration: process.env.FORGE_RUNTIME_INCARNATION_GENERATION?.trim()
600
+ || process.env.FORGE_WRITER_GENERATION?.trim()
426
601
  || process.env.FORGE_ACTIVE_RUNTIME_REVISION?.trim()
427
602
  || 'unbound',
428
603
  },
@@ -455,13 +630,24 @@ async function withAssistantPluginResourceLeases<T>(
455
630
  retainForReconciliation = isAssistantPluginError(error) && error.effectOutcome === 'outcome_unknown';
456
631
  throw error;
457
632
  } finally {
458
- if (!retainForReconciliation) releaseExactExecutionLeases(controllerHome, repository.repoId, ownerJobId, expected);
633
+ if (!retainForReconciliation) {
634
+ try {
635
+ releaseExactExecutionLeases(controllerHome, scope.scopeKey, ownerJobId, expected, { visibility: 'ephemeral' });
636
+ } catch (error) {
637
+ // A successful provider effect can rotate Runtime authority (for example
638
+ // a fenced Runtime cutover). The old sidecar must not release leases
639
+ // after it is fenced, but that cleanup denial must not overwrite the
640
+ // already-known provider result. The canonical successor Runtime can
641
+ // reclaim this ephemeral lease from its fenced owner immediately.
642
+ if (!isRuntimeWriteFenceError(error)) throw error;
643
+ }
644
+ }
459
645
  }
460
646
  }
461
647
 
462
- function semanticKey(repository: RepositoryRecord, pluginId: string, actionId: string, args: Record<string, unknown>): string {
648
+ function semanticKey(scope: PluginActionScopeContext, pluginId: string, actionId: string, args: Record<string, unknown>): string {
463
649
  const digest = createHash('sha256').update(JSON.stringify(canonical(args))).digest('hex').slice(0, 20);
464
- return `plugin-action:${repository.repoId}:${pluginId}:${actionId}:${digest}`;
650
+ return `plugin-action:${scope.scopeKey}:${pluginId}:${actionId}:${digest}`;
465
651
  }
466
652
 
467
653
  function validatePrimitive(type: string, value: unknown): boolean {
@@ -515,7 +701,18 @@ function enforceConfirmation(action: AssistantPluginActionDescriptor, request: A
515
701
  // Ordinary authorization is delegated to the host AI/tool permission model.
516
702
  // Forge keeps only the explicit strong-confirmation boundary for
517
703
  // destructive or irreversible plugin operations.
518
- if (action.confirmation === 'none' || action.confirmation === 'authorization') return;
704
+ if (action.confirmation === 'none') return;
705
+ if (action.confirmation === 'authorization') {
706
+ if (action.requiredConfirmationText) {
707
+ if (request.confirmAuthorization !== true) {
708
+ throw new Error(`PLUGIN_CONFIRMATION_REQUIRED: ${request.pluginId}/${request.actionId} requires confirmAuthorization=true`);
709
+ }
710
+ if (request.confirmationText !== action.requiredConfirmationText) {
711
+ throw new Error(`PLUGIN_CONFIRMATION_TEXT_REQUIRED: provide confirmationText=${action.requiredConfirmationText}`);
712
+ }
713
+ }
714
+ return;
715
+ }
519
716
  if (action.confirmation === 'strong_confirmation') {
520
717
  if (request.confirmAuthorization !== true) {
521
718
  throw new Error(`PLUGIN_CONFIRMATION_REQUIRED: ${request.pluginId}/${request.actionId} requires confirmAuthorization=true`);
@@ -582,7 +779,7 @@ function authorizationTargetMatches(
582
779
 
583
780
  async function resolveAutomatedWriteAuthorization(input: {
584
781
  controllerHome: string;
585
- repository: RepositoryRecord;
782
+ scope: PluginActionScopeContext;
586
783
  adapter: AssistantPluginAdapter;
587
784
  manifest: AssistantPluginManifest;
588
785
  action: AssistantPluginActionDescriptor;
@@ -610,14 +807,14 @@ async function resolveAutomatedWriteAuthorization(input: {
610
807
  }
611
808
  const targetContext = await input.adapter.resolveAuthorizationContext({
612
809
  controllerHome: input.controllerHome,
613
- repoId: input.repository.repoId,
614
- repoRoot: input.repository.canonicalRoot,
810
+ ...pluginActionExecutionFields(input.scope),
615
811
  pluginId: input.manifest.pluginId,
616
812
  actionId: input.action.actionId,
617
813
  requestId: input.requestId,
618
814
  args: input.args,
619
815
  origin: input.origin,
620
816
  jobId: input.jobId,
817
+ authorizationGrantRefs: refs,
621
818
  });
622
819
  if (!targetContext) {
623
820
  throw new AssistantPluginError(
@@ -630,7 +827,7 @@ async function resolveAutomatedWriteAuthorization(input: {
630
827
  const grant = refs
631
828
  .map((ref) => findActivePluginCapabilityAuthorizationById(input.controllerHome, ref))
632
829
  .find((candidate) => candidate
633
- && candidate.repoId === input.repository.repoId
830
+ && candidate.repoId === input.scope.grantRepoId
634
831
  && candidate.pluginId === input.manifest.pluginId
635
832
  && candidate.capabilityId === capabilityId
636
833
  && authorizationTargetMatches(candidate.target, targetContext.target)
@@ -759,6 +956,7 @@ export function syncAssistantPluginRegistry(
759
956
  repository: RepositoryRecord,
760
957
  ): { manifests: AssistantPluginManifest[]; index: AssistantPluginRegistryIndex } {
761
958
  invalidateAssistantPluginManifestCache(controllerHome, repository.repoId);
959
+ migrateRepositoryPluginConfigLegacyFiles(controllerHome, repository);
762
960
  const manifests = listAssistantPluginManifests(controllerHome, repository, { forceRefresh: true });
763
961
  for (const manifest of manifests) {
764
962
  writeJsonAtomic(manifestPath(controllerHome, repository.repoId, manifest.pluginId), manifest);
@@ -776,6 +974,7 @@ export function syncAssistantPluginManifest(
776
974
  pluginId: string,
777
975
  ): { manifest: AssistantPluginManifest; index: AssistantPluginRegistryIndex } {
778
976
  invalidateAssistantPluginManifestCache(controllerHome, repository.repoId, pluginId);
977
+ migrateRepositoryPluginConfigLegacyFiles(controllerHome, repository);
779
978
  const manifest = computeManifest(controllerHome, repository, pluginId);
780
979
  writeJsonAtomic(manifestPath(controllerHome, repository.repoId, manifest.pluginId), manifest);
781
980
  markControllerContextProjectionDirty(repository.canonicalRoot, `plugin:${pluginId}:synced`);
@@ -819,25 +1018,39 @@ export function removeAssistantPluginManifestProjection(
819
1018
  }
820
1019
 
821
1020
  export function isDirectPluginReadAction(action: AssistantPluginActionDescriptor): boolean {
822
- return action.readOnly === true
1021
+ return action.executionMode !== 'direct_non_persistent'
1022
+ && action.readOnly === true
823
1023
  && action.risk === 'readonly'
824
1024
  && action.confirmation === 'none'
825
1025
  && action.idempotent === true;
826
1026
  }
827
1027
 
828
1028
  export function isDirectNonPersistentPluginAction(action: AssistantPluginActionDescriptor): boolean {
829
- return action.executionMode === 'direct_non_persistent'
830
- && action.readOnly === false
831
- && action.risk === 'workspace_write'
832
- && action.confirmation === 'authorization'
833
- && action.idempotent === false
834
- && action.remoteEffectWorkCompletion === undefined
835
- && action.resourceClaims.length === 0;
1029
+ if (
1030
+ action.executionMode !== 'direct_non_persistent'
1031
+ || action.remoteEffectWorkCompletion !== undefined
1032
+ || action.resourceClaims.length !== 0
1033
+ ) {
1034
+ return false;
1035
+ }
1036
+
1037
+ // Direct non-persistent actions are provider-local operations that must never
1038
+ // enter the durable effect/receipt path. They are intentionally limited to
1039
+ // either a pure provider read or a provider-local workspace effect. Remote
1040
+ // writes/destructive effects still require the normal durable application
1041
+ // path and its reconciliation authority.
1042
+ if (action.readOnly) {
1043
+ return action.risk === 'readonly'
1044
+ && (action.confirmation === 'none' || action.confirmation === 'authorization')
1045
+ && action.idempotent === true;
1046
+ }
1047
+ return action.risk === 'workspace_write'
1048
+ && (action.confirmation === 'authorization' || action.confirmation === 'none');
836
1049
  }
837
1050
 
838
- export async function executeAssistantPluginDirectNonPersistent(
1051
+ async function executePluginDirectNonPersistentInScope(
839
1052
  controllerHome: string,
840
- repository: RepositoryRecord,
1053
+ scope: PluginActionScopeContext,
841
1054
  request: AssistantPluginActionRequest,
842
1055
  ): Promise<{ manifest: AssistantPluginManifest; action: AssistantPluginActionDescriptor; result: Record<string, unknown> }> {
843
1056
  const adapter = resolvePluginAdapter(controllerHome, request.pluginId);
@@ -845,7 +1058,9 @@ export async function executeAssistantPluginDirectNonPersistent(
845
1058
  if (PLUGIN_ADAPTERS.get(request.pluginId) !== adapter) {
846
1059
  throw new Error(`PLUGIN_DIRECT_NON_PERSISTENT_FIRST_PARTY_REQUIRED: ${request.pluginId}/${request.actionId}`);
847
1060
  }
848
- const manifestLookup = getAssistantPluginManifestForExecution(controllerHome, repository, request.pluginId, adapter);
1061
+ const manifestLookup = scope.kind === 'controller'
1062
+ ? getControllerPluginManifestForExecution(controllerHome, request.pluginId, adapter)
1063
+ : getAssistantPluginManifestForExecution(controllerHome, scope.repository!, request.pluginId, adapter);
849
1064
  const manifest = manifestLookup.manifest;
850
1065
  const action = actionForManifest(manifest, request.actionId);
851
1066
  if (!manifest.enabled && action.actionId !== 'configure') {
@@ -860,7 +1075,7 @@ export async function executeAssistantPluginDirectNonPersistent(
860
1075
  if (request.workId || request.workRepoId) {
861
1076
  throw new Error(`PLUGIN_DIRECT_NON_PERSISTENT_WORK_ATTRIBUTION_FORBIDDEN: ${request.pluginId}/${request.actionId}`);
862
1077
  }
863
- if (request.confirmAuthorization !== true) {
1078
+ if (action.confirmation === 'authorization' && request.confirmAuthorization !== true) {
864
1079
  throw new Error(`PLUGIN_DIRECT_NON_PERSISTENT_AUTHORIZATION_REQUIRED: ${request.pluginId}/${request.actionId} requires confirm_authorization=true for this invocation`);
865
1080
  }
866
1081
  const normalizedArgs = validateActionArguments(action, request.args ?? {});
@@ -868,7 +1083,7 @@ export async function executeAssistantPluginDirectNonPersistent(
868
1083
  if (requiresAutomatedWriteAuthorization(request.origin)) {
869
1084
  await resolveAutomatedWriteAuthorization({
870
1085
  controllerHome,
871
- repository,
1086
+ scope,
872
1087
  adapter,
873
1088
  manifest,
874
1089
  action,
@@ -880,8 +1095,7 @@ export async function executeAssistantPluginDirectNonPersistent(
880
1095
  }
881
1096
  const result = await adapter.executeAction({
882
1097
  controllerHome,
883
- repoId: repository.repoId,
884
- repoRoot: repository.canonicalRoot,
1098
+ ...pluginActionExecutionFields(scope),
885
1099
  pluginId: request.pluginId,
886
1100
  actionId: request.actionId,
887
1101
  requestId: request.requestId,
@@ -895,12 +1109,29 @@ export async function executeAssistantPluginDirectNonPersistent(
895
1109
  return { manifest, action, result };
896
1110
  }
897
1111
 
898
- export async function executeAssistantPluginReadDirect(
1112
+ export function executeAssistantPluginDirectNonPersistent(
899
1113
  controllerHome: string,
900
1114
  repository: RepositoryRecord,
901
1115
  request: AssistantPluginActionRequest,
902
- ): Promise<{ manifest: AssistantPluginManifest; action: AssistantPluginActionDescriptor; result: Record<string, unknown> }> {
903
- const manifest = getAssistantPluginManifest(controllerHome, repository, request.pluginId);
1116
+ ) {
1117
+ return executePluginDirectNonPersistentInScope(controllerHome, repositoryPluginActionScope(repository), request);
1118
+ }
1119
+
1120
+ export function executeControllerPluginDirectNonPersistent(
1121
+ controllerHome: string,
1122
+ request: AssistantPluginActionRequest,
1123
+ ) {
1124
+ return executePluginDirectNonPersistentInScope(controllerHome, controllerPluginActionScope(controllerHome), request);
1125
+ }
1126
+
1127
+ async function executePluginReadDirectInScope(
1128
+ controllerHome: string,
1129
+ scope: PluginActionScopeContext,
1130
+ request: AssistantPluginActionRequest,
1131
+ ): Promise<{ manifest: AssistantPluginManifest; action: AssistantPluginActionDescriptor; result: Record<string, unknown>; receipt: PluginActionReceipt }> {
1132
+ const manifest = scope.kind === 'controller'
1133
+ ? getControllerPluginManifest(controllerHome, request.pluginId)
1134
+ : getAssistantPluginManifest(controllerHome, scope.repository!, request.pluginId);
904
1135
  const action = actionForManifest(manifest, request.actionId);
905
1136
  if (!manifest.enabled && action.actionId !== 'configure') {
906
1137
  throw new Error(`PLUGIN_DISABLED: ${request.pluginId} is disabled`);
@@ -910,24 +1141,68 @@ export async function executeAssistantPluginReadDirect(
910
1141
  }
911
1142
  const normalizedArgs = validateActionArguments(action, request.args ?? {});
912
1143
  enforceConfirmation(action, { ...request, args: normalizedArgs });
913
- const result = await executeAssistantPluginAction({
1144
+ const result = await executePluginActionInScope(scope, {
914
1145
  controllerHome,
915
- repoId: repository.repoId,
916
- repoRoot: repository.canonicalRoot,
1146
+ ...pluginActionExecutionFields(scope),
917
1147
  pluginId: request.pluginId,
918
1148
  actionId: request.actionId,
919
1149
  requestId: request.requestId,
920
1150
  args: normalizedArgs,
921
1151
  origin: request.origin,
922
1152
  });
923
- return { manifest, action, result };
1153
+ const createdAt = new Date().toISOString();
1154
+ const resultDigest = createHash('sha256').update(JSON.stringify(result)).digest('hex');
1155
+ const observationSemanticKey = semanticKey(scope, request.pluginId, request.actionId, normalizedArgs);
1156
+ const observationActor = request.origin?.actor?.trim() ?? '';
1157
+ // Observation evidence is content-addressed so repeated identical reads do not create
1158
+ // an unbounded receipt trail. This remains provenance only, never replay/effect state.
1159
+ const receiptId = `PLG-OBS-${createHash('sha256')
1160
+ .update(`${observationSemanticKey}:${resultDigest}:${observationActor}`)
1161
+ .digest('hex').slice(0, 24)}`;
1162
+ const receipt: PluginActionReceipt = {
1163
+ schemaVersion: 1,
1164
+ receiptId,
1165
+ requestId: request.requestId,
1166
+ scopeKey: scope.scopeKey,
1167
+ ...(scope.repository ? { repoId: scope.repository.repoId } : {}),
1168
+ pluginId: request.pluginId,
1169
+ actionId: request.actionId,
1170
+ semanticKey: observationSemanticKey,
1171
+ status: 'succeeded',
1172
+ createdAt,
1173
+ ...(request.origin ? { origin: request.origin } : {}),
1174
+ observationOnly: true,
1175
+ resultDigest,
1176
+ };
1177
+ // Direct reads stay on the inline fast path. Persist only a compact observation
1178
+ // receipt, never provider result content and never request replay state.
1179
+ writeJsonAtomic(pluginActionReceiptPath(controllerHome, scope.scopeKey, receiptId), receipt);
1180
+ return { manifest, action, result, receipt };
1181
+ }
1182
+
1183
+ export function executeAssistantPluginReadDirect(
1184
+ controllerHome: string,
1185
+ repository: RepositoryRecord,
1186
+ request: AssistantPluginActionRequest,
1187
+ ) {
1188
+ return executePluginReadDirectInScope(controllerHome, repositoryPluginActionScope(repository), request);
1189
+ }
1190
+
1191
+ export function executeControllerPluginReadDirect(
1192
+ controllerHome: string,
1193
+ request: AssistantPluginActionRequest,
1194
+ ) {
1195
+ return executePluginReadDirectInScope(controllerHome, controllerPluginActionScope(controllerHome), request);
924
1196
  }
925
1197
 
926
1198
  export interface PluginActionReceipt {
927
1199
  schemaVersion: 1;
928
1200
  receiptId: string;
929
1201
  requestId: string;
930
- repoId: string;
1202
+ /** Canonical plugin-action receipt scope: `instance` for controller plugins, repository id otherwise. */
1203
+ scopeKey: string;
1204
+ /** Repository provenance only; absent for controller-scoped plugin actions. */
1205
+ repoId?: string;
931
1206
  /** Work authority repository when it differs from controller/provider receipt scope. */
932
1207
  workRepoId?: string;
933
1208
  pluginId: string;
@@ -941,24 +1216,35 @@ export interface PluginActionReceipt {
941
1216
  origin?: { surface: string; actor?: string; correlationId?: string };
942
1217
  authorization?: PluginActionAuthorizationEvidence;
943
1218
  result?: Record<string, unknown>;
944
- error?: { code: string; message: string };
1219
+ error?: { code: string; message: string; retryable?: boolean; details?: Record<string, unknown> };
1220
+ /** True for a compact direct-read observation; it is evidence, never an effect-completion authority. */
1221
+ observationOnly?: boolean;
1222
+ /** SHA-256 of the direct-read result. Result content is intentionally not persisted for observation-only receipts. */
1223
+ resultDigest?: string;
945
1224
  }
946
1225
 
947
1226
  interface PluginActionRequestIndex {
948
1227
  requestId: string;
949
- repoId: string;
1228
+ scopeKey: string;
1229
+ repoId?: string;
950
1230
  workRepoId?: string;
951
1231
  receiptId: string;
952
1232
  semanticKey: string;
953
1233
  createdAt: string;
954
1234
  }
955
1235
 
956
- function pluginActionReceiptRoot(controllerHome: string, repoId: string): string {
957
- const root = join(repositoryControllerRoot(controllerHome, repoId), 'plugin-action-receipts');
1236
+ function pluginActionReceiptRoot(controllerHome: string, scopeKey: string): string {
1237
+ const root = scopeKey === CONTROLLER_PLUGIN_SCOPE_KEY
1238
+ ? join(controllerSystemRoot(controllerHome), 'plugin-action-receipts')
1239
+ : join(repositoryControllerRoot(controllerHome, scopeKey), 'plugin-action-receipts');
958
1240
  mkdirSync(root, { recursive: true });
959
1241
  return root;
960
1242
  }
961
1243
 
1244
+ function legacyControllerPluginActionReceiptPath(controllerHome: string, receiptId: string): string {
1245
+ return join(repositoryControllerRoot(controllerHome, CONTROLLER_SCOPE_REPO_ID), 'plugin-action-receipts', `${sanitizeFileComponent(receiptId)}.json`);
1246
+ }
1247
+
962
1248
  function pluginActionRequestPath(controllerHome: string, requestId: string): string {
963
1249
  const hash = createHash('sha256').update(requestId).digest('hex');
964
1250
  const root = join(ensureControllerHome(controllerHome), 'indexes', 'plugin-actions', 'requests');
@@ -966,14 +1252,30 @@ function pluginActionRequestPath(controllerHome: string, requestId: string): str
966
1252
  return join(root, `${hash}.json`);
967
1253
  }
968
1254
 
969
- function pluginActionReceiptPath(controllerHome: string, repoId: string, receiptId: string): string {
970
- return join(pluginActionReceiptRoot(controllerHome, repoId), `${sanitizeFileComponent(receiptId)}.json`);
1255
+ function pluginActionReceiptPath(controllerHome: string, scopeKey: string, receiptId: string): string {
1256
+ return join(pluginActionReceiptRoot(controllerHome, scopeKey), `${sanitizeFileComponent(receiptId)}.json`);
971
1257
  }
972
1258
 
973
- function readPluginActionReceipt(controllerHome: string, repoId: string, receiptId: string): PluginActionReceipt | undefined {
974
- const path = pluginActionReceiptPath(controllerHome, repoId, receiptId);
975
- if (!existsSync(path)) return undefined;
976
- return readJsonFile<PluginActionReceipt>(path);
1259
+ function normalizePluginActionReceipt(receipt: PluginActionReceipt): PluginActionReceipt {
1260
+ const scopeKey = receipt.scopeKey?.trim()
1261
+ || (receipt.repoId === CONTROLLER_SCOPE_REPO_ID ? CONTROLLER_PLUGIN_SCOPE_KEY : receipt.repoId?.trim())
1262
+ || '';
1263
+ if (!scopeKey) throw new Error('PLUGIN_RECEIPT_SCOPE_CORRUPT');
1264
+ return {
1265
+ ...receipt,
1266
+ scopeKey,
1267
+ ...(scopeKey === CONTROLLER_PLUGIN_SCOPE_KEY ? { repoId: undefined } : { repoId: receipt.repoId ?? scopeKey }),
1268
+ };
1269
+ }
1270
+
1271
+ function readPluginActionReceipt(controllerHome: string, scopeKey: string, receiptId: string): PluginActionReceipt | undefined {
1272
+ const path = pluginActionReceiptPath(controllerHome, scopeKey, receiptId);
1273
+ if (existsSync(path)) return normalizePluginActionReceipt(readJsonFile<PluginActionReceipt>(path));
1274
+ if (scopeKey === CONTROLLER_PLUGIN_SCOPE_KEY || scopeKey === CONTROLLER_SCOPE_REPO_ID) {
1275
+ const legacyPath = legacyControllerPluginActionReceiptPath(controllerHome, receiptId);
1276
+ if (existsSync(legacyPath)) return normalizePluginActionReceipt(readJsonFile<PluginActionReceipt>(legacyPath));
1277
+ }
1278
+ return undefined;
977
1279
  }
978
1280
 
979
1281
  export function findPluginActionReceipt(
@@ -981,6 +1283,12 @@ export function findPluginActionReceipt(
981
1283
  receiptId: string,
982
1284
  ): PluginActionReceipt | undefined {
983
1285
  const home = ensureControllerHome(controllerHome);
1286
+ // Controller-scoped plugins (for example Local System) live under system/, not
1287
+ // repositories/. They are first-class evidence producers and must be searched too.
1288
+ const controllerReceipt = readPluginActionReceipt(home, CONTROLLER_PLUGIN_SCOPE_KEY, receiptId)
1289
+ // One-way read compatibility for pre-cutover controller receipts only.
1290
+ ?? readPluginActionReceipt(home, CONTROLLER_SCOPE_REPO_ID, receiptId);
1291
+ if (controllerReceipt) return controllerReceipt;
984
1292
  const repositoriesRoot = join(home, 'repositories');
985
1293
  try {
986
1294
  for (const repoId of readdirSync(repositoriesRoot)) {
@@ -999,7 +1307,9 @@ export function readPluginActionReceiptForRequest(controllerHome: string, reques
999
1307
  if (!existsSync(path)) return undefined;
1000
1308
  const index = readJsonFile<PluginActionRequestIndex>(path);
1001
1309
  if (index.requestId !== requestId) throw new Error('PLUGIN_RECEIPT_REQUEST_MISMATCH');
1002
- const receipt = readPluginActionReceipt(controllerHome, index.repoId, index.receiptId);
1310
+ const scopeKey = index.scopeKey?.trim() || index.repoId?.trim();
1311
+ if (!scopeKey) throw new Error('PLUGIN_RECEIPT_REQUEST_SCOPE_CORRUPT');
1312
+ const receipt = readPluginActionReceipt(controllerHome, scopeKey, index.receiptId);
1003
1313
  if (!receipt || receipt.requestId !== requestId || receipt.semanticKey !== index.semanticKey) throw new Error('PLUGIN_RECEIPT_LOST');
1004
1314
  return receipt;
1005
1315
  }
@@ -1011,86 +1321,20 @@ export function compatibilityPluginJobFromReceipt(
1011
1321
  return compatibilityJobFromReceipt(receipt, checkoutId);
1012
1322
  }
1013
1323
 
1014
- const LOCAL_SYSTEM_MUTATION_ACTIONS = new Set([
1015
- 'authorize_target',
1016
- 'revoke_target',
1017
- 'create_directory',
1018
- 'write_text',
1019
- 'delete_file',
1020
- 'delete_empty_directory',
1021
- 'initialize_git',
1022
- 'execute_project_script',
1023
- 'copy_file',
1024
- 'move_file',
1025
- 'rename_file',
1026
- 'open_application',
1027
- 'reveal_in_finder',
1028
- 'open_file',
1029
- ]);
1030
-
1031
- function localSystemActionRequiresWork(
1032
- repository: RepositoryRecord,
1033
- pluginId: string,
1034
- actionId: string,
1035
- args: Record<string, unknown>,
1036
- ): boolean {
1037
- if (repository.repoId !== CONTROLLER_SCOPE_REPO_ID || pluginId !== 'local_system') return false;
1038
- if (LOCAL_SYSTEM_MUTATION_ACTIONS.has(actionId)) return true;
1039
- if (actionId !== 'execute_command' || !Array.isArray(args.command)) return false;
1040
- return classifyRepositoryCommand(args.command.map(String)).risk !== 'readonly';
1041
- }
1042
-
1043
- function workRiskForPluginAction(action: AssistantPluginActionDescriptor): WorkRisk {
1044
- if (action.risk === 'readonly') return 'readonly';
1045
- if (action.risk === 'workspace_write') return 'medium';
1046
- if (action.risk === 'remote_write') return 'high';
1047
- return 'destructive';
1048
- }
1049
-
1050
- function localEffectTarget(
1051
- args: Record<string, unknown>,
1052
- result: Record<string, unknown>,
1053
- ): LocalEffectCompletionReceipt['target'] {
1054
- const inner = result.result && typeof result.result === 'object' && !Array.isArray(result.result)
1055
- ? result.result as Record<string, unknown>
1056
- : result;
1057
- const target = inner.target && typeof inner.target === 'object' && !Array.isArray(inner.target)
1058
- ? inner.target as Record<string, unknown>
1059
- : undefined;
1060
- const id = String(
1061
- target?.workspaceId
1062
- ?? inner.workspaceId
1063
- ?? args.target_key
1064
- ?? args.destination_target_key
1065
- ?? args.source_target_key
1066
- ?? 'controller-local',
1067
- );
1068
- const identityFingerprint = typeof target?.identityFingerprint === 'string'
1069
- ? target.identityFingerprint
1070
- : typeof inner.identityFingerprint === 'string'
1071
- ? inner.identityFingerprint
1072
- : undefined;
1073
- return {
1074
- kind: id.startsWith('workspace_') || args.target_key || args.destination_target_key || args.source_target_key
1075
- ? 'workspace_target'
1076
- : 'controller_local',
1077
- id,
1078
- ...(identityFingerprint ? { identityFingerprint } : {}),
1079
- };
1080
- }
1081
-
1082
- function workAttributionRepoId(repository: RepositoryRecord, request: AssistantPluginActionRequest): string {
1324
+ function workAttributionRepoId(scope: PluginActionScopeContext, request: AssistantPluginActionRequest): string {
1083
1325
  const explicit = request.workRepoId?.trim();
1084
1326
  if (!request.workId?.trim()) {
1085
1327
  if (explicit) throw new Error(`WORK_PLUGIN_ATTRIBUTION_REPO_WITHOUT_WORK: ${explicit}`);
1086
- return repository.repoId;
1328
+ return scope.repository?.repoId ?? '';
1087
1329
  }
1088
- return explicit || repository.repoId;
1330
+ if (explicit) return explicit;
1331
+ if (scope.repository) return scope.repository.repoId;
1332
+ throw new Error(`WORK_PLUGIN_ATTRIBUTION_REPO_REQUIRED: controller-scoped action ${request.pluginId}/${request.actionId} must name the Work repository explicitly`);
1089
1333
  }
1090
1334
 
1091
1335
  function remoteEffectWorkForPluginAction(
1092
1336
  controllerHome: string,
1093
- repository: RepositoryRecord,
1337
+ scope: PluginActionScopeContext,
1094
1338
  action: AssistantPluginActionDescriptor,
1095
1339
  request: AssistantPluginActionRequest,
1096
1340
  receiptId?: string,
@@ -1100,7 +1344,7 @@ function remoteEffectWorkForPluginAction(
1100
1344
  if (action.risk !== 'remote_write') {
1101
1345
  throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_REQUIRES_REMOTE_WRITE: ${action.actionId}`);
1102
1346
  }
1103
- const workRepoId = workAttributionRepoId(repository, request);
1347
+ const workRepoId = workAttributionRepoId(scope, request);
1104
1348
  const work = getWorkContract({ controllerHome, repoId: workRepoId }, workId);
1105
1349
  if (!work) throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_NOT_FOUND: ${workId}`);
1106
1350
  if (work.workKind !== 'remote_effect') {
@@ -1109,49 +1353,64 @@ function remoteEffectWorkForPluginAction(
1109
1353
  if (work.checks.length > 0) {
1110
1354
  throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_CHECKS_PRESENT: ${workId} declares repository checks and cannot be completed by a plugin receipt alone`);
1111
1355
  }
1112
- if (isTerminalWorkContractStatus(work.status)) {
1356
+ if (isTerminalSemanticWorkState(work.semanticState)) {
1113
1357
  const receiptAlreadyBound = Boolean(receiptId) && (
1114
1358
  work.completionReceipt?.receiptId === receiptId
1115
1359
  || work.evidenceRefs.some((evidence) => evidence.evidenceId === receiptId)
1116
1360
  );
1117
- if (work.status === 'completed' && receiptAlreadyBound) return work;
1118
- throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_TERMINAL: ${workId} is ${work.status}`);
1361
+ if (work.semanticState === 'completed' && receiptAlreadyBound) return work;
1362
+ throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_TERMINAL: ${workId} is ${work.semanticState}`);
1119
1363
  }
1120
1364
  return work;
1121
1365
  }
1122
1366
 
1123
1367
  function attributedWorkForPluginAction(
1124
1368
  controllerHome: string,
1125
- repository: RepositoryRecord,
1369
+ scope: PluginActionScopeContext,
1126
1370
  action: AssistantPluginActionDescriptor,
1127
1371
  request: AssistantPluginActionRequest,
1128
1372
  ): WorkContract | undefined {
1129
1373
  const workId = request.workId?.trim();
1130
1374
  if (!workId || action.risk === 'remote_write') return undefined;
1131
- const workRepoId = workAttributionRepoId(repository, request);
1375
+ const workRepoId = workAttributionRepoId(scope, request);
1132
1376
  const work = getWorkContract({ controllerHome, repoId: workRepoId }, workId);
1133
1377
  if (!work) throw new Error(`WORK_PLUGIN_ATTRIBUTION_NOT_FOUND: ${workRepoId}:${workId}`);
1134
- if (isTerminalWorkContractStatus(work.status)) {
1135
- throw new Error(`WORK_PLUGIN_ATTRIBUTION_TERMINAL: ${workId} is ${work.status}`);
1378
+ if (isTerminalSemanticWorkState(work.semanticState)) {
1379
+ throw new Error(`WORK_PLUGIN_ATTRIBUTION_TERMINAL: ${workId} is ${work.semanticState}`);
1136
1380
  }
1137
1381
  return work;
1138
1382
  }
1139
1383
 
1384
+ /** Reuse the canonical Work binding check before a Process request ID is reserved. */
1385
+ export function preflightPluginActionWorkBinding(
1386
+ controllerHome: string,
1387
+ pluginScope: { kind: 'controller' } | { kind: 'repository'; repository: RepositoryRecord },
1388
+ action: AssistantPluginActionDescriptor,
1389
+ request: AssistantPluginActionRequest,
1390
+ ): void {
1391
+ if (!request.workId?.trim()) return;
1392
+ const scope = pluginScope.kind === 'controller'
1393
+ ? controllerPluginActionScope(controllerHome)
1394
+ : repositoryPluginActionScope(pluginScope.repository);
1395
+ if (action.risk === 'remote_write') remoteEffectWorkForPluginAction(controllerHome, scope, action, request);
1396
+ else attributedWorkForPluginAction(controllerHome, scope, action, request);
1397
+ }
1398
+
1140
1399
  function bindLocalEffectReceiptToAttributedWork(
1141
1400
  controllerHome: string,
1142
- repository: RepositoryRecord,
1401
+ scope: PluginActionScopeContext,
1143
1402
  action: AssistantPluginActionDescriptor,
1144
1403
  request: AssistantPluginActionRequest,
1145
1404
  receipt: PluginActionReceipt,
1146
1405
  work?: WorkContract,
1147
1406
  ): WorkContract | undefined {
1148
1407
  if (!request.workId || receipt.status !== 'succeeded') return work;
1149
- const workRepoId = workAttributionRepoId(repository, request);
1408
+ const workRepoId = workAttributionRepoId(scope, request);
1150
1409
  const attributed = work ?? getWorkContract({ controllerHome, repoId: workRepoId }, request.workId.trim());
1151
1410
  if (!attributed) throw new Error(`WORK_PLUGIN_ATTRIBUTION_NOT_FOUND: ${workRepoId}:${request.workId.trim()}`);
1152
1411
  if (attributed.workKind !== 'local_effect' || action.readOnly || action.risk === 'readonly') return attributed;
1153
1412
  if (attributed.evidenceRefs.some((evidence) => evidence.evidenceId === receipt.receiptId)) return attributed;
1154
- if (isTerminalWorkContractStatus(attributed.status)) return attributed;
1413
+ if (isTerminalSemanticWorkState(attributed.semanticState)) return attributed;
1155
1414
  return appendWorkEvidence({ controllerHome, repoId: workRepoId }, attributed.workId, {
1156
1415
  evidenceId: receipt.receiptId,
1157
1416
  title: 'typed local plugin effect completed',
@@ -1160,7 +1419,7 @@ function bindLocalEffectReceiptToAttributedWork(
1160
1419
  });
1161
1420
  }
1162
1421
 
1163
- export function finalizeRemoteEffectWorkFromActionReceipt(
1422
+ export function recordRemoteEffectWorkActionReceipt(
1164
1423
  controllerHome: string,
1165
1424
  repoId: string,
1166
1425
  workId: string,
@@ -1170,14 +1429,12 @@ export function finalizeRemoteEffectWorkFromActionReceipt(
1170
1429
  if (work.workKind !== 'remote_effect') {
1171
1430
  throw new Error(`WORK_REMOTE_EFFECT_FINALIZE_KIND_MISMATCH: ${workId} is ${work.workKind}, expected remote_effect`);
1172
1431
  }
1173
- if (work.status === 'completed' && work.completionReceipt?.source === 'remote_effect') return work;
1174
- if (isTerminalWorkContractStatus(work.status)) {
1175
- throw new Error(`WORK_REMOTE_EFFECT_FINALIZE_TERMINAL: ${workId} is ${work.status}`);
1176
- }
1432
+ if (work.completionReceipt?.source === 'remote_effect') return work;
1177
1433
  const receipt = work.evidenceRefs
1178
1434
  .map((evidence) => {
1179
1435
  if (!evidence.evidenceId) return undefined;
1180
1436
  return readPluginActionReceipt(controllerHome, repoId, evidence.evidenceId)
1437
+ ?? readPluginActionReceipt(controllerHome, CONTROLLER_PLUGIN_SCOPE_KEY, evidence.evidenceId)
1181
1438
  ?? readPluginActionReceipt(controllerHome, CONTROLLER_SCOPE_REPO_ID, evidence.evidenceId);
1182
1439
  })
1183
1440
  .find((candidate) => candidate?.workId === workId && candidate.status === 'succeeded');
@@ -1207,17 +1464,17 @@ export function finalizeRemoteEffectWorkFromActionReceipt(
1207
1464
 
1208
1465
  function bindRemoteEffectReceiptToWork(
1209
1466
  controllerHome: string,
1210
- repository: RepositoryRecord,
1467
+ scope: PluginActionScopeContext,
1211
1468
  action: AssistantPluginActionDescriptor,
1212
1469
  request: AssistantPluginActionRequest,
1213
1470
  receipt: PluginActionReceipt,
1214
1471
  ): WorkContract | undefined {
1215
1472
  if (!request.workId) return undefined;
1216
- if (receipt.status !== 'succeeded') return remoteEffectWorkForPluginAction(controllerHome, repository, action, request);
1217
- const work = remoteEffectWorkForPluginAction(controllerHome, repository, action, request, receipt.receiptId);
1473
+ if (receipt.status !== 'succeeded') return remoteEffectWorkForPluginAction(controllerHome, scope, action, request);
1474
+ const work = remoteEffectWorkForPluginAction(controllerHome, scope, action, request, receipt.receiptId);
1218
1475
  if (!work) return undefined;
1219
- if (work.status === 'completed') return work;
1220
- const workRepoId = workAttributionRepoId(repository, request);
1476
+ if (work.completionReceipt?.source === 'remote_effect') return work;
1477
+ const workRepoId = workAttributionRepoId(scope, request);
1221
1478
  let updated = work;
1222
1479
  if (!work.evidenceRefs.some((evidence) => evidence.evidenceId === receipt.receiptId)) {
1223
1480
  updated = appendWorkEvidence({ controllerHome, repoId: workRepoId }, work.workId, {
@@ -1254,7 +1511,7 @@ function compatibilityJobFromReceipt(receipt: PluginActionReceipt, checkoutId: s
1254
1511
  schemaVersion: 1,
1255
1512
  revision: 1,
1256
1513
  jobId: receipt.receiptId,
1257
- repoId: receipt.repoId,
1514
+ repoId: receipt.repoId ?? receipt.scopeKey,
1258
1515
  checkoutId,
1259
1516
  type: 'plugin-action',
1260
1517
  status: receipt.effectOutcome === 'outcome_unknown'
@@ -1286,9 +1543,9 @@ function compatibilityJobFromReceipt(receipt: PluginActionReceipt, checkoutId: s
1286
1543
  * Deterministic plugin action path: validate → authorize/confirm → invoke adapter → receipt.
1287
1544
  * Does not create ExecutionJobs or call a model.
1288
1545
  */
1289
- export async function submitAssistantPluginAction(
1546
+ async function submitPluginAction(
1290
1547
  controllerHome: string,
1291
- repository: RepositoryRecord,
1548
+ scope: PluginActionScopeContext,
1292
1549
  request: AssistantPluginActionRequest,
1293
1550
  ): Promise<{
1294
1551
  manifest: AssistantPluginManifest;
@@ -1302,7 +1559,9 @@ export async function submitAssistantPluginAction(
1302
1559
  }> {
1303
1560
  const adapter = resolvePluginAdapter(controllerHome, request.pluginId);
1304
1561
  if (!adapter) throw new Error(`PLUGIN_NOT_FOUND: ${request.pluginId}`);
1305
- const manifest = getAssistantPluginManifest(controllerHome, repository, request.pluginId);
1562
+ const manifest = scope.kind === 'controller'
1563
+ ? getControllerPluginManifest(controllerHome, request.pluginId)
1564
+ : getAssistantPluginManifest(controllerHome, scope.repository!, request.pluginId);
1306
1565
  const action = actionForManifest(manifest, request.actionId);
1307
1566
  if (!manifest.enabled && action.actionId !== 'configure') {
1308
1567
  throw new Error(`PLUGIN_DISABLED: ${request.pluginId} is disabled`);
@@ -1313,36 +1572,38 @@ export async function submitAssistantPluginAction(
1313
1572
  const normalizedArgs = validateActionArguments(action, request.args ?? {});
1314
1573
  enforceConfirmation(action, { ...request, args: normalizedArgs });
1315
1574
 
1316
- const key = semanticKey(repository, request.pluginId, request.actionId, normalizedArgs);
1575
+ const key = semanticKey(scope, request.pluginId, request.actionId, normalizedArgs);
1317
1576
  const requestPath = pluginActionRequestPath(controllerHome, request.requestId);
1318
1577
  if (existsSync(requestPath)) {
1319
1578
  const index = readJsonFile<PluginActionRequestIndex>(requestPath);
1320
1579
  if (index.semanticKey !== key) {
1321
1580
  throw new Error(`REQUEST_ID_CONFLICT: ${request.requestId} already belongs to ${index.semanticKey}`);
1322
1581
  }
1323
- if (index.repoId !== repository.repoId) {
1324
- throw new Error(`REQUEST_ID_REPO_CONFLICT: ${request.requestId} already belongs to repository ${index.repoId}`);
1582
+ const existingScopeKey = index.scopeKey?.trim() || index.repoId?.trim();
1583
+ if (!existingScopeKey) throw new Error(`PLUGIN_RECEIPT_REQUEST_SCOPE_CORRUPT: ${request.requestId}`);
1584
+ if (existingScopeKey !== scope.scopeKey) {
1585
+ throw new Error(`REQUEST_ID_SCOPE_CONFLICT: ${request.requestId} already belongs to ${existingScopeKey}`);
1325
1586
  }
1326
- const existing = readPluginActionReceipt(controllerHome, index.repoId, index.receiptId);
1587
+ const existing = readPluginActionReceipt(controllerHome, existingScopeKey, index.receiptId);
1327
1588
  if (!existing) {
1328
1589
  throw new Error(`PLUGIN_RECEIPT_LOST: ${request.requestId}`);
1329
1590
  }
1330
1591
  if (request.workId && existing.workId !== request.workId) {
1331
1592
  throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_CONFLICT: ${request.requestId} already belongs to Work ${existing.workId ?? 'none'}`);
1332
1593
  }
1333
- const requestedWorkRepoId = request.workId ? workAttributionRepoId(repository, request) : undefined;
1334
- if (requestedWorkRepoId && (index.workRepoId ?? index.repoId) !== requestedWorkRepoId) {
1335
- throw new Error(`WORK_PLUGIN_ATTRIBUTION_REPO_CONFLICT: ${request.requestId} already belongs to Work repository ${index.workRepoId ?? index.repoId}`);
1594
+ const requestedWorkRepoId = request.workId ? workAttributionRepoId(scope, request) : undefined;
1595
+ if (requestedWorkRepoId && index.workRepoId !== requestedWorkRepoId) {
1596
+ throw new Error(`WORK_PLUGIN_ATTRIBUTION_REPO_CONFLICT: ${request.requestId} already belongs to Work repository ${index.workRepoId ?? 'none'}`);
1336
1597
  }
1337
1598
  if (request.workId && action.risk === 'remote_write') {
1338
- bindRemoteEffectReceiptToWork(controllerHome, repository, action, request, existing);
1599
+ bindRemoteEffectReceiptToWork(controllerHome, scope, action, request, existing);
1339
1600
  } else if (request.workId) {
1340
- bindLocalEffectReceiptToAttributedWork(controllerHome, repository, action, request, existing);
1601
+ bindLocalEffectReceiptToAttributedWork(controllerHome, scope, action, request, existing);
1341
1602
  }
1342
1603
  return {
1343
1604
  manifest,
1344
1605
  action,
1345
- job: compatibilityJobFromReceipt(existing, repository.activeCheckoutId),
1606
+ job: compatibilityJobFromReceipt(existing, scope.checkoutId ?? CONTROLLER_PLUGIN_SCOPE_KEY),
1346
1607
  deduplicated: true,
1347
1608
  result: existing.result,
1348
1609
  receipt: existing,
@@ -1360,7 +1621,7 @@ export async function submitAssistantPluginAction(
1360
1621
  if (requiresAutomatedWriteAuthorization(request.origin)) {
1361
1622
  activeAuthorizationGrant = await resolveAutomatedWriteAuthorization({
1362
1623
  controllerHome,
1363
- repository,
1624
+ scope,
1364
1625
  adapter,
1365
1626
  manifest,
1366
1627
  action,
@@ -1380,77 +1641,33 @@ export async function submitAssistantPluginAction(
1380
1641
  };
1381
1642
  }
1382
1643
  }
1383
- const requiresLocalEffectWork = localSystemActionRequiresWork(
1384
- repository,
1385
- request.pluginId,
1386
- request.actionId,
1387
- normalizedArgs,
1388
- );
1389
1644
  const boundRemoteWork = action.risk === 'remote_write'
1390
- ? remoteEffectWorkForPluginAction(controllerHome, repository, action, request)
1645
+ ? remoteEffectWorkForPluginAction(controllerHome, scope, action, request)
1391
1646
  : undefined;
1392
- const attributedWork = !requiresLocalEffectWork
1393
- ? attributedWorkForPluginAction(controllerHome, repository, action, request)
1647
+ // Capability execution never creates semantic Work implicitly. Existing Work
1648
+ // attribution is opt-in through work_id; otherwise the plugin receipt is the
1649
+ // durable effect record.
1650
+ const attributedWork = action.risk !== 'remote_write'
1651
+ ? attributedWorkForPluginAction(controllerHome, scope, action, request)
1394
1652
  : undefined;
1395
- const acceptedWork = requiresLocalEffectWork
1396
- ? acceptSubmittedWorkContract(controllerHome, {
1397
- requestId: request.requestId,
1398
- repoId: repository.repoId,
1399
- semanticKey: `local-effect:${key}`,
1400
- operation: {
1401
- name: `plugin:${request.pluginId}/${request.actionId}`,
1402
- semanticKey: key,
1403
- argumentHash: createHash('sha256').update(JSON.stringify(normalizedArgs)).digest('hex'),
1404
- mode: 'mutating',
1405
- idempotent: action.idempotent,
1406
- replayable: action.idempotent,
1407
- resourceClaims: claimsForAssistantPluginAction(action, repository, manifest.pluginId),
1408
- },
1409
- objective: `Execute bounded controller-local effect ${request.pluginId}/${request.actionId}`,
1410
- mode: 'direct_control',
1411
- requestedBy: 'chatgpt',
1412
- principalId: request.origin.actor,
1413
- controllerInstanceId: process.env.FORGE_RUNTIME_INSTANCE_ID?.trim()
1414
- || process.env.FORGE_WRITER_INSTANCE_ID?.trim()
1415
- || process.env.FORGE_DAEMON_INSTANCE_ID?.trim(),
1416
- workKind: 'local_effect',
1417
- risk: workRiskForPluginAction(action),
1418
- acceptanceCriteria: ['The requested local effect completes within its authorized Target Grant boundary.'],
1419
- constraints: {
1420
- requireHandoffOnAmbiguity: true,
1421
- allowDestructive: action.risk === 'destructive'
1422
- && request.confirmAuthorization === true
1423
- && request.confirmationText === action.requiredConfirmationText,
1424
- },
1425
- }).contract
1426
- : undefined;
1427
- if (acceptedWork) {
1428
- activateWorkContract(
1429
- { controllerHome, repoId: repository.repoId },
1430
- acceptedWork.workId,
1431
- {
1432
- phase: 'implementation',
1433
- summary: `Controller-local plugin action ${request.pluginId}/${request.actionId} started.`,
1434
- evidenceState: 'partial',
1653
+ if (scope.repository) {
1654
+ appendRuntimeEvent(controllerHome, {
1655
+ repoId: scope.repository.repoId,
1656
+ entityType: 'plugin',
1657
+ entityId: manifest.pluginId,
1658
+ eventType: 'plugin_action_requested',
1659
+ requestId: request.requestId,
1660
+ revision: manifest.revision,
1661
+ data: {
1662
+ actionId: action.actionId,
1663
+ receiptId,
1664
+ risk: action.risk,
1665
+ confirmation: action.confirmation,
1666
+ capabilityId,
1667
+ authorizationReuseSupported: action.confirmation === 'authorization' && Boolean(adapter.resolveAuthorizationContext),
1435
1668
  },
1436
- );
1669
+ });
1437
1670
  }
1438
- appendRuntimeEvent(controllerHome, {
1439
- repoId: repository.repoId,
1440
- entityType: 'plugin',
1441
- entityId: manifest.pluginId,
1442
- eventType: 'plugin_action_requested',
1443
- requestId: request.requestId,
1444
- revision: manifest.revision,
1445
- data: {
1446
- actionId: action.actionId,
1447
- receiptId,
1448
- risk: action.risk,
1449
- confirmation: action.confirmation,
1450
- capabilityId,
1451
- authorizationReuseSupported: action.confirmation === 'authorization' && Boolean(adapter.resolveAuthorizationContext),
1452
- },
1453
- });
1454
1671
 
1455
1672
  try {
1456
1673
  if (action.confirmation === 'authorization'
@@ -1458,8 +1675,7 @@ export async function submitAssistantPluginAction(
1458
1675
  && originMayEstablishCapabilityAuthorization(request.origin)) {
1459
1676
  authorizationContext = await adapter.resolveAuthorizationContext({
1460
1677
  controllerHome,
1461
- repoId: repository.repoId,
1462
- repoRoot: repository.canonicalRoot,
1678
+ ...pluginActionExecutionFields(scope),
1463
1679
  pluginId: request.pluginId,
1464
1680
  actionId: request.actionId,
1465
1681
  requestId: request.requestId,
@@ -1479,7 +1695,7 @@ export async function submitAssistantPluginAction(
1479
1695
  try {
1480
1696
  activeAuthorizationGrant = findActivePluginCapabilityAuthorization(controllerHome, {
1481
1697
  ownerScope: pluginCapabilityAuthorizationOwnerScope(request.origin),
1482
- repoId: repository.repoId,
1698
+ repoId: scope.grantRepoId,
1483
1699
  pluginId: request.pluginId,
1484
1700
  capabilityId,
1485
1701
  target: authorizationContext.target,
@@ -1504,15 +1720,21 @@ export async function submitAssistantPluginAction(
1504
1720
  }
1505
1721
  }
1506
1722
 
1723
+ if (action.requiresExplicitCapabilityGrant && !activeAuthorizationGrant) {
1724
+ throw new AssistantPluginError(
1725
+ 'PLUGIN_CAPABILITY_GRANT_REQUIRED',
1726
+ `${manifest.pluginId}/${action.actionId} requires an explicit reusable capability grant for its exact target. Run the dedicated authorization action first.`,
1727
+ );
1728
+ }
1729
+
1507
1730
  const result = await withAssistantPluginResourceLeases(
1508
1731
  controllerHome,
1509
- repository,
1732
+ scope,
1510
1733
  action,
1511
1734
  request,
1512
- () => executeAssistantPluginAction({
1735
+ () => executePluginActionInScope(scope, {
1513
1736
  controllerHome,
1514
- repoId: repository.repoId,
1515
- repoRoot: repository.canonicalRoot,
1737
+ ...pluginActionExecutionFields(scope),
1516
1738
  pluginId: request.pluginId,
1517
1739
  actionId: request.actionId,
1518
1740
  requestId: request.requestId,
@@ -1527,6 +1749,31 @@ export async function submitAssistantPluginAction(
1527
1749
  : undefined,
1528
1750
  }),
1529
1751
  );
1752
+ // create_session materializes the exact provider/session identity during
1753
+ // execution, so its authorization target cannot always be resolved before
1754
+ // the action runs. Resolve that postcondition once after a successful
1755
+ // interactive bootstrap, then persist the same exact-target grant path as
1756
+ // every other authorization-class Browser action. Scheduled execution
1757
+ // still remains grant-only because this branch is interactive-origin only.
1758
+ if (action.actionId === 'create_session'
1759
+ && action.confirmation === 'authorization'
1760
+ && !authorizationContext
1761
+ && originMayEstablishCapabilityAuthorization(request.origin)
1762
+ && adapter.resolveAuthorizationContext) {
1763
+ authorizationContext = await adapter.resolveAuthorizationContext({
1764
+ controllerHome,
1765
+ ...pluginActionExecutionFields(scope),
1766
+ pluginId: request.pluginId,
1767
+ actionId: request.actionId,
1768
+ requestId: request.requestId,
1769
+ args: normalizedArgs,
1770
+ origin: request.origin,
1771
+ jobId: receiptId,
1772
+ timeoutMs: request.timeoutMs,
1773
+ signal: request.signal,
1774
+ });
1775
+ }
1776
+
1530
1777
  if (action.confirmation === 'authorization'
1531
1778
  && authorizationContext
1532
1779
  && !activeAuthorizationGrant
@@ -1534,7 +1781,7 @@ export async function submitAssistantPluginAction(
1534
1781
  try {
1535
1782
  const grant = recordPluginCapabilityAuthorization(controllerHome, {
1536
1783
  ownerScope: pluginCapabilityAuthorizationOwnerScope(request.origin),
1537
- repoId: repository.repoId,
1784
+ repoId: scope.grantRepoId,
1538
1785
  pluginId: request.pluginId,
1539
1786
  capabilityId,
1540
1787
  target: authorizationContext.target,
@@ -1568,42 +1815,8 @@ export async function submitAssistantPluginAction(
1568
1815
  work: {
1569
1816
  workId: boundRemoteWork.workId,
1570
1817
  workKind: 'remote_effect',
1818
+ semanticState: 'open' as const,
1571
1819
  ...(terminalRemoteEffect ? { completionOutcome: 'completed_remote' as const } : {}),
1572
- status: terminalRemoteEffect ? 'completed' : 'running',
1573
- },
1574
- };
1575
- }
1576
- if (acceptedWork) {
1577
- const recordedAt = new Date().toISOString();
1578
- const target = localEffectTarget(normalizedArgs, result);
1579
- appendWorkEvidence({ controllerHome, repoId: repository.repoId }, acceptedWork.workId, {
1580
- title: 'controller-local effect completed',
1581
- summary: `${request.pluginId}/${request.actionId} completed for ${target.id}.`,
1582
- detailLevel: 'summary',
1583
- });
1584
- recordWorkCompletionReceipt(
1585
- { controllerHome, repoId: repository.repoId },
1586
- acceptedWork.workId,
1587
- {
1588
- schemaVersion: 1,
1589
- receiptId: `LFX-${Date.now()}-${createHash('sha256').update(`${request.requestId}:${acceptedWork.workId}`).digest('hex').slice(0, 8)}`,
1590
- source: 'local_effect',
1591
- workId: acceptedWork.workId,
1592
- operation: `${request.pluginId}/${request.actionId}`,
1593
- target,
1594
- changed: true,
1595
- recordedAt,
1596
- },
1597
- 'completed_local',
1598
- 'local_effect',
1599
- );
1600
- resultWithLineage = {
1601
- ...result,
1602
- work: {
1603
- workId: acceptedWork.workId,
1604
- workKind: 'local_effect',
1605
- completionOutcome: 'completed_local',
1606
- status: 'completed',
1607
1820
  },
1608
1821
  };
1609
1822
  }
@@ -1611,48 +1824,50 @@ export async function submitAssistantPluginAction(
1611
1824
  schemaVersion: 1,
1612
1825
  receiptId,
1613
1826
  requestId: request.requestId,
1614
- repoId: repository.repoId,
1615
- ...(request.workId ? { workRepoId: workAttributionRepoId(repository, request) } : {}),
1827
+ scopeKey: scope.scopeKey,
1828
+ ...(scope.repository ? { repoId: scope.repository.repoId } : {}),
1829
+ ...(request.workId ? { workRepoId: workAttributionRepoId(scope, request) } : {}),
1616
1830
  pluginId: request.pluginId,
1617
1831
  actionId: request.actionId,
1618
1832
  semanticKey: key,
1619
1833
  status: 'succeeded',
1620
1834
  createdAt,
1621
- ...(acceptedWork
1622
- ? { workId: acceptedWork.workId }
1623
- : boundRemoteWork
1624
- ? { workId: boundRemoteWork.workId }
1625
- : attributedWork
1626
- ? { workId: attributedWork.workId }
1627
- : {}),
1835
+ ...(boundRemoteWork
1836
+ ? { workId: boundRemoteWork.workId }
1837
+ : attributedWork
1838
+ ? { workId: attributedWork.workId }
1839
+ : {}),
1628
1840
  origin: request.origin,
1629
1841
  authorization,
1630
1842
  result: resultWithLineage,
1631
1843
  };
1632
- writeJsonAtomic(pluginActionReceiptPath(controllerHome, repository.repoId, receiptId), receipt);
1844
+ writeJsonAtomic(pluginActionReceiptPath(controllerHome, scope.scopeKey, receiptId), receipt);
1633
1845
  writeJsonAtomic(requestPath, {
1634
1846
  requestId: request.requestId,
1635
- repoId: repository.repoId,
1636
- ...(request.workId ? { workRepoId: workAttributionRepoId(repository, request) } : {}),
1847
+ scopeKey: scope.scopeKey,
1848
+ ...(scope.repository ? { repoId: scope.repository.repoId } : {}),
1849
+ ...(request.workId ? { workRepoId: workAttributionRepoId(scope, request) } : {}),
1637
1850
  receiptId,
1638
1851
  semanticKey: key,
1639
1852
  createdAt,
1640
1853
  } satisfies PluginActionRequestIndex);
1641
1854
  if (boundRemoteWork) {
1642
- bindRemoteEffectReceiptToWork(controllerHome, repository, action, request, receipt);
1855
+ bindRemoteEffectReceiptToWork(controllerHome, scope, action, request, receipt);
1643
1856
  } else if (attributedWork) {
1644
- bindLocalEffectReceiptToAttributedWork(controllerHome, repository, action, request, receipt, attributedWork);
1857
+ bindLocalEffectReceiptToAttributedWork(controllerHome, scope, action, request, receipt, attributedWork);
1645
1858
  }
1646
- const nextManifest = getAssistantPluginManifest(controllerHome, repository, request.pluginId);
1859
+ const nextManifest = scope.kind === 'controller'
1860
+ ? getControllerPluginManifest(controllerHome, request.pluginId)
1861
+ : getAssistantPluginManifest(controllerHome, scope.repository!, request.pluginId);
1647
1862
  return {
1648
1863
  manifest: nextManifest,
1649
1864
  action,
1650
- job: compatibilityJobFromReceipt(receipt, repository.activeCheckoutId),
1865
+ job: compatibilityJobFromReceipt(receipt, scope.checkoutId ?? CONTROLLER_PLUGIN_SCOPE_KEY),
1651
1866
  deduplicated: false,
1652
1867
  result: resultWithLineage,
1653
1868
  receipt,
1654
1869
  authorization,
1655
- workId: acceptedWork?.workId ?? boundRemoteWork?.workId ?? attributedWork?.workId,
1870
+ workId: boundRemoteWork?.workId ?? attributedWork?.workId,
1656
1871
  };
1657
1872
  } catch (error) {
1658
1873
  const message = error instanceof Error ? error.message : String(error);
@@ -1663,69 +1878,60 @@ export async function submitAssistantPluginAction(
1663
1878
  ? error.effectOutcome
1664
1879
  : 'failed';
1665
1880
  const outcomeUnknown = effectOutcome === 'outcome_unknown';
1881
+ const failure: NonNullable<PluginActionReceipt['error']> = {
1882
+ code,
1883
+ message: redactSensitiveText(message).text,
1884
+ ...(isAssistantPluginError(error) ? { retryable: error.retryable } : {}),
1885
+ };
1886
+ // Error evidence stays compact; oversized provider diagnostics must not
1887
+ // turn failure settlement into an unbounded persistence write.
1888
+ if (isAssistantPluginError(error) && error.details) {
1889
+ try {
1890
+ const details = redactSensitiveValue(error.details).value;
1891
+ if (Buffer.byteLength(JSON.stringify(details), 'utf8') <= 8_192) failure.details = details;
1892
+ } catch {
1893
+ // Non-JSON provider diagnostics cannot override the settled failure.
1894
+ }
1895
+ }
1666
1896
  const outcomeResult = outcomeUnknown
1667
- ? { outcome: 'outcome_unknown', error: { code, message } }
1897
+ ? { outcome: 'outcome_unknown', error: failure }
1668
1898
  : undefined;
1669
1899
  if (boundRemoteWork) {
1670
- appendWorkEvidence({ controllerHome, repoId: workAttributionRepoId(repository, request) }, boundRemoteWork.workId, {
1900
+ appendWorkEvidence({ controllerHome, repoId: workAttributionRepoId(scope, request) }, boundRemoteWork.workId, {
1671
1901
  title: outcomeUnknown ? 'typed remote plugin effect outcome unknown' : 'typed remote plugin effect failed',
1672
1902
  summary: `${request.pluginId}/${request.actionId}: ${message}`.slice(0, 1_000),
1673
1903
  detailLevel: 'summary',
1674
1904
  });
1675
1905
  }
1676
- if (acceptedWork) {
1677
- if (outcomeUnknown) {
1678
- appendWorkEvidence({ controllerHome, repoId: repository.repoId }, acceptedWork.workId, {
1679
- title: 'controller-local effect outcome unknown',
1680
- summary: `${request.pluginId}/${request.actionId}: ${message}`.slice(0, 1_000),
1681
- detailLevel: 'summary',
1682
- });
1683
- } else {
1684
- const current = getWorkContract({ controllerHome, repoId: repository.repoId }, acceptedWork.workId);
1685
- failWorkContract(
1686
- { controllerHome, repoId: repository.repoId },
1687
- acceptedWork.workId,
1688
- {
1689
- phase: current?.phase ?? 'implementation',
1690
- summary: `Controller-local plugin action failed: ${request.pluginId}/${request.actionId}: ${message}`.slice(0, 1_000),
1691
- evidenceRefs: [{
1692
- title: 'controller-local effect failed',
1693
- summary: `${request.pluginId}/${request.actionId}: ${message}`.slice(0, 1_000),
1694
- detailLevel: 'summary',
1695
- }, ...(current?.evidenceRefs ?? [])],
1696
- },
1697
- );
1698
- }
1699
- }
1700
1906
  const receipt: PluginActionReceipt = {
1701
1907
  schemaVersion: 1,
1702
1908
  receiptId,
1703
1909
  requestId: request.requestId,
1704
- repoId: repository.repoId,
1705
- ...(request.workId ? { workRepoId: workAttributionRepoId(repository, request) } : {}),
1910
+ scopeKey: scope.scopeKey,
1911
+ ...(scope.repository ? { repoId: scope.repository.repoId } : {}),
1912
+ ...(request.workId ? { workRepoId: workAttributionRepoId(scope, request) } : {}),
1706
1913
  pluginId: request.pluginId,
1707
1914
  actionId: request.actionId,
1708
1915
  semanticKey: key,
1709
1916
  status: 'failed',
1710
1917
  effectOutcome,
1711
1918
  createdAt,
1712
- ...(acceptedWork
1713
- ? { workId: acceptedWork.workId }
1714
- : boundRemoteWork
1715
- ? { workId: boundRemoteWork.workId }
1716
- : attributedWork
1717
- ? { workId: attributedWork.workId }
1718
- : {}),
1919
+ ...(boundRemoteWork
1920
+ ? { workId: boundRemoteWork.workId }
1921
+ : attributedWork
1922
+ ? { workId: attributedWork.workId }
1923
+ : {}),
1719
1924
  origin: request.origin,
1720
1925
  authorization,
1721
1926
  ...(outcomeResult ? { result: outcomeResult } : {}),
1722
- error: { code, message },
1927
+ error: failure,
1723
1928
  };
1724
- writeJsonAtomic(pluginActionReceiptPath(controllerHome, repository.repoId, receiptId), receipt);
1929
+ writeJsonAtomic(pluginActionReceiptPath(controllerHome, scope.scopeKey, receiptId), receipt);
1725
1930
  writeJsonAtomic(requestPath, {
1726
1931
  requestId: request.requestId,
1727
- repoId: repository.repoId,
1728
- ...(request.workId ? { workRepoId: workAttributionRepoId(repository, request) } : {}),
1932
+ scopeKey: scope.scopeKey,
1933
+ ...(scope.repository ? { repoId: scope.repository.repoId } : {}),
1934
+ ...(request.workId ? { workRepoId: workAttributionRepoId(scope, request) } : {}),
1729
1935
  receiptId,
1730
1936
  semanticKey: key,
1731
1937
  createdAt,
@@ -1734,40 +1940,42 @@ export async function submitAssistantPluginAction(
1734
1940
  return {
1735
1941
  manifest,
1736
1942
  action,
1737
- job: compatibilityJobFromReceipt(receipt, repository.activeCheckoutId),
1943
+ job: compatibilityJobFromReceipt(receipt, scope.checkoutId ?? CONTROLLER_PLUGIN_SCOPE_KEY),
1738
1944
  deduplicated: false,
1739
1945
  result: outcomeResult,
1740
1946
  receipt,
1741
1947
  authorization,
1742
- workId: acceptedWork?.workId ?? boundRemoteWork?.workId ?? attributedWork?.workId,
1948
+ workId: boundRemoteWork?.workId ?? attributedWork?.workId,
1743
1949
  };
1744
1950
  }
1745
1951
  throw error;
1746
1952
  }
1747
1953
  }
1748
1954
 
1749
- export async function executeControllerScopedPluginAction(
1750
- input: Omit<AssistantPluginActionExecutionInput, 'repoId' | 'repoRoot'>,
1751
- ): Promise<Record<string, unknown>> {
1752
- const repository = controllerPluginRepository(input.controllerHome);
1753
- return executeAssistantPluginAction({
1754
- ...input,
1755
- repoId: repository.repoId,
1756
- repoRoot: repository.canonicalRoot,
1757
- });
1955
+ export function submitAssistantPluginAction(
1956
+ controllerHome: string,
1957
+ repository: RepositoryRecord,
1958
+ request: AssistantPluginActionRequest,
1959
+ ) {
1960
+ return submitPluginAction(controllerHome, repositoryPluginActionScope(repository), request);
1758
1961
  }
1759
1962
 
1760
- export async function executeAssistantPluginAction(
1963
+ export function submitControllerPluginAction(
1964
+ controllerHome: string,
1965
+ request: AssistantPluginActionRequest,
1966
+ ) {
1967
+ return submitPluginAction(controllerHome, controllerPluginActionScope(controllerHome), request);
1968
+ }
1969
+
1970
+ async function executePluginActionInScope(
1971
+ scope: PluginActionScopeContext,
1761
1972
  input: AssistantPluginActionExecutionInput,
1762
1973
  ): Promise<Record<string, unknown>> {
1763
1974
  const adapter = resolvePluginAdapter(input.controllerHome, input.pluginId);
1764
1975
  if (!adapter) throw new Error(`PLUGIN_NOT_FOUND: ${input.pluginId}`);
1765
- const repository = {
1766
- repoId: input.repoId,
1767
- canonicalRoot: input.repoRoot,
1768
- activeCheckoutId: 'active',
1769
- } as RepositoryRecord;
1770
- const manifestLookup = getAssistantPluginManifestForExecution(input.controllerHome, repository, input.pluginId, adapter);
1976
+ const manifestLookup = scope.kind === 'controller'
1977
+ ? getControllerPluginManifestForExecution(input.controllerHome, input.pluginId, adapter)
1978
+ : getAssistantPluginManifestForExecution(input.controllerHome, scope.repository!, input.pluginId, adapter);
1771
1979
  const manifest = manifestLookup.manifest;
1772
1980
  const action = actionForManifest(manifest, input.actionId);
1773
1981
  if (action.executionMode === 'direct_non_persistent') {
@@ -1777,7 +1985,7 @@ export async function executeAssistantPluginAction(
1777
1985
  if (requiresAutomatedWriteAuthorization(input.origin)) {
1778
1986
  await resolveAutomatedWriteAuthorization({
1779
1987
  controllerHome: input.controllerHome,
1780
- repository,
1988
+ scope,
1781
1989
  adapter,
1782
1990
  manifest,
1783
1991
  action,
@@ -1791,38 +1999,53 @@ export async function executeAssistantPluginAction(
1791
1999
  try {
1792
2000
  const result = await adapter.executeAction({
1793
2001
  ...input,
2002
+ ...pluginActionExecutionFields(scope),
1794
2003
  args: normalizedArgs,
1795
2004
  providerIdentityPrevalidated: manifestLookup.providerIdentityPrevalidated,
1796
2005
  });
1797
2006
  for (const affectedPluginId of adapter.affectedPluginIdsAfterAction?.(input.actionId, result) ?? []) {
1798
2007
  if (!affectedPluginId || affectedPluginId === input.pluginId) continue;
1799
- invalidateAssistantPluginManifestCache(input.controllerHome, repository.repoId, affectedPluginId);
1800
2008
  const affectedAdapter = resolvePluginAdapter(input.controllerHome, affectedPluginId);
1801
- if (affectedAdapter && adapterMatchesRepository(affectedAdapter, repository)) {
1802
- syncAssistantPluginManifest(input.controllerHome, repository, affectedPluginId);
2009
+ if (scope.kind === 'controller') {
2010
+ invalidateAssistantPluginManifestCache(input.controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, affectedPluginId);
2011
+ if (affectedAdapter && adapterMatchesController(affectedAdapter)) {
2012
+ syncControllerPluginManifest(input.controllerHome, affectedPluginId);
2013
+ } else {
2014
+ removeControllerPluginManifestProjection(input.controllerHome, affectedPluginId);
2015
+ }
1803
2016
  } else {
1804
- rmSync(manifestPath(input.controllerHome, repository.repoId, affectedPluginId), { force: true });
2017
+ const repository = scope.repository!;
2018
+ invalidateAssistantPluginManifestCache(input.controllerHome, repository.repoId, affectedPluginId);
2019
+ if (affectedAdapter && adapterMatchesRepository(affectedAdapter, repository)) {
2020
+ syncAssistantPluginManifest(input.controllerHome, repository, affectedPluginId);
2021
+ } else {
2022
+ rmSync(manifestPath(input.controllerHome, repository.repoId, affectedPluginId), { force: true });
2023
+ }
1805
2024
  }
1806
2025
  }
1807
2026
  const refreshManifest = adapter.shouldRefreshManifestAfterAction?.(input.actionId) ?? true;
1808
2027
  const nextManifest = refreshManifest
1809
- ? syncAssistantPluginManifest(input.controllerHome, repository, input.pluginId).manifest
2028
+ ? scope.kind === 'controller'
2029
+ ? syncControllerPluginManifest(input.controllerHome, input.pluginId).manifest
2030
+ : syncAssistantPluginManifest(input.controllerHome, scope.repository!, input.pluginId).manifest
1810
2031
  : manifest;
1811
- appendRuntimeEvent(input.controllerHome, {
1812
- repoId: input.repoId,
1813
- entityType: 'plugin',
1814
- entityId: input.pluginId,
1815
- eventType: 'plugin_action_succeeded',
1816
- requestId: input.requestId,
1817
- revision: nextManifest.revision,
1818
- data: {
1819
- actionId: input.actionId,
1820
- jobId: input.jobId,
1821
- resultKeys: Object.keys(result).slice(0, 20),
1822
- lifecycleState: nextManifest.lifecycle.state,
1823
- healthState: nextManifest.health.state,
1824
- },
1825
- });
2032
+ if (scope.repository) {
2033
+ appendRuntimeEvent(input.controllerHome, {
2034
+ repoId: scope.repository.repoId,
2035
+ entityType: 'plugin',
2036
+ entityId: input.pluginId,
2037
+ eventType: 'plugin_action_succeeded',
2038
+ requestId: input.requestId,
2039
+ revision: nextManifest.revision,
2040
+ data: {
2041
+ actionId: input.actionId,
2042
+ jobId: input.jobId,
2043
+ resultKeys: Object.keys(result).slice(0, 20),
2044
+ lifecycleState: nextManifest.lifecycle.state,
2045
+ healthState: nextManifest.health.state,
2046
+ },
2047
+ });
2048
+ }
1826
2049
  return {
1827
2050
  schemaVersion: 1,
1828
2051
  plugin: {
@@ -1850,25 +2073,49 @@ export async function executeAssistantPluginAction(
1850
2073
  actionId: input.actionId,
1851
2074
  },
1852
2075
  });
1853
- const refreshed = syncAssistantPluginManifest(input.controllerHome, repository, input.pluginId);
1854
- const nextManifest = refreshed.manifest;
1855
- appendRuntimeEvent(input.controllerHome, {
1856
- repoId: input.repoId,
1857
- entityType: 'plugin',
1858
- entityId: input.pluginId,
1859
- eventType: 'plugin_action_failed',
1860
- requestId: input.requestId,
1861
- revision: nextManifest.revision,
1862
- data: {
1863
- actionId: input.actionId,
1864
- jobId: input.jobId,
1865
- code: pluginError.code,
1866
- retryable: pluginError.retryable,
1867
- },
1868
- });
2076
+ const nextManifest = scope.kind === 'controller'
2077
+ ? syncControllerPluginManifest(input.controllerHome, input.pluginId).manifest
2078
+ : syncAssistantPluginManifest(input.controllerHome, scope.repository!, input.pluginId).manifest;
2079
+ if (scope.repository) {
2080
+ appendRuntimeEvent(input.controllerHome, {
2081
+ repoId: scope.repository.repoId,
2082
+ entityType: 'plugin',
2083
+ entityId: input.pluginId,
2084
+ eventType: 'plugin_action_failed',
2085
+ requestId: input.requestId,
2086
+ revision: nextManifest.revision,
2087
+ data: {
2088
+ actionId: input.actionId,
2089
+ jobId: input.jobId,
2090
+ code: pluginError.code,
2091
+ retryable: pluginError.retryable,
2092
+ },
2093
+ });
2094
+ }
1869
2095
  throw new AssistantPluginError(pluginError.code, pluginError.message.replace(/^[^:]+:\s*/, ''), {
1870
2096
  retryable: pluginError.retryable,
1871
2097
  details: pluginError.details,
1872
2098
  });
1873
2099
  }
1874
2100
  }
2101
+
2102
+ export function executeControllerScopedPluginAction(
2103
+ input: Omit<AssistantPluginActionExecutionInput, 'repoId' | 'repoRoot'>,
2104
+ ): Promise<Record<string, unknown>> {
2105
+ const scope = controllerPluginActionScope(input.controllerHome);
2106
+ return executePluginActionInScope(scope, {
2107
+ ...input,
2108
+ ...pluginActionExecutionFields(scope),
2109
+ });
2110
+ }
2111
+
2112
+ export function executeAssistantPluginAction(
2113
+ input: AssistantPluginActionExecutionInput,
2114
+ ): Promise<Record<string, unknown>> {
2115
+ const repository = {
2116
+ repoId: input.repoId,
2117
+ canonicalRoot: input.repoRoot,
2118
+ activeCheckoutId: 'active',
2119
+ } as RepositoryRecord;
2120
+ return executePluginActionInScope(repositoryPluginActionScope(repository), input);
2121
+ }