@moretea-labs/forge 1.8.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (528) hide show
  1. package/AGENTS.md +70 -7
  2. package/CHANGELOG.md +14 -0
  3. package/CLAUDE.md +17 -2
  4. package/CONTRIBUTING.md +3 -2
  5. package/THIRD_PARTY_NOTICES.md +1 -1
  6. package/adapters/chatgpt/browser-delivery-host.ts +7 -1
  7. package/adapters/chatgpt/browser-delivery-runtime.ts +529 -56
  8. package/adapters/chatgpt/controller-binding-store.ts +101 -22
  9. package/adapters/chatgpt/controller-round-host.ts +15 -25
  10. package/adapters/chatgpt/controller-round-settlement-store.ts +58 -4
  11. package/adapters/chatgpt/provider-delivery.ts +228 -1
  12. package/adapters/chatgpt/work-conversation-binding-store.ts +79 -53
  13. package/adapters/computer/chatgpt-extension-target.ts +463 -0
  14. package/adapters/computer/chatgpt-target-router.ts +103 -0
  15. package/adapters/computer/chrome-extension/background.js +385 -0
  16. package/adapters/computer/chrome-extension/content.js +378 -0
  17. package/adapters/computer/chrome-extension/core.js +24 -0
  18. package/adapters/computer/chrome-extension/manifest.json +30 -0
  19. package/adapters/computer/desktop-operator-discovery.ts +11 -31
  20. package/adapters/computer/interaction-target-authority.ts +142 -18
  21. package/adapters/mcp/auth.ts +0 -1
  22. package/adapters/mcp/instructions.ts +5 -4
  23. package/adapters/mcp/multi-repository.ts +3 -19
  24. package/adapters/mcp/oauth.ts +73 -0
  25. package/adapters/mcp/paths.ts +4 -1
  26. package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +87 -0
  27. package/adapters/mcp/runtime-gateway/bounded-result-runtime.ts +62 -0
  28. package/adapters/mcp/runtime-gateway/cognition-settlement.ts +201 -0
  29. package/adapters/mcp/runtime-gateway/context-adapter.ts +411 -73
  30. package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +91 -0
  31. package/adapters/mcp/runtime-gateway/controller-authority-adapter.ts +20 -5
  32. package/adapters/mcp/runtime-gateway/core-capability-adapter.ts +258 -0
  33. package/adapters/mcp/runtime-gateway/engineering-preconditions.ts +10 -27
  34. package/adapters/mcp/runtime-gateway/engineering-tool-contract.ts +15 -0
  35. package/adapters/mcp/runtime-gateway/execution-tools.ts +8 -42
  36. package/adapters/mcp/runtime-gateway/gateway-execution-adapter.ts +2 -1
  37. package/adapters/mcp/runtime-gateway/plugin-adapter.ts +117 -45
  38. package/adapters/mcp/runtime-gateway/process-tools.ts +190 -22
  39. package/adapters/mcp/runtime-gateway/protected-computer-adapter.ts +16 -4
  40. package/adapters/mcp/runtime-gateway/recovery-adapter.ts +69 -52
  41. package/adapters/mcp/runtime-gateway/repository-compat-adapter.ts +4 -35
  42. package/adapters/mcp/runtime-gateway/result-adapter.ts +10 -4
  43. package/adapters/mcp/runtime-gateway/router.ts +18 -504
  44. package/adapters/mcp/runtime-gateway/routing-policy.ts +499 -0
  45. package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +14 -65
  46. package/adapters/mcp/runtime-gateway/runtime-readiness-observation.ts +218 -0
  47. package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +113 -173
  48. package/adapters/mcp/runtime-gateway/runtime-tools.ts +15 -5
  49. package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +1 -0
  50. package/adapters/mcp/runtime-gateway/shared-adapter.ts +20 -0
  51. package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +358 -266
  52. package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +136 -0
  53. package/adapters/mcp/runtime-gateway/work-adapter.ts +261 -3122
  54. package/adapters/mcp/runtime-gateway/work-controller-operations.ts +257 -0
  55. package/adapters/mcp/runtime-gateway/work-detail-projection.ts +60 -0
  56. package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +246 -0
  57. package/adapters/mcp/runtime-gateway/work-learning-operations.ts +145 -0
  58. package/adapters/mcp/runtime-gateway/work-list-projection.ts +38 -0
  59. package/adapters/mcp/runtime-gateway/work-plan-operations.ts +382 -0
  60. package/adapters/mcp/runtime-gateway/work-repair-adapter.ts +308 -0
  61. package/adapters/mcp/runtime-gateway/work-requirement-operations.ts +148 -0
  62. package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +263 -0
  63. package/adapters/mcp/runtime-gateway/work-workflow-operations.ts +113 -0
  64. package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +156 -0
  65. package/adapters/mcp/server.ts +67 -23
  66. package/adapters/mcp/setup.ts +1 -3
  67. package/adapters/mcp/tool-mapping/legacy-tool-service.ts +44 -170
  68. package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +4 -9
  69. package/adapters/mcp/tool-mapping/repository-tools.ts +133 -142
  70. package/adapters/mcp/toolset-names.ts +5 -0
  71. package/adapters/mcp/toolset.ts +6 -5
  72. package/adapters/mcp/transports/http-observation.ts +365 -0
  73. package/adapters/mcp/transports/http.ts +154 -677
  74. package/adapters/mcp/transports/oauth-http.ts +322 -0
  75. package/adapters/mcp/transports/session-registry.ts +6 -0
  76. package/adapters/mcp/tunnels/openai-secure-tunnel.ts +89 -5
  77. package/assets/plugin-registry.v1.json +6 -4
  78. package/assets/reference-configs/agentic-development-flow.md +25 -0
  79. package/assets/reference-configs/global-working-rules.md +19 -0
  80. package/assets/skill-version.json +2 -2
  81. package/docs/architecture/CURRENT.md +111 -41
  82. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +14 -5
  83. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +6 -3
  84. package/docs/architecture/decisions/20260907-assistant-learning-loop.md +2 -0
  85. package/docs/architecture/decisions/20260907-controller-round-transition-policy.md +19 -4
  86. package/docs/architecture/decisions/20260916-workflow-supervisor-authority.md +249 -0
  87. package/docs/architecture/decisions/20260917-cognitive-memory-foundation.md +82 -0
  88. package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +221 -0
  89. package/docs/architecture/decisions/20260919-autonomous-work-liveness.md +62 -0
  90. package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +72 -0
  91. package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +237 -0
  92. package/docs/architecture/decisions/20261005-forge-v3-desktop-client-boundary.md +77 -0
  93. package/docs/architecture/domains/verification.md +1 -0
  94. package/docs/architecture/domains/workflow-engine.md +6 -0
  95. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
  96. package/docs/forge-plugin-management.md +2 -2
  97. package/docs/operations/controller-performance-and-502.md +17 -0
  98. package/docs/operations/controller-reliability-runbook.md +2 -2
  99. package/docs/operations/features.md +1 -1
  100. package/docs/operations/features.zh-CN.md +1 -1
  101. package/docs/operations/homebrew.md +2 -2
  102. package/docs/operations/mcp-tool-exposure.md +2 -0
  103. package/docs/operations/releasing.md +2 -0
  104. package/docs/operations/standalone-disaster-recovery.md +14 -6
  105. package/docs/wiki/Implementation.md +1 -1
  106. package/package.json +25 -14
  107. package/packages/kernel/cognition/api/index.ts +6 -0
  108. package/packages/kernel/cognition/application/activation-service.ts +451 -0
  109. package/packages/kernel/cognition/application/consolidation-service.ts +185 -0
  110. package/packages/kernel/cognition/application/legacy-experience-adapter.ts +38 -0
  111. package/packages/kernel/cognition/application/memory-service.ts +64 -0
  112. package/packages/kernel/cognition/domain/learning.ts +106 -0
  113. package/packages/kernel/cognition/domain/memory.ts +254 -0
  114. package/packages/kernel/controller/api/index.ts +1 -0
  115. package/packages/kernel/controller/application/continuation-service.ts +281 -0
  116. package/packages/kernel/controller/application/controller-round-service.ts +12 -0
  117. package/packages/kernel/controller/domain/controller-round-transition-policy.ts +242 -21
  118. package/packages/kernel/controller/domain/controller-round.ts +4 -1
  119. package/packages/kernel/controller/domain/execution-quality.ts +3 -1
  120. package/packages/kernel/controller/infrastructure/controller-round-store.ts +689 -103
  121. package/packages/kernel/controller/infrastructure/controller-session-store.ts +297 -71
  122. package/packages/kernel/controller/ports/controller-host.ts +2 -0
  123. package/packages/kernel/identity/api/index.ts +10 -0
  124. package/packages/kernel/identity/domain/activity-projection.ts +43 -0
  125. package/packages/kernel/identity/domain/capability-broker.ts +60 -0
  126. package/packages/kernel/identity/domain/grant.ts +72 -0
  127. package/packages/kernel/identity/domain/owned-resource.ts +116 -0
  128. package/packages/kernel/identity/domain/user-request.ts +93 -0
  129. package/packages/kernel/identity/infrastructure/activity-projection-store.ts +77 -0
  130. package/packages/kernel/identity/infrastructure/capability-broker.ts +81 -0
  131. package/packages/kernel/identity/infrastructure/grant-store.ts +284 -0
  132. package/packages/kernel/identity/infrastructure/owned-resource-store.ts +259 -0
  133. package/packages/kernel/identity/infrastructure/user-request-store.ts +182 -0
  134. package/packages/kernel/scheduler/api/index.ts +0 -2
  135. package/packages/kernel/scheduler/application/schedule-service.ts +3 -0
  136. package/packages/kernel/scheduler/application/settlement.ts +0 -15
  137. package/packages/kernel/scheduler/domain/schedule.ts +87 -0
  138. package/packages/kernel/scheduler/infrastructure/schedule-store.ts +153 -133
  139. package/packages/kernel/work/api/index.ts +3 -2
  140. package/packages/kernel/work/application/work-service.ts +17 -42
  141. package/packages/kernel/work/domain/current-task-lineage.ts +104 -0
  142. package/packages/kernel/work/domain/engineering-design.ts +10 -5
  143. package/packages/kernel/work/domain/engineering-profile.ts +8 -12
  144. package/packages/kernel/work/domain/evaluation-promotion-receipt.ts +186 -0
  145. package/packages/kernel/work/domain/execution-concurrency.ts +19 -26
  146. package/packages/kernel/work/domain/execution-snapshot.ts +0 -31
  147. package/packages/kernel/work/domain/implementation-review.ts +14 -331
  148. package/packages/kernel/work/domain/objective-graph.ts +119 -0
  149. package/packages/kernel/work/domain/project-engineering-contract.ts +23 -1
  150. package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
  151. package/packages/kernel/work/domain/state-machine.ts +25 -63
  152. package/packages/kernel/work/domain/types.ts +57 -51
  153. package/packages/kernel/work/infrastructure/work-contract-store.ts +819 -728
  154. package/packages/kernel/work/ports/work-contract-store.ts +5 -2
  155. package/packages/plugin-runtime/browser/session-authority.ts +2 -1
  156. package/packages/plugin-runtime/computer/chatgpt-identity.ts +24 -0
  157. package/packages/plugin-runtime/computer/conversation-target.ts +143 -0
  158. package/packages/plugin-runtime/computer/index.ts +2 -0
  159. package/packages/plugin-runtime/computer/target-authority.ts +26 -1
  160. package/packages/plugin-runtime/external/unix-jsonl-transport.ts +142 -18
  161. package/packages/protocols/computer/contract.ts +27 -1
  162. package/scripts/activate-source-baseline.ts +7 -9
  163. package/scripts/canary-controller-semantic-wait.ts +12 -24
  164. package/scripts/check-mcp-compatibility.ts +48 -136
  165. package/scripts/check-runtime-architecture.mjs +622 -217
  166. package/scripts/forge-local-recovery-helper.mjs +93 -15
  167. package/scripts/lib/project-init-lib.sh +3 -0
  168. package/scripts/open-source-audit-allowlist.txt +1 -2
  169. package/scripts/run-bounded-command.mjs +5 -0
  170. package/scripts/run-governed-gate.ts +0 -1
  171. package/scripts/run-mobile-monitor-bridge.ts +0 -1
  172. package/scripts/smoke-control-plane-failure-domain.ts +94 -0
  173. package/scripts/smoke-mcp-http-runtime.ts +5 -0
  174. package/scripts/smoke-runtime-recovery.ts +12 -43
  175. package/scripts/smoke-schedule-engine.ts +23 -15
  176. package/scripts/smoke-unattended-transport-recovery.ts +223 -0
  177. package/scripts/smoke-user-request-continuation-restart.ts +232 -0
  178. package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +92 -0
  179. package/scripts/smoke-workflow-supervisor-process-restart.ts +276 -0
  180. package/scripts/smoke-workflow-supervisor-restart-recovery.ts +150 -0
  181. package/scripts/smoke-workflow-supervisor.ts +103 -0
  182. package/scripts/verify-forge-runtime.sh +2 -1
  183. package/src/cli/agent-jobs/executor-health.ts +11 -34
  184. package/src/cli/agent-jobs/integration.ts +18 -49
  185. package/src/cli/agent-jobs/job-manager.ts +7 -3
  186. package/src/cli/commands/brain-assistant.ts +27 -13
  187. package/src/cli/commands/brain.ts +13 -15
  188. package/src/cli/commands/chatgpt.ts +1 -163
  189. package/src/cli/commands/recovery.ts +135 -121
  190. package/src/cli/commands/runtime.ts +3 -3
  191. package/src/cli/commands/setup-profile.ts +8 -0
  192. package/src/cli/controller/check-runner.ts +13 -2
  193. package/src/cli/controller/completion-backlog.ts +3 -3
  194. package/src/cli/controller/completion-orchestrator.ts +4 -43
  195. package/src/cli/controller/completion-target.ts +3 -72
  196. package/src/cli/controller/context/source-materializer.ts +81 -100
  197. package/src/cli/controller/context-pack.ts +10 -8
  198. package/src/cli/controller/execution-completion.ts +4 -7
  199. package/src/cli/controller/execution-policy.ts +36 -153
  200. package/src/cli/controller/governance.ts +2 -3
  201. package/src/cli/controller/issue-store.ts +4 -99
  202. package/src/cli/controller/launch-agents.ts +59 -18
  203. package/src/cli/controller/operational-plan.ts +24 -51
  204. package/src/cli/controller/runtime-config.ts +5 -1
  205. package/src/cli/controller/stuck-state-migration.ts +2 -3
  206. package/src/cli/controller/systemd-user.ts +39 -1
  207. package/src/cli/editing/edit-session.ts +1 -7
  208. package/src/cli/local-bridge/console-automations.ts +18 -13
  209. package/src/cli/local-bridge/console-view-models.ts +3 -12
  210. package/src/cli/local-bridge/facade-api.ts +64 -191
  211. package/src/cli/local-bridge/server.ts +24 -147
  212. package/src/cli/local-bridge/types.ts +0 -1
  213. package/src/cli/mcp/oauth-client-retirement.ts +74 -0
  214. package/src/cli/repositories/command-executor.ts +29 -23
  215. package/src/cli/repositories/command-scope.ts +35 -16
  216. package/src/cli/repositories/controller-home.ts +48 -0
  217. package/src/cli/repositories/ephemeral-workspace.ts +20 -30
  218. package/src/cli/repositories/git-command-executor.ts +2 -2
  219. package/src/cli/repositories/registry.ts +77 -11
  220. package/src/cli/repositories/structured-git.ts +62 -2
  221. package/src/cli/repository/session-cache.ts +92 -6
  222. package/src/runtime/context/assistant-context.ts +20 -7
  223. package/src/runtime/context/assistant-work-context.ts +89 -27
  224. package/src/runtime/context/automatic-learning.ts +565 -0
  225. package/src/runtime/context/cognitive-attention.ts +173 -0
  226. package/src/runtime/context/cognitive-skill-canary.ts +158 -0
  227. package/src/runtime/context/context-closure.ts +46 -9
  228. package/src/runtime/context/context-pack-process.ts +103 -0
  229. package/src/runtime/context/context-plane.ts +1 -9
  230. package/src/runtime/context/direct-controller-learning.ts +298 -0
  231. package/src/runtime/context/semantic-navigation-contract.ts +9 -0
  232. package/src/runtime/context/semantic-navigation.ts +6 -34
  233. package/src/runtime/context/typescript-navigation-process.ts +145 -0
  234. package/src/runtime/context/typescript-navigation.ts +89 -3
  235. package/src/runtime/control-plane/cleanup-artifact-retention.ts +212 -9
  236. package/src/runtime/control-plane/concurrency/work-execution-concurrency.ts +9 -47
  237. package/src/runtime/control-plane/execution/controller-authority-recovery.ts +34 -4
  238. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +29 -20
  239. package/src/runtime/control-plane/execution/execution-identity.ts +23 -3
  240. package/src/runtime/control-plane/execution/repository-work-attribution.ts +7 -30
  241. package/src/runtime/control-plane/execution/retained-work-resume.ts +206 -199
  242. package/src/runtime/control-plane/execution/session-store.ts +31 -1
  243. package/src/runtime/control-plane/execution/validation.ts +6 -8
  244. package/src/runtime/control-plane/execution/work-authority-reconciler.ts +13 -42
  245. package/src/runtime/control-plane/execution/work-check-evidence.ts +137 -0
  246. package/src/runtime/control-plane/execution/work-completion-authority.ts +30 -85
  247. package/src/runtime/control-plane/execution/work-delivery-service.ts +497 -0
  248. package/src/runtime/control-plane/execution/work-evidence-policy.ts +12 -50
  249. package/src/runtime/control-plane/execution/work-execution-support.ts +50 -149
  250. package/src/runtime/control-plane/execution/work-handle-authority.ts +188 -109
  251. package/src/runtime/control-plane/execution/work-handle-store.ts +19 -43
  252. package/src/runtime/control-plane/execution/work-head-settlement.ts +53 -20
  253. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +52 -29
  254. package/src/runtime/control-plane/execution/work-operation-service.ts +43 -27
  255. package/src/runtime/control-plane/execution/work-preparation-service.ts +126 -266
  256. package/src/runtime/control-plane/execution/work-remote-delivery.ts +2 -3
  257. package/src/runtime/control-plane/execution/work-revision-diff.ts +39 -0
  258. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +173 -186
  259. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +28 -84
  260. package/src/runtime/control-plane/execution/work-verification-service.ts +406 -50
  261. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
  262. package/src/runtime/control-plane/facade/capability-registry.ts +362 -37
  263. package/src/runtime/control-plane/facade/check-normalization.ts +3 -9
  264. package/src/runtime/control-plane/facade/codex-delegation.ts +10 -11
  265. package/src/runtime/control-plane/facade/controller-connection.ts +135 -0
  266. package/src/runtime/control-plane/facade/goal-workloop-access.ts +1 -20
  267. package/src/runtime/control-plane/facade/goal-workloop.ts +281 -1753
  268. package/src/runtime/control-plane/facade/handoff-inbox-application.ts +12 -25
  269. package/src/runtime/control-plane/facade/handoff-inbox-store.ts +256 -52
  270. package/src/runtime/control-plane/facade/index.ts +0 -1
  271. package/src/runtime/control-plane/facade/operation-digest.ts +12 -15
  272. package/src/runtime/control-plane/facade/plan-contract-store.ts +451 -991
  273. package/src/runtime/control-plane/facade/policy-gate.ts +0 -23
  274. package/src/runtime/control-plane/facade/repository-work-admission.ts +12 -22
  275. package/src/runtime/control-plane/facade/requirement-authority.ts +102 -199
  276. package/src/runtime/control-plane/facade/requirement-board.ts +3 -3
  277. package/src/runtime/control-plane/facade/rh-work-operation-contract.ts +51 -14
  278. package/src/runtime/control-plane/facade/self-healing-loop.ts +18 -53
  279. package/src/runtime/control-plane/facade/suggested-actions.ts +15 -18
  280. package/src/runtime/control-plane/facade/types.ts +49 -108
  281. package/src/runtime/control-plane/facade/work-continuation.ts +8 -8
  282. package/src/runtime/control-plane/generated-cache-retention.ts +97 -1
  283. package/src/runtime/control-plane/global-scheduler/autonomous-continuation.ts +248 -0
  284. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +1 -1
  285. package/src/runtime/control-plane/global-scheduler/maintenance.ts +239 -112
  286. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +2 -1
  287. package/src/runtime/control-plane/global-scheduler/scheduler.ts +66 -40
  288. package/src/runtime/control-plane/global-scheduler/wake-signal.ts +42 -2
  289. package/src/runtime/control-plane/governance/access-policy.ts +19 -2
  290. package/src/runtime/control-plane/governance/authorization.ts +20 -3
  291. package/src/runtime/control-plane/launcher/chatgpt-round-continuation.ts +63 -300
  292. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +28 -300
  293. package/src/runtime/control-plane/launcher/local-conversation-controller.ts +238 -0
  294. package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +5 -2
  295. package/src/runtime/control-plane/launcher/thin-launcher.ts +96 -240
  296. package/src/runtime/control-plane/persistence/cognition-store.ts +820 -0
  297. package/src/runtime/control-plane/persistence/experience-store.ts +162 -29
  298. package/src/runtime/control-plane/persistence/requirement-store.ts +216 -74
  299. package/src/runtime/control-plane/persistence/sqlite-store.ts +113 -6
  300. package/src/runtime/control-plane/release-retention.ts +135 -47
  301. package/src/runtime/control-plane/repository-namespace-retention.ts +2 -2
  302. package/src/runtime/control-plane/runtime-cleanup.ts +290 -21
  303. package/src/runtime/control-plane/workspace/project-onboarding.ts +80 -0
  304. package/src/runtime/control-plane/workspace/semantic-sync-service.ts +19 -5
  305. package/src/runtime/control-plane/workspace/workspace-store.ts +169 -0
  306. package/src/runtime/diagnostics/incident-repair.ts +313 -146
  307. package/src/runtime/diagnostics/mcp-timing.ts +114 -0
  308. package/src/runtime/evidence/artifact-store.ts +158 -40
  309. package/src/runtime/evidence/evidence-store.ts +112 -8
  310. package/src/runtime/evidence/output-handle-index.ts +225 -0
  311. package/src/runtime/evidence/result-store.ts +158 -44
  312. package/src/runtime/execution/jobs/store.ts +3 -4
  313. package/src/runtime/execution/managed-workspace.ts +57 -0
  314. package/src/runtime/execution/process-runtime/check-facade.ts +1 -1
  315. package/src/runtime/execution/process-runtime/check-result-retention.ts +2 -2
  316. package/src/runtime/execution/process-runtime/check-result.ts +105 -1
  317. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +18 -8
  318. package/src/runtime/execution/process-runtime/command-facade.ts +79 -27
  319. package/src/runtime/execution/process-runtime/gc.ts +15 -2
  320. package/src/runtime/execution/process-runtime/handle-index.ts +176 -0
  321. package/src/runtime/execution/process-runtime/host-command-lane.ts +139 -0
  322. package/src/runtime/execution/process-runtime/index.ts +3 -0
  323. package/src/runtime/execution/process-runtime/lightweight-managed.ts +251 -72
  324. package/src/runtime/execution/process-runtime/persisted-check.ts +3 -3
  325. package/src/runtime/execution/process-runtime/process-runner-entry.ts +47 -5
  326. package/src/runtime/execution/process-runtime/process-scope.ts +160 -0
  327. package/src/runtime/execution/process-runtime/resource-claims.ts +65 -61
  328. package/src/runtime/execution/process-runtime/runtime.ts +35 -2
  329. package/src/runtime/execution/process-runtime/store.ts +30 -2
  330. package/src/runtime/execution/process-runtime/types.ts +6 -1
  331. package/src/runtime/execution/workers/executor.ts +2 -6
  332. package/src/runtime/platform/service-inventory.ts +130 -0
  333. package/src/runtime/platform/service-manager.ts +8 -0
  334. package/src/runtime/plugins/action-application.ts +85 -28
  335. package/src/runtime/plugins/app-store-connect-adapter.ts +38 -14
  336. package/src/runtime/plugins/browser-adapter.ts +757 -34
  337. package/src/runtime/plugins/browser-automation-service.ts +23 -3
  338. package/src/runtime/plugins/browser-macos-bridge.ts +321 -29
  339. package/src/runtime/plugins/browser-manifest-surface.ts +45 -11
  340. package/src/runtime/plugins/browser-native-session-service.ts +63 -13
  341. package/src/runtime/plugins/browser-registration.ts +16 -0
  342. package/src/runtime/plugins/browser-runtime.ts +14 -1
  343. package/src/runtime/plugins/browser-session-authority.ts +2 -2
  344. package/src/runtime/plugins/browser-session-store.ts +46 -76
  345. package/src/runtime/plugins/capability-authorization-grants.ts +258 -62
  346. package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +219 -0
  347. package/src/runtime/plugins/computer-chatgpt-macos-target.ts +891 -0
  348. package/src/runtime/plugins/computer-registration.ts +222 -11
  349. package/src/runtime/plugins/desktop-operator-external-policy.ts +36 -4
  350. package/src/runtime/plugins/desktop-operator-registration.ts +12 -4
  351. package/src/runtime/plugins/external-adapter.ts +42 -6
  352. package/src/runtime/plugins/external-registration.ts +21 -1
  353. package/src/runtime/plugins/gmail-adapter.ts +23 -2
  354. package/src/runtime/plugins/google-calendar-adapter.ts +2 -2
  355. package/src/runtime/plugins/google-shared.ts +14 -7
  356. package/src/runtime/plugins/google-tasks-adapter.ts +2 -2
  357. package/src/runtime/plugins/ios-adapter.ts +1 -1
  358. package/src/runtime/plugins/lightweight-action.ts +208 -2
  359. package/src/runtime/plugins/local-system-adapter.ts +12 -2
  360. package/src/runtime/plugins/managed-process-adapter.ts +12 -5
  361. package/src/runtime/plugins/plugin-action-sidecar.ts +17 -13
  362. package/src/runtime/plugins/plugin-management-adapter.ts +1 -1
  363. package/src/runtime/plugins/store.ts +602 -428
  364. package/src/runtime/plugins/types.ts +20 -0
  365. package/src/runtime/plugins/xiaohongshu-publish.ts +16 -0
  366. package/src/runtime/projections/invalidation.ts +7 -3
  367. package/src/runtime/projections/materialized-view.ts +80 -36
  368. package/src/runtime/recovery/actions.ts +3 -3
  369. package/src/runtime/recovery/capability-action-application.ts +46 -45
  370. package/src/runtime/recovery/lifecycle-inventory.ts +29 -29
  371. package/src/runtime/recovery/maintenance-executor.ts +374 -123
  372. package/src/runtime/recovery/store.ts +29 -11
  373. package/src/runtime/release/release-coordinator.ts +296 -0
  374. package/src/runtime/release/release-session.ts +716 -0
  375. package/src/runtime/release/release-state-migration.ts +25 -0
  376. package/src/runtime/resources/leases/store.ts +56 -25
  377. package/src/runtime/root/computer-composition.ts +191 -6
  378. package/src/runtime/root/controller-progression-composition.ts +192 -0
  379. package/src/runtime/root/controller-round-composition.ts +20 -1
  380. package/src/runtime/root/deployment-topology.ts +106 -0
  381. package/src/runtime/root/entry.ts +4 -3
  382. package/src/runtime/root/known-good-recovery.ts +184 -0
  383. package/src/runtime/root/local-bridge.ts +0 -1
  384. package/src/runtime/root/mcp-transport.ts +111 -18
  385. package/src/runtime/root/ownership.ts +261 -9
  386. package/src/runtime/root/package-connector-service.ts +105 -19
  387. package/src/runtime/root/package-runtime-release.ts +117 -18
  388. package/src/runtime/root/package-runtime-service.ts +17 -11
  389. package/src/runtime/root/release-execution-canary.ts +44 -12
  390. package/src/runtime/root/release-loader.ts +90 -8
  391. package/src/runtime/root/release-manifest.ts +80 -6
  392. package/src/runtime/root/release-materialize.ts +372 -28
  393. package/src/runtime/root/release-store.ts +395 -36
  394. package/src/runtime/root/runtime-lane.ts +191 -0
  395. package/src/runtime/root/runtime.ts +159 -27
  396. package/src/runtime/root/scheduled-controller-composition.ts +28 -3
  397. package/src/runtime/root/service-runner.ts +2 -2
  398. package/src/runtime/root/service.ts +36 -33
  399. package/src/runtime/root/status.ts +48 -0
  400. package/src/runtime/root/types.ts +32 -3
  401. package/src/runtime/root/workflow-supervisor-composition.ts +493 -0
  402. package/src/runtime/root/workflow-supervisor-runtime.ts +209 -0
  403. package/src/runtime/root/write-fence.ts +50 -2
  404. package/src/runtime/safe-tooling/google-credential-store.ts +76 -21
  405. package/src/runtime/safe-tooling/google-oauth-broker.ts +25 -7
  406. package/src/runtime/safe-tooling/workspace-auth.ts +1 -1
  407. package/src/runtime/shared/bounded-child-supervisor.ts +9 -1
  408. package/src/runtime/shared/process-environment.ts +20 -9
  409. package/src/runtime/shared/response-budget.ts +1 -3
  410. package/src/runtime/standalone-recovery/controller-home-migration.ts +16 -24
  411. package/src/runtime/standalone-recovery/core.ts +3702 -342
  412. package/src/runtime/standalone-recovery/entry.ts +469 -39
  413. package/src/runtime/standalone-recovery/installer.ts +262 -148
  414. package/src/runtime/standalone-recovery/mcp-server.ts +29 -193
  415. package/src/runtime/standalone-recovery/operation-lock.ts +1 -1
  416. package/src/runtime/standalone-recovery/performance.ts +54 -16
  417. package/src/runtime/standalone-recovery/release.ts +53 -5
  418. package/src/runtime/standalone-recovery/service-labels.ts +5 -2
  419. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +5 -2
  420. package/src/runtime/workflow/schedules/browser-probe.ts +17 -6
  421. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +10 -3
  422. package/src/runtime/workflow/schedules/engine.ts +138 -172
  423. package/src/runtime/workflow/schedules/work-continuation.ts +44 -5
  424. package/src/runtime/workflows/runtime.ts +15 -19
  425. package/src/testing/test-governance.ts +5 -1
  426. package/supervisor/chatgpt-conversation.ts +23 -0
  427. package/supervisor/client.ts +201 -0
  428. package/supervisor/control-plane.ts +868 -0
  429. package/supervisor/entry.ts +15 -0
  430. package/supervisor/forge-validators.ts +80 -0
  431. package/supervisor/index.ts +5 -0
  432. package/supervisor/native-browser-adapter.ts +786 -0
  433. package/supervisor/native-messaging/host.ts +39 -0
  434. package/supervisor/native-messaging/manifest.ts +26 -0
  435. package/supervisor/native-messaging/protocol.ts +27 -0
  436. package/supervisor/paths.ts +18 -0
  437. package/supervisor/protocol.ts +202 -0
  438. package/supervisor/server.ts +404 -0
  439. package/supervisor/store.ts +1451 -0
  440. package/supervisor/tsconfig.json +8 -0
  441. package/supervisor/types.ts +255 -0
  442. package/adapters/chatgpt/controller-host.ts +0 -119
  443. package/adapters/mcp/controller-round-compatibility.ts +0 -184
  444. package/adapters/mcp/frozen-client-semantic-compatibility.ts +0 -292
  445. package/adapters/mcp/runtime-gateway/work-compat-adapter.ts +0 -502
  446. package/packages/kernel/progression/api/index.ts +0 -2
  447. package/packages/kernel/progression/application/projector.ts +0 -211
  448. package/packages/kernel/progression/domain/types.ts +0 -116
  449. package/packages/kernel/progression/index.ts +0 -1
  450. package/packages/kernel/scheduler/application/continuation-service.ts +0 -212
  451. package/packages/kernel/scheduler/domain/continuation.ts +0 -38
  452. package/packages/kernel/scheduler/infrastructure/continuation-dispatch-store.ts +0 -38
  453. package/packages/kernel/work/domain/admission-policy.ts +0 -34
  454. package/scripts/benchmark-background-check-overlap.ts +0 -228
  455. package/scripts/benchmark-computer-hotpath.ts +0 -255
  456. package/scripts/benchmark-controller-hotpath.ts +0 -212
  457. package/scripts/benchmark-controller-round2.ts +0 -191
  458. package/scripts/benchmark-ios-agent-device.ts +0 -511
  459. package/scripts/benchmark-mcp-host.sh +0 -117
  460. package/scripts/benchmark-mcp-transport.ts +0 -169
  461. package/scripts/benchmark-operational-memory-activation.ts +0 -235
  462. package/scripts/benchmark-operational-shadow.ts +0 -28
  463. package/scripts/benchmark-quality-harness.ts +0 -146
  464. package/scripts/benchmark-route-session-concurrency.ts +0 -915
  465. package/scripts/benchmark-thin-harness-gateway-ab.ts +0 -632
  466. package/scripts/benchmark-thin-harness-mcp-e2e.ts +0 -498
  467. package/scripts/benchmark-thin-harness.ts +0 -251
  468. package/scripts/benchmark-typescript-navigation.ts +0 -186
  469. package/scripts/benchmark-work-efficiency.ts +0 -334
  470. package/scripts/build-controller-ui.ts +0 -28
  471. package/scripts/check-npm-release.sh +0 -32
  472. package/scripts/check-public-release-surface.sh +0 -19
  473. package/scripts/check-release-published.sh +0 -57
  474. package/scripts/check-release-readiness.sh +0 -37
  475. package/scripts/check-release-version.mjs +0 -74
  476. package/scripts/check-tarball-install-smoke.sh +0 -150
  477. package/scripts/package-source-archive.sh +0 -147
  478. package/scripts/public-release-files.txt +0 -39
  479. package/scripts/public-release.ts +0 -174
  480. package/scripts/publish-release-tarball.sh +0 -27
  481. package/scripts/route-nl-vs-ts-eval.ts +0 -679
  482. package/scripts/stage-runtime-release.ts +0 -44
  483. package/src/cli/controller/progress.ts +0 -656
  484. package/src/cli/controller/work-mode.ts +0 -181
  485. package/src/cli/local-bridge/console-assets.ts +0 -17
  486. package/src/cli/local-bridge/dashboard.ts +0 -17
  487. package/src/cli/local-bridge/frontend/src/App.tsx +0 -10
  488. package/src/cli/local-bridge/frontend/src/components/Button.tsx +0 -1
  489. package/src/cli/local-bridge/frontend/src/components/DetailPane.tsx +0 -3
  490. package/src/cli/local-bridge/frontend/src/components/SearchField.tsx +0 -1
  491. package/src/cli/local-bridge/frontend/src/components/SectionHeader.tsx +0 -1
  492. package/src/cli/local-bridge/frontend/src/components/Segmented.tsx +0 -1
  493. package/src/cli/local-bridge/frontend/src/components/StatusText.tsx +0 -3
  494. package/src/cli/local-bridge/frontend/src/design/icons.tsx +0 -11
  495. package/src/cli/local-bridge/frontend/src/design/tokens.css +0 -8
  496. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +0 -153
  497. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +0 -37
  498. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +0 -183
  499. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +0 -2
  500. package/src/cli/local-bridge/frontend/src/features/system/SystemView.tsx +0 -2
  501. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +0 -4
  502. package/src/cli/local-bridge/frontend/src/lib/api.ts +0 -30
  503. package/src/cli/local-bridge/frontend/src/lib/format.ts +0 -4
  504. package/src/cli/local-bridge/frontend/src/lib/routes.ts +0 -7
  505. package/src/cli/local-bridge/frontend/src/main.tsx +0 -1
  506. package/src/cli/local-bridge/frontend/src/shell/AppShell.tsx +0 -1
  507. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +0 -2
  508. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +0 -4
  509. package/src/cli/local-bridge/frontend/src/styles.css +0 -28
  510. package/src/cli/local-bridge/frontend/src/types.ts +0 -15
  511. package/src/cli/local-bridge/frontend/src/vite-env.d.ts +0 -1
  512. package/src/cli/local-bridge/frontend/vite.config.ts +0 -27
  513. package/src/cli/local-bridge/ui-dist/app.css +0 -1
  514. package/src/cli/local-bridge/ui-dist/app.js +0 -11228
  515. package/src/runtime/control-plane/execution/content-equivalent-commit-authority.ts +0 -169
  516. package/src/runtime/control-plane/execution/direct-canonical-work-reconciliation.ts +0 -85
  517. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +0 -655
  518. package/src/runtime/control-plane/execution/work-finalization-service.ts +0 -2850
  519. package/src/runtime/control-plane/execution/work-path-scope.ts +0 -37
  520. package/src/runtime/control-plane/execution/work-task-receipt.ts +0 -468
  521. package/src/runtime/control-plane/facade/controller-round-relay.ts +0 -3
  522. package/src/runtime/control-plane/facade/work-admission-policy.ts +0 -142
  523. package/src/runtime/control-plane/facade/work-implementation-review.ts +0 -2
  524. package/src/runtime/control-plane/facade/work-state-machine.ts +0 -2
  525. package/src/runtime/control-plane/facade/work-task-projection.ts +0 -66
  526. package/src/runtime/control-plane/routing/index.ts +0 -1
  527. package/src/runtime/control-plane/routing/route-policy.ts +0 -401
  528. package/src/runtime/control-plane/routing/workspace-admission.ts +0 -70
@@ -1,7 +1,7 @@
1
- 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';
4
+ import { CONTROLLER_SCOPE_REPO_ID, FORGE_INSTANCE_SCOPE_KEY, controllerSystemRoot, ensureControllerHome, repositoryControllerRoot } from '../../cli/repositories/controller-home';
5
5
  import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, rmSync } from 'fs';
6
6
  import type { ExecutionJob, ResourceClaimSpec } from '../execution/jobs/types';
7
7
  import { appendRuntimeEvent } from '../evidence/event-ledger';
@@ -20,17 +20,14 @@ import {
20
20
  } from './capability-authorization-grants';
21
21
  import { markControllerContextProjectionDirty } from '../projections/controller-context';
22
22
  import { acquireExecutionLeases, releaseExactExecutionLeases } from '../resources/leases/store';
23
- import { classifyRepositoryCommand } from '../../cli/repositories/command-classifier';
23
+ import { isRuntimeWriteFenceError } from '../root/write-fence';
24
24
  import {
25
- acceptSubmittedWorkContract,
26
- activateWorkContract,
27
25
  appendWorkEvidence,
28
- failWorkContract,
29
26
  getWorkContract,
30
27
  recordWorkCompletionReceipt,
31
28
  updateWorkContract,
32
29
  } from '../../../packages/kernel/work/api/index';
33
- import { isTerminalWorkContractStatus, type LocalEffectCompletionReceipt, type RemoteEffectCompletionReceipt, type WorkContract, type WorkRisk } from '../control-plane/facade/types';
30
+ import { isTerminalSemanticWorkState, type RemoteEffectCompletionReceipt, type WorkContract } from '../control-plane/facade/types';
34
31
  import type {
35
32
  AssistantPluginAdapter,
36
33
  AssistantPluginActionDescriptor,
@@ -75,65 +72,191 @@ function now(): string {
75
72
  return new Date().toISOString();
76
73
  }
77
74
 
78
- export function controllerPluginRepository(controllerHome: string): RepositoryRecord {
75
+ const CONTROLLER_PLUGIN_CACHE_SCOPE = 'controller';
76
+ const CONTROLLER_PLUGIN_GRANT_SCOPE = 'controller:global';
77
+ const CONTROLLER_PLUGIN_SCOPE_KEY = FORGE_INSTANCE_SCOPE_KEY;
78
+
79
+ interface PluginActionScopeContext {
80
+ kind: 'controller' | 'repository';
81
+ scopeKey: string;
82
+ grantRepoId: string;
83
+ root: string;
84
+ checkoutId?: string;
85
+ repository?: RepositoryRecord;
86
+ }
87
+
88
+ function repositoryPluginActionScope(repository: RepositoryRecord): PluginActionScopeContext {
89
+ return {
90
+ kind: 'repository',
91
+ scopeKey: repository.repoId,
92
+ grantRepoId: repository.repoId,
93
+ root: repository.canonicalRoot,
94
+ checkoutId: repository.activeCheckoutId,
95
+ repository,
96
+ };
97
+ }
98
+
99
+ function controllerPluginActionScope(controllerHome: string): PluginActionScopeContext {
100
+ return {
101
+ kind: 'controller',
102
+ scopeKey: CONTROLLER_PLUGIN_SCOPE_KEY,
103
+ grantRepoId: CONTROLLER_PLUGIN_GRANT_SCOPE,
104
+ root: controllerSystemRoot(controllerHome),
105
+ };
106
+ }
107
+
108
+ function pluginActionExecutionFields(scope: PluginActionScopeContext): { repoId: string; repoRoot: string } {
109
+ // Adapter fields are a compatibility projection. controller:global identifies
110
+ // the Grant/config domain; it is not a Repository Registry identity.
111
+ return { repoId: scope.grantRepoId, repoRoot: scope.root };
112
+ }
113
+
114
+ function controllerPluginsRoot(controllerHome: string): string {
115
+ return join(controllerSystemRoot(controllerHome), 'plugins');
116
+ }
117
+
118
+ function controllerManifestPath(controllerHome: string, pluginId: string): string {
119
+ return join(controllerPluginsRoot(controllerHome), 'manifests', `${sanitizeFileComponent(pluginId)}.json`);
120
+ }
121
+
122
+ function controllerIndexPath(controllerHome: string): string {
123
+ return join(controllerPluginsRoot(controllerHome), 'index.json');
124
+ }
125
+
126
+ function adapterMatchesController(adapter: AssistantPluginAdapter): boolean {
127
+ const scope = adapter.scope ?? 'repository';
128
+ return scope === 'controller' || scope === 'controller_with_repository_overlay';
129
+ }
130
+
131
+ function adapterMatchesRepository(adapter: AssistantPluginAdapter, _repository: RepositoryRecord): boolean {
132
+ const scope = adapter.scope ?? 'repository';
133
+ return scope === 'repository' || scope === 'controller_with_repository_overlay';
134
+ }
135
+
136
+ function computeControllerManifest(
137
+ controllerHome: string,
138
+ pluginId: string,
139
+ resolvedAdapter?: AssistantPluginAdapter,
140
+ ): AssistantPluginManifest {
141
+ const adapter = resolvedAdapter ?? resolvePluginAdapter(controllerHome, pluginId);
142
+ if (!adapter || !adapterMatchesController(adapter)) throw new Error(`PLUGIN_NOT_FOUND: ${pluginId}`);
143
+ const previous = readControllerStoredPluginManifest(controllerHome, pluginId);
79
144
  const root = controllerSystemRoot(controllerHome);
80
- const timestamp = now();
145
+ const built = adapter.buildManifest(previous?.revision ?? 0, previous?.updatedAt, root, {
146
+ controllerHome,
147
+ repoId: CONTROLLER_PLUGIN_GRANT_SCOPE,
148
+ repoRoot: root,
149
+ controllerScoped: true,
150
+ });
151
+ const changed = !previous || fingerprintManifest(previous) !== fingerprintManifest(built);
81
152
  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',
153
+ ...built,
154
+ revision: previous ? (changed ? previous.revision + 1 : previous.revision) : 1,
155
+ updatedAt: changed ? now() : previous?.updatedAt ?? built.updatedAt,
97
156
  };
98
157
  }
99
158
 
100
- /**
101
- * Controller-scoped product facade. The synthetic RepositoryRecord remains an
102
- * internal storage/projection compatibility shape only; callers need no repo.
103
- */
104
159
  export function readControllerStoredPluginManifest(controllerHome: string, pluginId: string): AssistantPluginManifest | undefined {
105
- return readStoredAssistantPluginManifest(controllerHome, controllerPluginRepository(controllerHome), pluginId);
160
+ try {
161
+ const canonicalPath = controllerManifestPath(controllerHome, pluginId);
162
+ if (existsSync(canonicalPath)) return readJsonFile<AssistantPluginManifest>(canonicalPath);
163
+ const legacyPath = join(legacyControllerPluginsRoot(controllerHome), 'manifests', `${sanitizeFileComponent(pluginId)}.json`);
164
+ return existsSync(legacyPath) ? readJsonFile<AssistantPluginManifest>(legacyPath) : undefined;
165
+ } catch {
166
+ return undefined;
167
+ }
168
+ }
169
+
170
+ function listControllerPluginIds(controllerHome: string): string[] {
171
+ return listPluginAdapters(controllerHome).filter(adapterMatchesController).map((adapter) => adapter.pluginId).sort();
106
172
  }
107
173
 
108
174
  export function listControllerPluginManifests(
109
175
  controllerHome: string,
110
176
  options: ListAssistantPluginManifestsOptions = {},
111
177
  ): AssistantPluginManifest[] {
112
- return listAssistantPluginManifests(controllerHome, controllerPluginRepository(controllerHome), options);
178
+ const preferStored = options.preferStored === true && options.forceRefresh !== true;
179
+ const fallbackToLive = options.fallbackToLive !== false || !preferStored;
180
+ const cacheKey = listCacheKey(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, preferStored, fallbackToLive);
181
+ if (options.forceRefresh !== true) {
182
+ const cached = readPluginManifestCache(pluginManifestListCache, cacheKey, preferStored ? PLUGIN_MANIFEST_STORED_CACHE_TTL_MS : PLUGIN_MANIFEST_LIVE_CACHE_TTL_MS);
183
+ if (cached) return cached;
184
+ }
185
+ const manifests = listControllerPluginIds(controllerHome).map((pluginId) => {
186
+ if (preferStored) {
187
+ const stored = readControllerStoredPluginManifest(controllerHome, pluginId);
188
+ if (stored) return stored;
189
+ if (!fallbackToLive) return undefined;
190
+ }
191
+ return computeControllerManifest(controllerHome, pluginId);
192
+ }).filter((manifest): manifest is AssistantPluginManifest => Boolean(manifest)).sort((left, right) => left.pluginId.localeCompare(right.pluginId));
193
+ primePluginManifestItemCache(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, manifests, preferStored);
194
+ return writePluginManifestCache(pluginManifestListCache, cacheKey, manifests);
195
+ }
196
+
197
+ function getControllerPluginManifestForExecution(
198
+ controllerHome: string,
199
+ pluginId: string,
200
+ adapter: AssistantPluginAdapter,
201
+ ): { manifest: AssistantPluginManifest; providerIdentityPrevalidated: boolean } {
202
+ const cacheKey = itemCacheKey(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, pluginId, false);
203
+ const cached = readPluginManifestCache(pluginManifestItemCache, cacheKey);
204
+ const wrap = (manifest: AssistantPluginManifest) => ({
205
+ manifest,
206
+ providerIdentityPrevalidated: manifest.enabled && manifest.health.probed && manifest.health.ready,
207
+ });
208
+ if (cached) return wrap(cached);
209
+ return wrap(writePluginManifestCache(pluginManifestItemCache, cacheKey, computeControllerManifest(controllerHome, pluginId, adapter)));
113
210
  }
114
211
 
115
212
  export function getControllerPluginManifest(controllerHome: string, pluginId: string): AssistantPluginManifest {
116
- return getAssistantPluginManifest(controllerHome, controllerPluginRepository(controllerHome), pluginId);
213
+ const adapter = resolvePluginAdapter(controllerHome, pluginId);
214
+ if (!adapter || !adapterMatchesController(adapter)) throw new Error(`PLUGIN_NOT_FOUND: ${pluginId}`);
215
+ return getControllerPluginManifestForExecution(controllerHome, pluginId, adapter).manifest;
216
+ }
217
+
218
+ function writeControllerRegistry(controllerHome: string, manifests: AssistantPluginManifest[]): AssistantPluginRegistryIndex {
219
+ const index: AssistantPluginRegistryIndex = {
220
+ schemaVersion: 1,
221
+ updatedAt: now(),
222
+ plugins: manifests.map((manifest) => ({
223
+ ...pluginIndexEntry(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, manifest),
224
+ manifestPath: controllerManifestPath(controllerHome, manifest.pluginId),
225
+ })).sort((left, right) => left.pluginId.localeCompare(right.pluginId)),
226
+ };
227
+ writeJsonAtomic(controllerIndexPath(controllerHome), index);
228
+ return index;
117
229
  }
118
230
 
119
231
  export function syncControllerPluginRegistry(controllerHome: string) {
120
232
  reconcileFirstPartyExternalPluginRegistrations(controllerHome);
121
- return syncAssistantPluginRegistry(controllerHome, controllerPluginRepository(controllerHome));
233
+ invalidateAssistantPluginManifestCache(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE);
234
+ const manifests = listControllerPluginManifests(controllerHome, { forceRefresh: true });
235
+ for (const manifest of manifests) writeJsonAtomic(controllerManifestPath(controllerHome, manifest.pluginId), manifest);
236
+ return { manifests, index: writeControllerRegistry(controllerHome, manifests) };
122
237
  }
123
238
 
124
239
  export function syncControllerPluginManifest(controllerHome: string, pluginId: string) {
125
240
  reconcileFirstPartyExternalPluginRegistration(controllerHome, pluginId);
126
- return syncAssistantPluginManifest(controllerHome, controllerPluginRepository(controllerHome), pluginId);
241
+ invalidateAssistantPluginManifestCache(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, pluginId);
242
+ const manifest = computeControllerManifest(controllerHome, pluginId);
243
+ writeJsonAtomic(controllerManifestPath(controllerHome, pluginId), manifest);
244
+ cacheAssistantPluginManifest(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, manifest, false);
245
+ cacheAssistantPluginManifest(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, manifest, true);
246
+ const manifests = listControllerPluginIds(controllerHome).map((candidate) => candidate === pluginId
247
+ ? manifest
248
+ : readControllerStoredPluginManifest(controllerHome, candidate) ?? cachedManifestForRepository(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, candidate))
249
+ .filter((entry): entry is AssistantPluginManifest => Boolean(entry)).sort((left, right) => left.pluginId.localeCompare(right.pluginId));
250
+ return { manifest, index: writeControllerRegistry(controllerHome, manifests) };
127
251
  }
128
252
 
129
253
  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';
254
+ invalidateAssistantPluginManifestCache(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, pluginId);
255
+ rmSync(controllerManifestPath(controllerHome, pluginId), { force: true });
256
+ const current = readJsonFile<AssistantPluginRegistryIndex>(controllerIndexPath(controllerHome), { schemaVersion: 1, updatedAt: now(), plugins: [] });
257
+ const next = { ...current, updatedAt: now(), plugins: current.plugins.filter((entry) => entry.pluginId !== pluginId) };
258
+ writeJsonAtomic(controllerIndexPath(controllerHome), next);
259
+ return next;
137
260
  }
138
261
 
139
262
  function cloneCacheValue<T>(value: T): T {
@@ -226,7 +349,13 @@ export function clearAssistantPluginManifestCacheForTest(): void {
226
349
  }
227
350
 
228
351
  function pluginsRoot(controllerHome: string, repoId: string): string {
229
- return join(repositoryControllerRoot(controllerHome, repoId), 'plugins');
352
+ return repoId === CONTROLLER_SCOPE_REPO_ID
353
+ ? join(controllerSystemRoot(controllerHome), 'plugins')
354
+ : join(repositoryControllerRoot(controllerHome, repoId), 'plugins');
355
+ }
356
+
357
+ function legacyControllerPluginsRoot(controllerHome: string): string {
358
+ return join(repositoryControllerRoot(controllerHome, CONTROLLER_SCOPE_REPO_ID), 'plugins');
230
359
  }
231
360
 
232
361
  function manifestPath(controllerHome: string, repoId: string, pluginId: string): string {
@@ -271,8 +400,14 @@ function pluginIndexEntry(controllerHome: string, repoId: string, manifest: Assi
271
400
  }
272
401
 
273
402
  function readStoredManifest(controllerHome: string, repoId: string, pluginId: string): AssistantPluginManifest | undefined {
403
+ const canonicalPath = manifestPath(controllerHome, repoId, pluginId);
274
404
  try {
275
- return readJsonFile<AssistantPluginManifest>(manifestPath(controllerHome, repoId, pluginId));
405
+ if (existsSync(canonicalPath)) return readJsonFile<AssistantPluginManifest>(canonicalPath);
406
+ if (repoId === CONTROLLER_SCOPE_REPO_ID) {
407
+ const legacyPath = join(legacyControllerPluginsRoot(controllerHome), 'manifests', `${sanitizeFileComponent(pluginId)}.json`);
408
+ if (existsSync(legacyPath)) return readJsonFile<AssistantPluginManifest>(legacyPath);
409
+ }
410
+ return undefined;
276
411
  } catch {
277
412
  return undefined;
278
413
  }
@@ -348,7 +483,7 @@ function computeManifest(
348
483
  controllerHome,
349
484
  repoId: repository.repoId,
350
485
  repoRoot: repository.canonicalRoot,
351
- controllerScoped: repository.repoId === CONTROLLER_SCOPE_REPO_ID,
486
+ controllerScoped: false,
352
487
  });
353
488
  const changed = !previous || fingerprintManifest(previous) !== fingerprintManifest(built);
354
489
  return {
@@ -370,59 +505,76 @@ function writeRegistry(controllerHome: string, repoId: string, manifests: Assist
370
505
  return index;
371
506
  }
372
507
 
373
- export function claimsForAssistantPluginAction(
508
+ function claimsForPluginActionScope(
374
509
  action: AssistantPluginActionDescriptor,
375
- repository: RepositoryRecord,
510
+ scope: PluginActionScopeContext,
376
511
  trustedPluginId: string,
377
512
  ): ResourceClaimSpec[] {
378
- const controllerScoped = repository.repoId === CONTROLLER_SCOPE_REPO_ID;
379
513
  const pluginId = trustedPluginId.trim().replace(/[^a-zA-Z0-9._-]+/g, '-');
380
514
  if (!pluginId) throw new Error('PLUGIN_ID_REQUIRED_FOR_RESOURCE_CLAIMS');
381
515
  return action.resourceClaims.map((claim) => ({
382
516
  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
517
  ? `provider-state:${pluginId}`
386
- : controllerScoped
518
+ : scope.kind === 'controller'
387
519
  ? `controller-system:${claim.resource}`
388
520
  : claim.resource === 'workspace'
389
- ? `workspace:${repository.activeCheckoutId}`
521
+ ? `workspace:${scope.checkoutId}`
390
522
  : claim.resource === 'git-refs'
391
- ? `git-refs:${repository.repoId}`
392
- : `repo-state:${repository.repoId}`,
523
+ ? `git-refs:${scope.scopeKey}`
524
+ : `repo-state:${scope.scopeKey}`,
393
525
  mode: claim.mode,
394
526
  }));
395
527
  }
396
528
 
529
+ export function claimsForAssistantPluginAction(
530
+ action: AssistantPluginActionDescriptor,
531
+ repository: RepositoryRecord,
532
+ trustedPluginId: string,
533
+ ): ResourceClaimSpec[] {
534
+ return claimsForPluginActionScope(action, repositoryPluginActionScope(repository), trustedPluginId);
535
+ }
536
+
397
537
  async function withAssistantPluginResourceLeases<T>(
398
538
  controllerHome: string,
399
- repository: RepositoryRecord,
539
+ scope: PluginActionScopeContext,
400
540
  action: AssistantPluginActionDescriptor,
401
541
  request: AssistantPluginActionRequest,
402
542
  operation: () => Promise<T>,
403
543
  ): Promise<T> {
404
- const claims = claimsForAssistantPluginAction(action, repository, request.pluginId).map((claim) => ({
544
+ const claims = claimsForPluginActionScope(action, scope, request.pluginId).map((claim) => ({
405
545
  ...claim,
406
- repoId: repository.repoId,
407
- checkoutId: repository.activeCheckoutId,
546
+ repoId: scope.scopeKey,
547
+ ...(scope.checkoutId ? { checkoutId: scope.checkoutId } : {}),
408
548
  }));
409
549
  if (claims.length === 0) return operation();
410
550
 
411
- const ownerJobId = `plugin:${request.requestId}`;
551
+ // requestId is the semantic idempotency key, but before its receipt/index is
552
+ // persisted two concurrent replays can enter this function. Give each live
553
+ // invocation a distinct lease owner so the later replay contends on the
554
+ // first invocation's declared resource instead of silently accumulating a
555
+ // second same-owner lease and then failing exact release with LEASE_SET_MISMATCH.
556
+ const ownerJobId = `plugin:${request.requestId}:${randomUUID()}`;
412
557
  const timeoutMs = Math.max(5_000, Math.min(10 * 60_000, request.timeoutMs ?? action.defaultTimeoutMs));
413
- const acquisition = acquireExecutionLeases(controllerHome, repository.repoId, ownerJobId, claims, {
558
+ const acquisition = acquireExecutionLeases(controllerHome, scope.scopeKey, ownerJobId, claims, {
414
559
  ttlMs: timeoutMs + 60_000,
560
+ // This lease exists only to fence concurrent invocations of one external
561
+ // effect. It is not Scheduler/recovery authority, so derived projection,
562
+ // event-ledger, and wake side effects must never participate in the effect
563
+ // outcome. The lease file + fencing token remain cross-process durable for
564
+ // the bounded invocation and expire if its process dies.
565
+ visibility: 'ephemeral',
415
566
  ownerIdentity: {
416
- repositoryId: repository.repoId,
417
- checkoutId: repository.activeCheckoutId,
418
- worktreeId: repository.activeCheckoutId,
567
+ repositoryId: scope.scopeKey,
568
+ checkoutId: scope.checkoutId ?? CONTROLLER_PLUGIN_SCOPE_KEY,
569
+ worktreeId: scope.checkoutId ?? CONTROLLER_PLUGIN_SCOPE_KEY,
419
570
  branch: 'workflow-plugin-action',
420
571
  principalId: request.origin.actor?.trim() || 'workflow-plugin-action',
421
572
  controllerInstanceId: process.env.FORGE_RUNTIME_INSTANCE_ID?.trim()
422
573
  || process.env.FORGE_WRITER_INSTANCE_ID?.trim()
423
574
  || process.env.FORGE_DAEMON_INSTANCE_ID?.trim()
424
575
  || `process:${process.pid}`,
425
- controllerGeneration: process.env.FORGE_WRITER_GENERATION?.trim()
576
+ controllerGeneration: process.env.FORGE_RUNTIME_INCARNATION_GENERATION?.trim()
577
+ || process.env.FORGE_WRITER_GENERATION?.trim()
426
578
  || process.env.FORGE_ACTIVE_RUNTIME_REVISION?.trim()
427
579
  || 'unbound',
428
580
  },
@@ -455,13 +607,24 @@ async function withAssistantPluginResourceLeases<T>(
455
607
  retainForReconciliation = isAssistantPluginError(error) && error.effectOutcome === 'outcome_unknown';
456
608
  throw error;
457
609
  } finally {
458
- if (!retainForReconciliation) releaseExactExecutionLeases(controllerHome, repository.repoId, ownerJobId, expected);
610
+ if (!retainForReconciliation) {
611
+ try {
612
+ releaseExactExecutionLeases(controllerHome, scope.scopeKey, ownerJobId, expected, { visibility: 'ephemeral' });
613
+ } catch (error) {
614
+ // A successful provider effect can rotate Runtime authority (for example
615
+ // a fenced Runtime cutover). The old sidecar must not release leases
616
+ // after it is fenced, but that cleanup denial must not overwrite the
617
+ // already-known provider result. The canonical successor Runtime can
618
+ // reclaim this ephemeral lease from its fenced owner immediately.
619
+ if (!isRuntimeWriteFenceError(error)) throw error;
620
+ }
621
+ }
459
622
  }
460
623
  }
461
624
 
462
- function semanticKey(repository: RepositoryRecord, pluginId: string, actionId: string, args: Record<string, unknown>): string {
625
+ function semanticKey(scope: PluginActionScopeContext, pluginId: string, actionId: string, args: Record<string, unknown>): string {
463
626
  const digest = createHash('sha256').update(JSON.stringify(canonical(args))).digest('hex').slice(0, 20);
464
- return `plugin-action:${repository.repoId}:${pluginId}:${actionId}:${digest}`;
627
+ return `plugin-action:${scope.scopeKey}:${pluginId}:${actionId}:${digest}`;
465
628
  }
466
629
 
467
630
  function validatePrimitive(type: string, value: unknown): boolean {
@@ -582,7 +745,7 @@ function authorizationTargetMatches(
582
745
 
583
746
  async function resolveAutomatedWriteAuthorization(input: {
584
747
  controllerHome: string;
585
- repository: RepositoryRecord;
748
+ scope: PluginActionScopeContext;
586
749
  adapter: AssistantPluginAdapter;
587
750
  manifest: AssistantPluginManifest;
588
751
  action: AssistantPluginActionDescriptor;
@@ -610,14 +773,14 @@ async function resolveAutomatedWriteAuthorization(input: {
610
773
  }
611
774
  const targetContext = await input.adapter.resolveAuthorizationContext({
612
775
  controllerHome: input.controllerHome,
613
- repoId: input.repository.repoId,
614
- repoRoot: input.repository.canonicalRoot,
776
+ ...pluginActionExecutionFields(input.scope),
615
777
  pluginId: input.manifest.pluginId,
616
778
  actionId: input.action.actionId,
617
779
  requestId: input.requestId,
618
780
  args: input.args,
619
781
  origin: input.origin,
620
782
  jobId: input.jobId,
783
+ authorizationGrantRefs: refs,
621
784
  });
622
785
  if (!targetContext) {
623
786
  throw new AssistantPluginError(
@@ -630,7 +793,7 @@ async function resolveAutomatedWriteAuthorization(input: {
630
793
  const grant = refs
631
794
  .map((ref) => findActivePluginCapabilityAuthorizationById(input.controllerHome, ref))
632
795
  .find((candidate) => candidate
633
- && candidate.repoId === input.repository.repoId
796
+ && candidate.repoId === input.scope.grantRepoId
634
797
  && candidate.pluginId === input.manifest.pluginId
635
798
  && candidate.capabilityId === capabilityId
636
799
  && authorizationTargetMatches(candidate.target, targetContext.target)
@@ -819,25 +982,39 @@ export function removeAssistantPluginManifestProjection(
819
982
  }
820
983
 
821
984
  export function isDirectPluginReadAction(action: AssistantPluginActionDescriptor): boolean {
822
- return action.readOnly === true
985
+ return action.executionMode !== 'direct_non_persistent'
986
+ && action.readOnly === true
823
987
  && action.risk === 'readonly'
824
988
  && action.confirmation === 'none'
825
989
  && action.idempotent === true;
826
990
  }
827
991
 
828
992
  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;
993
+ if (
994
+ action.executionMode !== 'direct_non_persistent'
995
+ || action.remoteEffectWorkCompletion !== undefined
996
+ || action.resourceClaims.length !== 0
997
+ ) {
998
+ return false;
999
+ }
1000
+
1001
+ // Direct non-persistent actions are provider-local operations that must never
1002
+ // enter the durable effect/receipt path. They are intentionally limited to
1003
+ // either a pure provider read or a provider-local workspace effect. Remote
1004
+ // writes/destructive effects still require the normal durable application
1005
+ // path and its reconciliation authority.
1006
+ if (action.readOnly) {
1007
+ return action.risk === 'readonly'
1008
+ && (action.confirmation === 'none' || action.confirmation === 'authorization')
1009
+ && action.idempotent === true;
1010
+ }
1011
+ return action.risk === 'workspace_write'
1012
+ && (action.confirmation === 'authorization' || action.confirmation === 'none');
836
1013
  }
837
1014
 
838
- export async function executeAssistantPluginDirectNonPersistent(
1015
+ async function executePluginDirectNonPersistentInScope(
839
1016
  controllerHome: string,
840
- repository: RepositoryRecord,
1017
+ scope: PluginActionScopeContext,
841
1018
  request: AssistantPluginActionRequest,
842
1019
  ): Promise<{ manifest: AssistantPluginManifest; action: AssistantPluginActionDescriptor; result: Record<string, unknown> }> {
843
1020
  const adapter = resolvePluginAdapter(controllerHome, request.pluginId);
@@ -845,7 +1022,9 @@ export async function executeAssistantPluginDirectNonPersistent(
845
1022
  if (PLUGIN_ADAPTERS.get(request.pluginId) !== adapter) {
846
1023
  throw new Error(`PLUGIN_DIRECT_NON_PERSISTENT_FIRST_PARTY_REQUIRED: ${request.pluginId}/${request.actionId}`);
847
1024
  }
848
- const manifestLookup = getAssistantPluginManifestForExecution(controllerHome, repository, request.pluginId, adapter);
1025
+ const manifestLookup = scope.kind === 'controller'
1026
+ ? getControllerPluginManifestForExecution(controllerHome, request.pluginId, adapter)
1027
+ : getAssistantPluginManifestForExecution(controllerHome, scope.repository!, request.pluginId, adapter);
849
1028
  const manifest = manifestLookup.manifest;
850
1029
  const action = actionForManifest(manifest, request.actionId);
851
1030
  if (!manifest.enabled && action.actionId !== 'configure') {
@@ -860,7 +1039,7 @@ export async function executeAssistantPluginDirectNonPersistent(
860
1039
  if (request.workId || request.workRepoId) {
861
1040
  throw new Error(`PLUGIN_DIRECT_NON_PERSISTENT_WORK_ATTRIBUTION_FORBIDDEN: ${request.pluginId}/${request.actionId}`);
862
1041
  }
863
- if (request.confirmAuthorization !== true) {
1042
+ if (action.confirmation === 'authorization' && request.confirmAuthorization !== true) {
864
1043
  throw new Error(`PLUGIN_DIRECT_NON_PERSISTENT_AUTHORIZATION_REQUIRED: ${request.pluginId}/${request.actionId} requires confirm_authorization=true for this invocation`);
865
1044
  }
866
1045
  const normalizedArgs = validateActionArguments(action, request.args ?? {});
@@ -868,7 +1047,7 @@ export async function executeAssistantPluginDirectNonPersistent(
868
1047
  if (requiresAutomatedWriteAuthorization(request.origin)) {
869
1048
  await resolveAutomatedWriteAuthorization({
870
1049
  controllerHome,
871
- repository,
1050
+ scope,
872
1051
  adapter,
873
1052
  manifest,
874
1053
  action,
@@ -880,8 +1059,7 @@ export async function executeAssistantPluginDirectNonPersistent(
880
1059
  }
881
1060
  const result = await adapter.executeAction({
882
1061
  controllerHome,
883
- repoId: repository.repoId,
884
- repoRoot: repository.canonicalRoot,
1062
+ ...pluginActionExecutionFields(scope),
885
1063
  pluginId: request.pluginId,
886
1064
  actionId: request.actionId,
887
1065
  requestId: request.requestId,
@@ -895,12 +1073,29 @@ export async function executeAssistantPluginDirectNonPersistent(
895
1073
  return { manifest, action, result };
896
1074
  }
897
1075
 
898
- export async function executeAssistantPluginReadDirect(
1076
+ export function executeAssistantPluginDirectNonPersistent(
899
1077
  controllerHome: string,
900
1078
  repository: RepositoryRecord,
901
1079
  request: AssistantPluginActionRequest,
902
- ): Promise<{ manifest: AssistantPluginManifest; action: AssistantPluginActionDescriptor; result: Record<string, unknown> }> {
903
- const manifest = getAssistantPluginManifest(controllerHome, repository, request.pluginId);
1080
+ ) {
1081
+ return executePluginDirectNonPersistentInScope(controllerHome, repositoryPluginActionScope(repository), request);
1082
+ }
1083
+
1084
+ export function executeControllerPluginDirectNonPersistent(
1085
+ controllerHome: string,
1086
+ request: AssistantPluginActionRequest,
1087
+ ) {
1088
+ return executePluginDirectNonPersistentInScope(controllerHome, controllerPluginActionScope(controllerHome), request);
1089
+ }
1090
+
1091
+ async function executePluginReadDirectInScope(
1092
+ controllerHome: string,
1093
+ scope: PluginActionScopeContext,
1094
+ request: AssistantPluginActionRequest,
1095
+ ): Promise<{ manifest: AssistantPluginManifest; action: AssistantPluginActionDescriptor; result: Record<string, unknown>; receipt: PluginActionReceipt }> {
1096
+ const manifest = scope.kind === 'controller'
1097
+ ? getControllerPluginManifest(controllerHome, request.pluginId)
1098
+ : getAssistantPluginManifest(controllerHome, scope.repository!, request.pluginId);
904
1099
  const action = actionForManifest(manifest, request.actionId);
905
1100
  if (!manifest.enabled && action.actionId !== 'configure') {
906
1101
  throw new Error(`PLUGIN_DISABLED: ${request.pluginId} is disabled`);
@@ -910,24 +1105,68 @@ export async function executeAssistantPluginReadDirect(
910
1105
  }
911
1106
  const normalizedArgs = validateActionArguments(action, request.args ?? {});
912
1107
  enforceConfirmation(action, { ...request, args: normalizedArgs });
913
- const result = await executeAssistantPluginAction({
1108
+ const result = await executePluginActionInScope(scope, {
914
1109
  controllerHome,
915
- repoId: repository.repoId,
916
- repoRoot: repository.canonicalRoot,
1110
+ ...pluginActionExecutionFields(scope),
917
1111
  pluginId: request.pluginId,
918
1112
  actionId: request.actionId,
919
1113
  requestId: request.requestId,
920
1114
  args: normalizedArgs,
921
1115
  origin: request.origin,
922
1116
  });
923
- return { manifest, action, result };
1117
+ const createdAt = new Date().toISOString();
1118
+ const resultDigest = createHash('sha256').update(JSON.stringify(result)).digest('hex');
1119
+ const observationSemanticKey = semanticKey(scope, request.pluginId, request.actionId, normalizedArgs);
1120
+ const observationActor = request.origin?.actor?.trim() ?? '';
1121
+ // Observation evidence is content-addressed so repeated identical reads do not create
1122
+ // an unbounded receipt trail. This remains provenance only, never replay/effect state.
1123
+ const receiptId = `PLG-OBS-${createHash('sha256')
1124
+ .update(`${observationSemanticKey}:${resultDigest}:${observationActor}`)
1125
+ .digest('hex').slice(0, 24)}`;
1126
+ const receipt: PluginActionReceipt = {
1127
+ schemaVersion: 1,
1128
+ receiptId,
1129
+ requestId: request.requestId,
1130
+ scopeKey: scope.scopeKey,
1131
+ ...(scope.repository ? { repoId: scope.repository.repoId } : {}),
1132
+ pluginId: request.pluginId,
1133
+ actionId: request.actionId,
1134
+ semanticKey: observationSemanticKey,
1135
+ status: 'succeeded',
1136
+ createdAt,
1137
+ ...(request.origin ? { origin: request.origin } : {}),
1138
+ observationOnly: true,
1139
+ resultDigest,
1140
+ };
1141
+ // Direct reads stay on the inline fast path. Persist only a compact observation
1142
+ // receipt, never provider result content and never request replay state.
1143
+ writeJsonAtomic(pluginActionReceiptPath(controllerHome, scope.scopeKey, receiptId), receipt);
1144
+ return { manifest, action, result, receipt };
1145
+ }
1146
+
1147
+ export function executeAssistantPluginReadDirect(
1148
+ controllerHome: string,
1149
+ repository: RepositoryRecord,
1150
+ request: AssistantPluginActionRequest,
1151
+ ) {
1152
+ return executePluginReadDirectInScope(controllerHome, repositoryPluginActionScope(repository), request);
1153
+ }
1154
+
1155
+ export function executeControllerPluginReadDirect(
1156
+ controllerHome: string,
1157
+ request: AssistantPluginActionRequest,
1158
+ ) {
1159
+ return executePluginReadDirectInScope(controllerHome, controllerPluginActionScope(controllerHome), request);
924
1160
  }
925
1161
 
926
1162
  export interface PluginActionReceipt {
927
1163
  schemaVersion: 1;
928
1164
  receiptId: string;
929
1165
  requestId: string;
930
- repoId: string;
1166
+ /** Canonical plugin-action receipt scope: `instance` for controller plugins, repository id otherwise. */
1167
+ scopeKey: string;
1168
+ /** Repository provenance only; absent for controller-scoped plugin actions. */
1169
+ repoId?: string;
931
1170
  /** Work authority repository when it differs from controller/provider receipt scope. */
932
1171
  workRepoId?: string;
933
1172
  pluginId: string;
@@ -942,23 +1181,34 @@ export interface PluginActionReceipt {
942
1181
  authorization?: PluginActionAuthorizationEvidence;
943
1182
  result?: Record<string, unknown>;
944
1183
  error?: { code: string; message: string };
1184
+ /** True for a compact direct-read observation; it is evidence, never an effect-completion authority. */
1185
+ observationOnly?: boolean;
1186
+ /** SHA-256 of the direct-read result. Result content is intentionally not persisted for observation-only receipts. */
1187
+ resultDigest?: string;
945
1188
  }
946
1189
 
947
1190
  interface PluginActionRequestIndex {
948
1191
  requestId: string;
949
- repoId: string;
1192
+ scopeKey: string;
1193
+ repoId?: string;
950
1194
  workRepoId?: string;
951
1195
  receiptId: string;
952
1196
  semanticKey: string;
953
1197
  createdAt: string;
954
1198
  }
955
1199
 
956
- function pluginActionReceiptRoot(controllerHome: string, repoId: string): string {
957
- const root = join(repositoryControllerRoot(controllerHome, repoId), 'plugin-action-receipts');
1200
+ function pluginActionReceiptRoot(controllerHome: string, scopeKey: string): string {
1201
+ const root = scopeKey === CONTROLLER_PLUGIN_SCOPE_KEY
1202
+ ? join(controllerSystemRoot(controllerHome), 'plugin-action-receipts')
1203
+ : join(repositoryControllerRoot(controllerHome, scopeKey), 'plugin-action-receipts');
958
1204
  mkdirSync(root, { recursive: true });
959
1205
  return root;
960
1206
  }
961
1207
 
1208
+ function legacyControllerPluginActionReceiptPath(controllerHome: string, receiptId: string): string {
1209
+ return join(repositoryControllerRoot(controllerHome, CONTROLLER_SCOPE_REPO_ID), 'plugin-action-receipts', `${sanitizeFileComponent(receiptId)}.json`);
1210
+ }
1211
+
962
1212
  function pluginActionRequestPath(controllerHome: string, requestId: string): string {
963
1213
  const hash = createHash('sha256').update(requestId).digest('hex');
964
1214
  const root = join(ensureControllerHome(controllerHome), 'indexes', 'plugin-actions', 'requests');
@@ -966,14 +1216,30 @@ function pluginActionRequestPath(controllerHome: string, requestId: string): str
966
1216
  return join(root, `${hash}.json`);
967
1217
  }
968
1218
 
969
- function pluginActionReceiptPath(controllerHome: string, repoId: string, receiptId: string): string {
970
- return join(pluginActionReceiptRoot(controllerHome, repoId), `${sanitizeFileComponent(receiptId)}.json`);
1219
+ function pluginActionReceiptPath(controllerHome: string, scopeKey: string, receiptId: string): string {
1220
+ return join(pluginActionReceiptRoot(controllerHome, scopeKey), `${sanitizeFileComponent(receiptId)}.json`);
971
1221
  }
972
1222
 
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);
1223
+ function normalizePluginActionReceipt(receipt: PluginActionReceipt): PluginActionReceipt {
1224
+ const scopeKey = receipt.scopeKey?.trim()
1225
+ || (receipt.repoId === CONTROLLER_SCOPE_REPO_ID ? CONTROLLER_PLUGIN_SCOPE_KEY : receipt.repoId?.trim())
1226
+ || '';
1227
+ if (!scopeKey) throw new Error('PLUGIN_RECEIPT_SCOPE_CORRUPT');
1228
+ return {
1229
+ ...receipt,
1230
+ scopeKey,
1231
+ ...(scopeKey === CONTROLLER_PLUGIN_SCOPE_KEY ? { repoId: undefined } : { repoId: receipt.repoId ?? scopeKey }),
1232
+ };
1233
+ }
1234
+
1235
+ function readPluginActionReceipt(controllerHome: string, scopeKey: string, receiptId: string): PluginActionReceipt | undefined {
1236
+ const path = pluginActionReceiptPath(controllerHome, scopeKey, receiptId);
1237
+ if (existsSync(path)) return normalizePluginActionReceipt(readJsonFile<PluginActionReceipt>(path));
1238
+ if (scopeKey === CONTROLLER_PLUGIN_SCOPE_KEY || scopeKey === CONTROLLER_SCOPE_REPO_ID) {
1239
+ const legacyPath = legacyControllerPluginActionReceiptPath(controllerHome, receiptId);
1240
+ if (existsSync(legacyPath)) return normalizePluginActionReceipt(readJsonFile<PluginActionReceipt>(legacyPath));
1241
+ }
1242
+ return undefined;
977
1243
  }
978
1244
 
979
1245
  export function findPluginActionReceipt(
@@ -981,6 +1247,12 @@ export function findPluginActionReceipt(
981
1247
  receiptId: string,
982
1248
  ): PluginActionReceipt | undefined {
983
1249
  const home = ensureControllerHome(controllerHome);
1250
+ // Controller-scoped plugins (for example Local System) live under system/, not
1251
+ // repositories/. They are first-class evidence producers and must be searched too.
1252
+ const controllerReceipt = readPluginActionReceipt(home, CONTROLLER_PLUGIN_SCOPE_KEY, receiptId)
1253
+ // One-way read compatibility for pre-cutover controller receipts only.
1254
+ ?? readPluginActionReceipt(home, CONTROLLER_SCOPE_REPO_ID, receiptId);
1255
+ if (controllerReceipt) return controllerReceipt;
984
1256
  const repositoriesRoot = join(home, 'repositories');
985
1257
  try {
986
1258
  for (const repoId of readdirSync(repositoriesRoot)) {
@@ -999,7 +1271,9 @@ export function readPluginActionReceiptForRequest(controllerHome: string, reques
999
1271
  if (!existsSync(path)) return undefined;
1000
1272
  const index = readJsonFile<PluginActionRequestIndex>(path);
1001
1273
  if (index.requestId !== requestId) throw new Error('PLUGIN_RECEIPT_REQUEST_MISMATCH');
1002
- const receipt = readPluginActionReceipt(controllerHome, index.repoId, index.receiptId);
1274
+ const scopeKey = index.scopeKey?.trim() || index.repoId?.trim();
1275
+ if (!scopeKey) throw new Error('PLUGIN_RECEIPT_REQUEST_SCOPE_CORRUPT');
1276
+ const receipt = readPluginActionReceipt(controllerHome, scopeKey, index.receiptId);
1003
1277
  if (!receipt || receipt.requestId !== requestId || receipt.semanticKey !== index.semanticKey) throw new Error('PLUGIN_RECEIPT_LOST');
1004
1278
  return receipt;
1005
1279
  }
@@ -1011,86 +1285,20 @@ export function compatibilityPluginJobFromReceipt(
1011
1285
  return compatibilityJobFromReceipt(receipt, checkoutId);
1012
1286
  }
1013
1287
 
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 {
1288
+ function workAttributionRepoId(scope: PluginActionScopeContext, request: AssistantPluginActionRequest): string {
1083
1289
  const explicit = request.workRepoId?.trim();
1084
1290
  if (!request.workId?.trim()) {
1085
1291
  if (explicit) throw new Error(`WORK_PLUGIN_ATTRIBUTION_REPO_WITHOUT_WORK: ${explicit}`);
1086
- return repository.repoId;
1292
+ return scope.repository?.repoId ?? '';
1087
1293
  }
1088
- return explicit || repository.repoId;
1294
+ if (explicit) return explicit;
1295
+ if (scope.repository) return scope.repository.repoId;
1296
+ throw new Error(`WORK_PLUGIN_ATTRIBUTION_REPO_REQUIRED: controller-scoped action ${request.pluginId}/${request.actionId} must name the Work repository explicitly`);
1089
1297
  }
1090
1298
 
1091
1299
  function remoteEffectWorkForPluginAction(
1092
1300
  controllerHome: string,
1093
- repository: RepositoryRecord,
1301
+ scope: PluginActionScopeContext,
1094
1302
  action: AssistantPluginActionDescriptor,
1095
1303
  request: AssistantPluginActionRequest,
1096
1304
  receiptId?: string,
@@ -1100,7 +1308,7 @@ function remoteEffectWorkForPluginAction(
1100
1308
  if (action.risk !== 'remote_write') {
1101
1309
  throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_REQUIRES_REMOTE_WRITE: ${action.actionId}`);
1102
1310
  }
1103
- const workRepoId = workAttributionRepoId(repository, request);
1311
+ const workRepoId = workAttributionRepoId(scope, request);
1104
1312
  const work = getWorkContract({ controllerHome, repoId: workRepoId }, workId);
1105
1313
  if (!work) throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_NOT_FOUND: ${workId}`);
1106
1314
  if (work.workKind !== 'remote_effect') {
@@ -1109,49 +1317,49 @@ function remoteEffectWorkForPluginAction(
1109
1317
  if (work.checks.length > 0) {
1110
1318
  throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_CHECKS_PRESENT: ${workId} declares repository checks and cannot be completed by a plugin receipt alone`);
1111
1319
  }
1112
- if (isTerminalWorkContractStatus(work.status)) {
1320
+ if (isTerminalSemanticWorkState(work.semanticState)) {
1113
1321
  const receiptAlreadyBound = Boolean(receiptId) && (
1114
1322
  work.completionReceipt?.receiptId === receiptId
1115
1323
  || work.evidenceRefs.some((evidence) => evidence.evidenceId === receiptId)
1116
1324
  );
1117
- if (work.status === 'completed' && receiptAlreadyBound) return work;
1118
- throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_TERMINAL: ${workId} is ${work.status}`);
1325
+ if (work.semanticState === 'completed' && receiptAlreadyBound) return work;
1326
+ throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_TERMINAL: ${workId} is ${work.semanticState}`);
1119
1327
  }
1120
1328
  return work;
1121
1329
  }
1122
1330
 
1123
1331
  function attributedWorkForPluginAction(
1124
1332
  controllerHome: string,
1125
- repository: RepositoryRecord,
1333
+ scope: PluginActionScopeContext,
1126
1334
  action: AssistantPluginActionDescriptor,
1127
1335
  request: AssistantPluginActionRequest,
1128
1336
  ): WorkContract | undefined {
1129
1337
  const workId = request.workId?.trim();
1130
1338
  if (!workId || action.risk === 'remote_write') return undefined;
1131
- const workRepoId = workAttributionRepoId(repository, request);
1339
+ const workRepoId = workAttributionRepoId(scope, request);
1132
1340
  const work = getWorkContract({ controllerHome, repoId: workRepoId }, workId);
1133
1341
  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}`);
1342
+ if (isTerminalSemanticWorkState(work.semanticState)) {
1343
+ throw new Error(`WORK_PLUGIN_ATTRIBUTION_TERMINAL: ${workId} is ${work.semanticState}`);
1136
1344
  }
1137
1345
  return work;
1138
1346
  }
1139
1347
 
1140
1348
  function bindLocalEffectReceiptToAttributedWork(
1141
1349
  controllerHome: string,
1142
- repository: RepositoryRecord,
1350
+ scope: PluginActionScopeContext,
1143
1351
  action: AssistantPluginActionDescriptor,
1144
1352
  request: AssistantPluginActionRequest,
1145
1353
  receipt: PluginActionReceipt,
1146
1354
  work?: WorkContract,
1147
1355
  ): WorkContract | undefined {
1148
1356
  if (!request.workId || receipt.status !== 'succeeded') return work;
1149
- const workRepoId = workAttributionRepoId(repository, request);
1357
+ const workRepoId = workAttributionRepoId(scope, request);
1150
1358
  const attributed = work ?? getWorkContract({ controllerHome, repoId: workRepoId }, request.workId.trim());
1151
1359
  if (!attributed) throw new Error(`WORK_PLUGIN_ATTRIBUTION_NOT_FOUND: ${workRepoId}:${request.workId.trim()}`);
1152
1360
  if (attributed.workKind !== 'local_effect' || action.readOnly || action.risk === 'readonly') return attributed;
1153
1361
  if (attributed.evidenceRefs.some((evidence) => evidence.evidenceId === receipt.receiptId)) return attributed;
1154
- if (isTerminalWorkContractStatus(attributed.status)) return attributed;
1362
+ if (isTerminalSemanticWorkState(attributed.semanticState)) return attributed;
1155
1363
  return appendWorkEvidence({ controllerHome, repoId: workRepoId }, attributed.workId, {
1156
1364
  evidenceId: receipt.receiptId,
1157
1365
  title: 'typed local plugin effect completed',
@@ -1160,7 +1368,7 @@ function bindLocalEffectReceiptToAttributedWork(
1160
1368
  });
1161
1369
  }
1162
1370
 
1163
- export function finalizeRemoteEffectWorkFromActionReceipt(
1371
+ export function recordRemoteEffectWorkActionReceipt(
1164
1372
  controllerHome: string,
1165
1373
  repoId: string,
1166
1374
  workId: string,
@@ -1170,14 +1378,12 @@ export function finalizeRemoteEffectWorkFromActionReceipt(
1170
1378
  if (work.workKind !== 'remote_effect') {
1171
1379
  throw new Error(`WORK_REMOTE_EFFECT_FINALIZE_KIND_MISMATCH: ${workId} is ${work.workKind}, expected remote_effect`);
1172
1380
  }
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
- }
1381
+ if (work.completionReceipt?.source === 'remote_effect') return work;
1177
1382
  const receipt = work.evidenceRefs
1178
1383
  .map((evidence) => {
1179
1384
  if (!evidence.evidenceId) return undefined;
1180
1385
  return readPluginActionReceipt(controllerHome, repoId, evidence.evidenceId)
1386
+ ?? readPluginActionReceipt(controllerHome, CONTROLLER_PLUGIN_SCOPE_KEY, evidence.evidenceId)
1181
1387
  ?? readPluginActionReceipt(controllerHome, CONTROLLER_SCOPE_REPO_ID, evidence.evidenceId);
1182
1388
  })
1183
1389
  .find((candidate) => candidate?.workId === workId && candidate.status === 'succeeded');
@@ -1207,17 +1413,17 @@ export function finalizeRemoteEffectWorkFromActionReceipt(
1207
1413
 
1208
1414
  function bindRemoteEffectReceiptToWork(
1209
1415
  controllerHome: string,
1210
- repository: RepositoryRecord,
1416
+ scope: PluginActionScopeContext,
1211
1417
  action: AssistantPluginActionDescriptor,
1212
1418
  request: AssistantPluginActionRequest,
1213
1419
  receipt: PluginActionReceipt,
1214
1420
  ): WorkContract | undefined {
1215
1421
  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);
1422
+ if (receipt.status !== 'succeeded') return remoteEffectWorkForPluginAction(controllerHome, scope, action, request);
1423
+ const work = remoteEffectWorkForPluginAction(controllerHome, scope, action, request, receipt.receiptId);
1218
1424
  if (!work) return undefined;
1219
- if (work.status === 'completed') return work;
1220
- const workRepoId = workAttributionRepoId(repository, request);
1425
+ if (work.completionReceipt?.source === 'remote_effect') return work;
1426
+ const workRepoId = workAttributionRepoId(scope, request);
1221
1427
  let updated = work;
1222
1428
  if (!work.evidenceRefs.some((evidence) => evidence.evidenceId === receipt.receiptId)) {
1223
1429
  updated = appendWorkEvidence({ controllerHome, repoId: workRepoId }, work.workId, {
@@ -1254,7 +1460,7 @@ function compatibilityJobFromReceipt(receipt: PluginActionReceipt, checkoutId: s
1254
1460
  schemaVersion: 1,
1255
1461
  revision: 1,
1256
1462
  jobId: receipt.receiptId,
1257
- repoId: receipt.repoId,
1463
+ repoId: receipt.repoId ?? receipt.scopeKey,
1258
1464
  checkoutId,
1259
1465
  type: 'plugin-action',
1260
1466
  status: receipt.effectOutcome === 'outcome_unknown'
@@ -1286,9 +1492,9 @@ function compatibilityJobFromReceipt(receipt: PluginActionReceipt, checkoutId: s
1286
1492
  * Deterministic plugin action path: validate → authorize/confirm → invoke adapter → receipt.
1287
1493
  * Does not create ExecutionJobs or call a model.
1288
1494
  */
1289
- export async function submitAssistantPluginAction(
1495
+ async function submitPluginAction(
1290
1496
  controllerHome: string,
1291
- repository: RepositoryRecord,
1497
+ scope: PluginActionScopeContext,
1292
1498
  request: AssistantPluginActionRequest,
1293
1499
  ): Promise<{
1294
1500
  manifest: AssistantPluginManifest;
@@ -1302,7 +1508,9 @@ export async function submitAssistantPluginAction(
1302
1508
  }> {
1303
1509
  const adapter = resolvePluginAdapter(controllerHome, request.pluginId);
1304
1510
  if (!adapter) throw new Error(`PLUGIN_NOT_FOUND: ${request.pluginId}`);
1305
- const manifest = getAssistantPluginManifest(controllerHome, repository, request.pluginId);
1511
+ const manifest = scope.kind === 'controller'
1512
+ ? getControllerPluginManifest(controllerHome, request.pluginId)
1513
+ : getAssistantPluginManifest(controllerHome, scope.repository!, request.pluginId);
1306
1514
  const action = actionForManifest(manifest, request.actionId);
1307
1515
  if (!manifest.enabled && action.actionId !== 'configure') {
1308
1516
  throw new Error(`PLUGIN_DISABLED: ${request.pluginId} is disabled`);
@@ -1313,36 +1521,38 @@ export async function submitAssistantPluginAction(
1313
1521
  const normalizedArgs = validateActionArguments(action, request.args ?? {});
1314
1522
  enforceConfirmation(action, { ...request, args: normalizedArgs });
1315
1523
 
1316
- const key = semanticKey(repository, request.pluginId, request.actionId, normalizedArgs);
1524
+ const key = semanticKey(scope, request.pluginId, request.actionId, normalizedArgs);
1317
1525
  const requestPath = pluginActionRequestPath(controllerHome, request.requestId);
1318
1526
  if (existsSync(requestPath)) {
1319
1527
  const index = readJsonFile<PluginActionRequestIndex>(requestPath);
1320
1528
  if (index.semanticKey !== key) {
1321
1529
  throw new Error(`REQUEST_ID_CONFLICT: ${request.requestId} already belongs to ${index.semanticKey}`);
1322
1530
  }
1323
- if (index.repoId !== repository.repoId) {
1324
- throw new Error(`REQUEST_ID_REPO_CONFLICT: ${request.requestId} already belongs to repository ${index.repoId}`);
1531
+ const existingScopeKey = index.scopeKey?.trim() || index.repoId?.trim();
1532
+ if (!existingScopeKey) throw new Error(`PLUGIN_RECEIPT_REQUEST_SCOPE_CORRUPT: ${request.requestId}`);
1533
+ if (existingScopeKey !== scope.scopeKey) {
1534
+ throw new Error(`REQUEST_ID_SCOPE_CONFLICT: ${request.requestId} already belongs to ${existingScopeKey}`);
1325
1535
  }
1326
- const existing = readPluginActionReceipt(controllerHome, index.repoId, index.receiptId);
1536
+ const existing = readPluginActionReceipt(controllerHome, existingScopeKey, index.receiptId);
1327
1537
  if (!existing) {
1328
1538
  throw new Error(`PLUGIN_RECEIPT_LOST: ${request.requestId}`);
1329
1539
  }
1330
1540
  if (request.workId && existing.workId !== request.workId) {
1331
1541
  throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_CONFLICT: ${request.requestId} already belongs to Work ${existing.workId ?? 'none'}`);
1332
1542
  }
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}`);
1543
+ const requestedWorkRepoId = request.workId ? workAttributionRepoId(scope, request) : undefined;
1544
+ if (requestedWorkRepoId && index.workRepoId !== requestedWorkRepoId) {
1545
+ throw new Error(`WORK_PLUGIN_ATTRIBUTION_REPO_CONFLICT: ${request.requestId} already belongs to Work repository ${index.workRepoId ?? 'none'}`);
1336
1546
  }
1337
1547
  if (request.workId && action.risk === 'remote_write') {
1338
- bindRemoteEffectReceiptToWork(controllerHome, repository, action, request, existing);
1548
+ bindRemoteEffectReceiptToWork(controllerHome, scope, action, request, existing);
1339
1549
  } else if (request.workId) {
1340
- bindLocalEffectReceiptToAttributedWork(controllerHome, repository, action, request, existing);
1550
+ bindLocalEffectReceiptToAttributedWork(controllerHome, scope, action, request, existing);
1341
1551
  }
1342
1552
  return {
1343
1553
  manifest,
1344
1554
  action,
1345
- job: compatibilityJobFromReceipt(existing, repository.activeCheckoutId),
1555
+ job: compatibilityJobFromReceipt(existing, scope.checkoutId ?? CONTROLLER_PLUGIN_SCOPE_KEY),
1346
1556
  deduplicated: true,
1347
1557
  result: existing.result,
1348
1558
  receipt: existing,
@@ -1360,7 +1570,7 @@ export async function submitAssistantPluginAction(
1360
1570
  if (requiresAutomatedWriteAuthorization(request.origin)) {
1361
1571
  activeAuthorizationGrant = await resolveAutomatedWriteAuthorization({
1362
1572
  controllerHome,
1363
- repository,
1573
+ scope,
1364
1574
  adapter,
1365
1575
  manifest,
1366
1576
  action,
@@ -1380,77 +1590,33 @@ export async function submitAssistantPluginAction(
1380
1590
  };
1381
1591
  }
1382
1592
  }
1383
- const requiresLocalEffectWork = localSystemActionRequiresWork(
1384
- repository,
1385
- request.pluginId,
1386
- request.actionId,
1387
- normalizedArgs,
1388
- );
1389
1593
  const boundRemoteWork = action.risk === 'remote_write'
1390
- ? remoteEffectWorkForPluginAction(controllerHome, repository, action, request)
1594
+ ? remoteEffectWorkForPluginAction(controllerHome, scope, action, request)
1391
1595
  : undefined;
1392
- const attributedWork = !requiresLocalEffectWork
1393
- ? attributedWorkForPluginAction(controllerHome, repository, action, request)
1394
- : 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
1596
+ // Capability execution never creates semantic Work implicitly. Existing Work
1597
+ // attribution is opt-in through work_id; otherwise the plugin receipt is the
1598
+ // durable effect record.
1599
+ const attributedWork = action.risk !== 'remote_write'
1600
+ ? attributedWorkForPluginAction(controllerHome, scope, action, request)
1426
1601
  : 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',
1602
+ if (scope.repository) {
1603
+ appendRuntimeEvent(controllerHome, {
1604
+ repoId: scope.repository.repoId,
1605
+ entityType: 'plugin',
1606
+ entityId: manifest.pluginId,
1607
+ eventType: 'plugin_action_requested',
1608
+ requestId: request.requestId,
1609
+ revision: manifest.revision,
1610
+ data: {
1611
+ actionId: action.actionId,
1612
+ receiptId,
1613
+ risk: action.risk,
1614
+ confirmation: action.confirmation,
1615
+ capabilityId,
1616
+ authorizationReuseSupported: action.confirmation === 'authorization' && Boolean(adapter.resolveAuthorizationContext),
1435
1617
  },
1436
- );
1618
+ });
1437
1619
  }
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
1620
 
1455
1621
  try {
1456
1622
  if (action.confirmation === 'authorization'
@@ -1458,8 +1624,7 @@ export async function submitAssistantPluginAction(
1458
1624
  && originMayEstablishCapabilityAuthorization(request.origin)) {
1459
1625
  authorizationContext = await adapter.resolveAuthorizationContext({
1460
1626
  controllerHome,
1461
- repoId: repository.repoId,
1462
- repoRoot: repository.canonicalRoot,
1627
+ ...pluginActionExecutionFields(scope),
1463
1628
  pluginId: request.pluginId,
1464
1629
  actionId: request.actionId,
1465
1630
  requestId: request.requestId,
@@ -1479,7 +1644,7 @@ export async function submitAssistantPluginAction(
1479
1644
  try {
1480
1645
  activeAuthorizationGrant = findActivePluginCapabilityAuthorization(controllerHome, {
1481
1646
  ownerScope: pluginCapabilityAuthorizationOwnerScope(request.origin),
1482
- repoId: repository.repoId,
1647
+ repoId: scope.grantRepoId,
1483
1648
  pluginId: request.pluginId,
1484
1649
  capabilityId,
1485
1650
  target: authorizationContext.target,
@@ -1506,13 +1671,12 @@ export async function submitAssistantPluginAction(
1506
1671
 
1507
1672
  const result = await withAssistantPluginResourceLeases(
1508
1673
  controllerHome,
1509
- repository,
1674
+ scope,
1510
1675
  action,
1511
1676
  request,
1512
- () => executeAssistantPluginAction({
1677
+ () => executePluginActionInScope(scope, {
1513
1678
  controllerHome,
1514
- repoId: repository.repoId,
1515
- repoRoot: repository.canonicalRoot,
1679
+ ...pluginActionExecutionFields(scope),
1516
1680
  pluginId: request.pluginId,
1517
1681
  actionId: request.actionId,
1518
1682
  requestId: request.requestId,
@@ -1527,6 +1691,31 @@ export async function submitAssistantPluginAction(
1527
1691
  : undefined,
1528
1692
  }),
1529
1693
  );
1694
+ // create_session materializes the exact provider/session identity during
1695
+ // execution, so its authorization target cannot always be resolved before
1696
+ // the action runs. Resolve that postcondition once after a successful
1697
+ // interactive bootstrap, then persist the same exact-target grant path as
1698
+ // every other authorization-class Browser action. Scheduled execution
1699
+ // still remains grant-only because this branch is interactive-origin only.
1700
+ if (action.actionId === 'create_session'
1701
+ && action.confirmation === 'authorization'
1702
+ && !authorizationContext
1703
+ && originMayEstablishCapabilityAuthorization(request.origin)
1704
+ && adapter.resolveAuthorizationContext) {
1705
+ authorizationContext = await adapter.resolveAuthorizationContext({
1706
+ controllerHome,
1707
+ ...pluginActionExecutionFields(scope),
1708
+ pluginId: request.pluginId,
1709
+ actionId: request.actionId,
1710
+ requestId: request.requestId,
1711
+ args: normalizedArgs,
1712
+ origin: request.origin,
1713
+ jobId: receiptId,
1714
+ timeoutMs: request.timeoutMs,
1715
+ signal: request.signal,
1716
+ });
1717
+ }
1718
+
1530
1719
  if (action.confirmation === 'authorization'
1531
1720
  && authorizationContext
1532
1721
  && !activeAuthorizationGrant
@@ -1534,7 +1723,7 @@ export async function submitAssistantPluginAction(
1534
1723
  try {
1535
1724
  const grant = recordPluginCapabilityAuthorization(controllerHome, {
1536
1725
  ownerScope: pluginCapabilityAuthorizationOwnerScope(request.origin),
1537
- repoId: repository.repoId,
1726
+ repoId: scope.grantRepoId,
1538
1727
  pluginId: request.pluginId,
1539
1728
  capabilityId,
1540
1729
  target: authorizationContext.target,
@@ -1568,42 +1757,8 @@ export async function submitAssistantPluginAction(
1568
1757
  work: {
1569
1758
  workId: boundRemoteWork.workId,
1570
1759
  workKind: 'remote_effect',
1760
+ semanticState: 'open' as const,
1571
1761
  ...(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
1762
  },
1608
1763
  };
1609
1764
  }
@@ -1611,48 +1766,50 @@ export async function submitAssistantPluginAction(
1611
1766
  schemaVersion: 1,
1612
1767
  receiptId,
1613
1768
  requestId: request.requestId,
1614
- repoId: repository.repoId,
1615
- ...(request.workId ? { workRepoId: workAttributionRepoId(repository, request) } : {}),
1769
+ scopeKey: scope.scopeKey,
1770
+ ...(scope.repository ? { repoId: scope.repository.repoId } : {}),
1771
+ ...(request.workId ? { workRepoId: workAttributionRepoId(scope, request) } : {}),
1616
1772
  pluginId: request.pluginId,
1617
1773
  actionId: request.actionId,
1618
1774
  semanticKey: key,
1619
1775
  status: 'succeeded',
1620
1776
  createdAt,
1621
- ...(acceptedWork
1622
- ? { workId: acceptedWork.workId }
1623
- : boundRemoteWork
1624
- ? { workId: boundRemoteWork.workId }
1625
- : attributedWork
1626
- ? { workId: attributedWork.workId }
1627
- : {}),
1777
+ ...(boundRemoteWork
1778
+ ? { workId: boundRemoteWork.workId }
1779
+ : attributedWork
1780
+ ? { workId: attributedWork.workId }
1781
+ : {}),
1628
1782
  origin: request.origin,
1629
1783
  authorization,
1630
1784
  result: resultWithLineage,
1631
1785
  };
1632
- writeJsonAtomic(pluginActionReceiptPath(controllerHome, repository.repoId, receiptId), receipt);
1786
+ writeJsonAtomic(pluginActionReceiptPath(controllerHome, scope.scopeKey, receiptId), receipt);
1633
1787
  writeJsonAtomic(requestPath, {
1634
1788
  requestId: request.requestId,
1635
- repoId: repository.repoId,
1636
- ...(request.workId ? { workRepoId: workAttributionRepoId(repository, request) } : {}),
1789
+ scopeKey: scope.scopeKey,
1790
+ ...(scope.repository ? { repoId: scope.repository.repoId } : {}),
1791
+ ...(request.workId ? { workRepoId: workAttributionRepoId(scope, request) } : {}),
1637
1792
  receiptId,
1638
1793
  semanticKey: key,
1639
1794
  createdAt,
1640
1795
  } satisfies PluginActionRequestIndex);
1641
1796
  if (boundRemoteWork) {
1642
- bindRemoteEffectReceiptToWork(controllerHome, repository, action, request, receipt);
1797
+ bindRemoteEffectReceiptToWork(controllerHome, scope, action, request, receipt);
1643
1798
  } else if (attributedWork) {
1644
- bindLocalEffectReceiptToAttributedWork(controllerHome, repository, action, request, receipt, attributedWork);
1799
+ bindLocalEffectReceiptToAttributedWork(controllerHome, scope, action, request, receipt, attributedWork);
1645
1800
  }
1646
- const nextManifest = getAssistantPluginManifest(controllerHome, repository, request.pluginId);
1801
+ const nextManifest = scope.kind === 'controller'
1802
+ ? getControllerPluginManifest(controllerHome, request.pluginId)
1803
+ : getAssistantPluginManifest(controllerHome, scope.repository!, request.pluginId);
1647
1804
  return {
1648
1805
  manifest: nextManifest,
1649
1806
  action,
1650
- job: compatibilityJobFromReceipt(receipt, repository.activeCheckoutId),
1807
+ job: compatibilityJobFromReceipt(receipt, scope.checkoutId ?? CONTROLLER_PLUGIN_SCOPE_KEY),
1651
1808
  deduplicated: false,
1652
1809
  result: resultWithLineage,
1653
1810
  receipt,
1654
1811
  authorization,
1655
- workId: acceptedWork?.workId ?? boundRemoteWork?.workId ?? attributedWork?.workId,
1812
+ workId: boundRemoteWork?.workId ?? attributedWork?.workId,
1656
1813
  };
1657
1814
  } catch (error) {
1658
1815
  const message = error instanceof Error ? error.message : String(error);
@@ -1667,65 +1824,41 @@ export async function submitAssistantPluginAction(
1667
1824
  ? { outcome: 'outcome_unknown', error: { code, message } }
1668
1825
  : undefined;
1669
1826
  if (boundRemoteWork) {
1670
- appendWorkEvidence({ controllerHome, repoId: workAttributionRepoId(repository, request) }, boundRemoteWork.workId, {
1827
+ appendWorkEvidence({ controllerHome, repoId: workAttributionRepoId(scope, request) }, boundRemoteWork.workId, {
1671
1828
  title: outcomeUnknown ? 'typed remote plugin effect outcome unknown' : 'typed remote plugin effect failed',
1672
1829
  summary: `${request.pluginId}/${request.actionId}: ${message}`.slice(0, 1_000),
1673
1830
  detailLevel: 'summary',
1674
1831
  });
1675
1832
  }
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
1833
  const receipt: PluginActionReceipt = {
1701
1834
  schemaVersion: 1,
1702
1835
  receiptId,
1703
1836
  requestId: request.requestId,
1704
- repoId: repository.repoId,
1705
- ...(request.workId ? { workRepoId: workAttributionRepoId(repository, request) } : {}),
1837
+ scopeKey: scope.scopeKey,
1838
+ ...(scope.repository ? { repoId: scope.repository.repoId } : {}),
1839
+ ...(request.workId ? { workRepoId: workAttributionRepoId(scope, request) } : {}),
1706
1840
  pluginId: request.pluginId,
1707
1841
  actionId: request.actionId,
1708
1842
  semanticKey: key,
1709
1843
  status: 'failed',
1710
1844
  effectOutcome,
1711
1845
  createdAt,
1712
- ...(acceptedWork
1713
- ? { workId: acceptedWork.workId }
1714
- : boundRemoteWork
1715
- ? { workId: boundRemoteWork.workId }
1716
- : attributedWork
1717
- ? { workId: attributedWork.workId }
1718
- : {}),
1846
+ ...(boundRemoteWork
1847
+ ? { workId: boundRemoteWork.workId }
1848
+ : attributedWork
1849
+ ? { workId: attributedWork.workId }
1850
+ : {}),
1719
1851
  origin: request.origin,
1720
1852
  authorization,
1721
1853
  ...(outcomeResult ? { result: outcomeResult } : {}),
1722
1854
  error: { code, message },
1723
1855
  };
1724
- writeJsonAtomic(pluginActionReceiptPath(controllerHome, repository.repoId, receiptId), receipt);
1856
+ writeJsonAtomic(pluginActionReceiptPath(controllerHome, scope.scopeKey, receiptId), receipt);
1725
1857
  writeJsonAtomic(requestPath, {
1726
1858
  requestId: request.requestId,
1727
- repoId: repository.repoId,
1728
- ...(request.workId ? { workRepoId: workAttributionRepoId(repository, request) } : {}),
1859
+ scopeKey: scope.scopeKey,
1860
+ ...(scope.repository ? { repoId: scope.repository.repoId } : {}),
1861
+ ...(request.workId ? { workRepoId: workAttributionRepoId(scope, request) } : {}),
1729
1862
  receiptId,
1730
1863
  semanticKey: key,
1731
1864
  createdAt,
@@ -1734,40 +1867,42 @@ export async function submitAssistantPluginAction(
1734
1867
  return {
1735
1868
  manifest,
1736
1869
  action,
1737
- job: compatibilityJobFromReceipt(receipt, repository.activeCheckoutId),
1870
+ job: compatibilityJobFromReceipt(receipt, scope.checkoutId ?? CONTROLLER_PLUGIN_SCOPE_KEY),
1738
1871
  deduplicated: false,
1739
1872
  result: outcomeResult,
1740
1873
  receipt,
1741
1874
  authorization,
1742
- workId: acceptedWork?.workId ?? boundRemoteWork?.workId ?? attributedWork?.workId,
1875
+ workId: boundRemoteWork?.workId ?? attributedWork?.workId,
1743
1876
  };
1744
1877
  }
1745
1878
  throw error;
1746
1879
  }
1747
1880
  }
1748
1881
 
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
- });
1882
+ export function submitAssistantPluginAction(
1883
+ controllerHome: string,
1884
+ repository: RepositoryRecord,
1885
+ request: AssistantPluginActionRequest,
1886
+ ) {
1887
+ return submitPluginAction(controllerHome, repositoryPluginActionScope(repository), request);
1758
1888
  }
1759
1889
 
1760
- export async function executeAssistantPluginAction(
1890
+ export function submitControllerPluginAction(
1891
+ controllerHome: string,
1892
+ request: AssistantPluginActionRequest,
1893
+ ) {
1894
+ return submitPluginAction(controllerHome, controllerPluginActionScope(controllerHome), request);
1895
+ }
1896
+
1897
+ async function executePluginActionInScope(
1898
+ scope: PluginActionScopeContext,
1761
1899
  input: AssistantPluginActionExecutionInput,
1762
1900
  ): Promise<Record<string, unknown>> {
1763
1901
  const adapter = resolvePluginAdapter(input.controllerHome, input.pluginId);
1764
1902
  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);
1903
+ const manifestLookup = scope.kind === 'controller'
1904
+ ? getControllerPluginManifestForExecution(input.controllerHome, input.pluginId, adapter)
1905
+ : getAssistantPluginManifestForExecution(input.controllerHome, scope.repository!, input.pluginId, adapter);
1771
1906
  const manifest = manifestLookup.manifest;
1772
1907
  const action = actionForManifest(manifest, input.actionId);
1773
1908
  if (action.executionMode === 'direct_non_persistent') {
@@ -1777,7 +1912,7 @@ export async function executeAssistantPluginAction(
1777
1912
  if (requiresAutomatedWriteAuthorization(input.origin)) {
1778
1913
  await resolveAutomatedWriteAuthorization({
1779
1914
  controllerHome: input.controllerHome,
1780
- repository,
1915
+ scope,
1781
1916
  adapter,
1782
1917
  manifest,
1783
1918
  action,
@@ -1791,38 +1926,53 @@ export async function executeAssistantPluginAction(
1791
1926
  try {
1792
1927
  const result = await adapter.executeAction({
1793
1928
  ...input,
1929
+ ...pluginActionExecutionFields(scope),
1794
1930
  args: normalizedArgs,
1795
1931
  providerIdentityPrevalidated: manifestLookup.providerIdentityPrevalidated,
1796
1932
  });
1797
1933
  for (const affectedPluginId of adapter.affectedPluginIdsAfterAction?.(input.actionId, result) ?? []) {
1798
1934
  if (!affectedPluginId || affectedPluginId === input.pluginId) continue;
1799
- invalidateAssistantPluginManifestCache(input.controllerHome, repository.repoId, affectedPluginId);
1800
1935
  const affectedAdapter = resolvePluginAdapter(input.controllerHome, affectedPluginId);
1801
- if (affectedAdapter && adapterMatchesRepository(affectedAdapter, repository)) {
1802
- syncAssistantPluginManifest(input.controllerHome, repository, affectedPluginId);
1936
+ if (scope.kind === 'controller') {
1937
+ invalidateAssistantPluginManifestCache(input.controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, affectedPluginId);
1938
+ if (affectedAdapter && adapterMatchesController(affectedAdapter)) {
1939
+ syncControllerPluginManifest(input.controllerHome, affectedPluginId);
1940
+ } else {
1941
+ removeControllerPluginManifestProjection(input.controllerHome, affectedPluginId);
1942
+ }
1803
1943
  } else {
1804
- rmSync(manifestPath(input.controllerHome, repository.repoId, affectedPluginId), { force: true });
1944
+ const repository = scope.repository!;
1945
+ invalidateAssistantPluginManifestCache(input.controllerHome, repository.repoId, affectedPluginId);
1946
+ if (affectedAdapter && adapterMatchesRepository(affectedAdapter, repository)) {
1947
+ syncAssistantPluginManifest(input.controllerHome, repository, affectedPluginId);
1948
+ } else {
1949
+ rmSync(manifestPath(input.controllerHome, repository.repoId, affectedPluginId), { force: true });
1950
+ }
1805
1951
  }
1806
1952
  }
1807
1953
  const refreshManifest = adapter.shouldRefreshManifestAfterAction?.(input.actionId) ?? true;
1808
1954
  const nextManifest = refreshManifest
1809
- ? syncAssistantPluginManifest(input.controllerHome, repository, input.pluginId).manifest
1955
+ ? scope.kind === 'controller'
1956
+ ? syncControllerPluginManifest(input.controllerHome, input.pluginId).manifest
1957
+ : syncAssistantPluginManifest(input.controllerHome, scope.repository!, input.pluginId).manifest
1810
1958
  : 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
- });
1959
+ if (scope.repository) {
1960
+ appendRuntimeEvent(input.controllerHome, {
1961
+ repoId: scope.repository.repoId,
1962
+ entityType: 'plugin',
1963
+ entityId: input.pluginId,
1964
+ eventType: 'plugin_action_succeeded',
1965
+ requestId: input.requestId,
1966
+ revision: nextManifest.revision,
1967
+ data: {
1968
+ actionId: input.actionId,
1969
+ jobId: input.jobId,
1970
+ resultKeys: Object.keys(result).slice(0, 20),
1971
+ lifecycleState: nextManifest.lifecycle.state,
1972
+ healthState: nextManifest.health.state,
1973
+ },
1974
+ });
1975
+ }
1826
1976
  return {
1827
1977
  schemaVersion: 1,
1828
1978
  plugin: {
@@ -1850,25 +2000,49 @@ export async function executeAssistantPluginAction(
1850
2000
  actionId: input.actionId,
1851
2001
  },
1852
2002
  });
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
- });
2003
+ const nextManifest = scope.kind === 'controller'
2004
+ ? syncControllerPluginManifest(input.controllerHome, input.pluginId).manifest
2005
+ : syncAssistantPluginManifest(input.controllerHome, scope.repository!, input.pluginId).manifest;
2006
+ if (scope.repository) {
2007
+ appendRuntimeEvent(input.controllerHome, {
2008
+ repoId: scope.repository.repoId,
2009
+ entityType: 'plugin',
2010
+ entityId: input.pluginId,
2011
+ eventType: 'plugin_action_failed',
2012
+ requestId: input.requestId,
2013
+ revision: nextManifest.revision,
2014
+ data: {
2015
+ actionId: input.actionId,
2016
+ jobId: input.jobId,
2017
+ code: pluginError.code,
2018
+ retryable: pluginError.retryable,
2019
+ },
2020
+ });
2021
+ }
1869
2022
  throw new AssistantPluginError(pluginError.code, pluginError.message.replace(/^[^:]+:\s*/, ''), {
1870
2023
  retryable: pluginError.retryable,
1871
2024
  details: pluginError.details,
1872
2025
  });
1873
2026
  }
1874
2027
  }
2028
+
2029
+ export function executeControllerScopedPluginAction(
2030
+ input: Omit<AssistantPluginActionExecutionInput, 'repoId' | 'repoRoot'>,
2031
+ ): Promise<Record<string, unknown>> {
2032
+ const scope = controllerPluginActionScope(input.controllerHome);
2033
+ return executePluginActionInScope(scope, {
2034
+ ...input,
2035
+ ...pluginActionExecutionFields(scope),
2036
+ });
2037
+ }
2038
+
2039
+ export function executeAssistantPluginAction(
2040
+ input: AssistantPluginActionExecutionInput,
2041
+ ): Promise<Record<string, unknown>> {
2042
+ const repository = {
2043
+ repoId: input.repoId,
2044
+ canonicalRoot: input.repoRoot,
2045
+ activeCheckoutId: 'active',
2046
+ } as RepositoryRecord;
2047
+ return executePluginActionInScope(repositoryPluginActionScope(repository), input);
2048
+ }