@moretea-labs/forge 1.7.2 → 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 (900) hide show
  1. package/AGENTS.md +121 -62
  2. package/CHANGELOG.md +40 -0
  3. package/CLAUDE.md +57 -62
  4. package/CONTRIBUTING.md +3 -2
  5. package/README.md +2 -2
  6. package/SKILL.md +0 -1
  7. package/THIRD_PARTY_NOTICES.md +5 -2
  8. package/adapters/browser/index.ts +2 -0
  9. package/adapters/chatgpt/browser-delivery-host.ts +77 -0
  10. package/adapters/chatgpt/browser-delivery-runtime.ts +1256 -0
  11. package/adapters/chatgpt/controller-binding-store.ts +134 -0
  12. package/adapters/chatgpt/controller-round-host.ts +68 -0
  13. package/adapters/chatgpt/controller-round-settlement-store.ts +99 -0
  14. package/adapters/chatgpt/provider-delivery.ts +340 -0
  15. package/adapters/chatgpt/work-conversation-binding-store.ts +218 -0
  16. package/adapters/chatgpt/wsl-bridge-delivery-host.ts +63 -0
  17. package/adapters/computer/chatgpt-extension-target.ts +463 -0
  18. package/adapters/computer/chatgpt-target-router.ts +103 -0
  19. package/adapters/computer/chrome-extension/background.js +385 -0
  20. package/adapters/computer/chrome-extension/content.js +378 -0
  21. package/adapters/computer/chrome-extension/core.js +24 -0
  22. package/adapters/computer/chrome-extension/manifest.json +30 -0
  23. package/adapters/computer/desktop-operator-contract.ts +7 -0
  24. package/adapters/computer/desktop-operator-discovery.ts +181 -0
  25. package/adapters/computer/desktop-operator-negotiation.ts +174 -0
  26. package/adapters/computer/desktop-operator-provider.ts +312 -0
  27. package/adapters/computer/index.ts +1 -0
  28. package/adapters/computer/interaction-target-authority.ts +1068 -0
  29. package/adapters/controller-process/binding-store.ts +34 -0
  30. package/adapters/controller-process/controller-host.ts +26 -0
  31. package/adapters/mcp/access-mode.ts +132 -0
  32. package/adapters/mcp/audit.ts +32 -0
  33. package/adapters/mcp/auth.ts +406 -0
  34. package/adapters/mcp/instructions.ts +37 -0
  35. package/adapters/mcp/legacy-context.ts +18 -0
  36. package/adapters/mcp/multi-repository.ts +421 -0
  37. package/adapters/mcp/oauth.ts +464 -0
  38. package/adapters/mcp/paths.ts +143 -0
  39. package/adapters/mcp/policy.ts +232 -0
  40. package/adapters/mcp/proxy-env.ts +62 -0
  41. package/adapters/mcp/readiness-projection.ts +24 -0
  42. package/adapters/mcp/redaction.ts +21 -0
  43. package/adapters/mcp/repo.ts +24 -0
  44. package/adapters/mcp/runtime-gateway/artifact-adapter.ts +52 -0
  45. package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +87 -0
  46. package/adapters/mcp/runtime-gateway/bounded-result-runtime.ts +62 -0
  47. package/adapters/mcp/runtime-gateway/cognition-settlement.ts +201 -0
  48. package/adapters/mcp/runtime-gateway/context-adapter.ts +1167 -0
  49. package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +91 -0
  50. package/adapters/mcp/runtime-gateway/controller-authority-adapter.ts +206 -0
  51. package/adapters/mcp/runtime-gateway/core-capability-adapter.ts +258 -0
  52. package/adapters/mcp/runtime-gateway/engineering-preconditions.ts +130 -0
  53. package/adapters/mcp/runtime-gateway/engineering-tool-contract.ts +15 -0
  54. package/adapters/mcp/runtime-gateway/execution-control-adapter.ts +28 -0
  55. package/adapters/mcp/runtime-gateway/execution-tools.ts +173 -0
  56. package/adapters/mcp/runtime-gateway/filesystem-adapter.ts +38 -0
  57. package/adapters/mcp/runtime-gateway/gateway-execution-adapter.ts +412 -0
  58. package/adapters/mcp/runtime-gateway/legacy-ios-tool-adapter.ts +105 -0
  59. package/adapters/mcp/runtime-gateway/maintenance-adapter.ts +44 -0
  60. package/adapters/mcp/runtime-gateway/model-adapter.ts +63 -0
  61. package/adapters/mcp/runtime-gateway/persisted-check-process.ts +3 -0
  62. package/adapters/mcp/runtime-gateway/plugin-adapter.ts +380 -0
  63. package/adapters/mcp/runtime-gateway/process-tools.ts +457 -0
  64. package/adapters/mcp/runtime-gateway/protected-computer-adapter.ts +88 -0
  65. package/adapters/mcp/runtime-gateway/recovery-adapter.ts +285 -0
  66. package/adapters/mcp/runtime-gateway/recovery-client-adapter.ts +70 -0
  67. package/adapters/mcp/runtime-gateway/recovery-tool-contract.ts +99 -0
  68. package/adapters/mcp/runtime-gateway/repository-compat-adapter.ts +79 -0
  69. package/adapters/mcp/runtime-gateway/result-adapter.ts +114 -0
  70. package/adapters/mcp/runtime-gateway/router.ts +242 -0
  71. package/adapters/mcp/runtime-gateway/routing-policy.ts +499 -0
  72. package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1200 -0
  73. package/adapters/mcp/runtime-gateway/runtime-readiness-observation.ts +218 -0
  74. package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +574 -0
  75. package/adapters/mcp/runtime-gateway/runtime-tool-shared.ts +27 -0
  76. package/adapters/mcp/runtime-gateway/runtime-tools.ts +78 -0
  77. package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +187 -0
  78. package/adapters/mcp/runtime-gateway/shared-adapter.ts +45 -0
  79. package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +962 -0
  80. package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +136 -0
  81. package/adapters/mcp/runtime-gateway/work-adapter.ts +325 -0
  82. package/adapters/mcp/runtime-gateway/work-controller-operations.ts +257 -0
  83. package/adapters/mcp/runtime-gateway/work-detail-projection.ts +60 -0
  84. package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +246 -0
  85. package/adapters/mcp/runtime-gateway/work-learning-operations.ts +145 -0
  86. package/adapters/mcp/runtime-gateway/work-list-projection.ts +38 -0
  87. package/adapters/mcp/runtime-gateway/work-plan-operations.ts +382 -0
  88. package/adapters/mcp/runtime-gateway/work-repair-adapter.ts +308 -0
  89. package/adapters/mcp/runtime-gateway/work-requirement-operations.ts +148 -0
  90. package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +263 -0
  91. package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +1 -0
  92. package/adapters/mcp/runtime-gateway/work-workflow-operations.ts +113 -0
  93. package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +156 -0
  94. package/adapters/mcp/server.ts +1092 -0
  95. package/adapters/mcp/setup.ts +789 -0
  96. package/adapters/mcp/tool-capability-cache.ts +87 -0
  97. package/adapters/mcp/tool-mapping/access-tools.ts +387 -0
  98. package/adapters/mcp/tool-mapping/legacy-tool-service.ts +5139 -0
  99. package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +145 -0
  100. package/adapters/mcp/tool-mapping/repository-tools.ts +1682 -0
  101. package/adapters/mcp/tool-mapping/tools.ts +9 -0
  102. package/adapters/mcp/tool-mapping/umbrella-tools.ts +117 -0
  103. package/adapters/mcp/toolset-names.ts +79 -0
  104. package/adapters/mcp/toolset-selection.ts +41 -0
  105. package/adapters/mcp/toolset.ts +337 -0
  106. package/adapters/mcp/transports/http-observation.ts +365 -0
  107. package/adapters/mcp/transports/http.ts +933 -0
  108. package/adapters/mcp/transports/oauth-http.ts +322 -0
  109. package/adapters/mcp/transports/session-registry.ts +519 -0
  110. package/adapters/mcp/transports/stdio.ts +8 -0
  111. package/adapters/mcp/tunnels/openai-secure-tunnel.ts +223 -0
  112. package/adapters/mcp/types.ts +41 -0
  113. package/assets/AGENTS.md +2 -16
  114. package/assets/CLAUDE.md +2 -16
  115. package/assets/hooks/AGENTS.md +3 -14
  116. package/assets/hooks/CLAUDE.md +3 -14
  117. package/assets/hooks/hook-input.sh +9 -2
  118. package/assets/hooks/lib/workflow-state.sh +28 -10
  119. package/assets/hooks/prompt-guard.sh +63 -37
  120. package/assets/hooks/security-sentinel.sh +2 -1
  121. package/assets/hooks/session-start-context.sh +7 -5
  122. package/assets/hooks/stop-orchestrator.sh +13 -3
  123. package/assets/initializer-question-pack.v4.json +11 -7
  124. package/assets/partials/05-workflow.partial.md +18 -73
  125. package/assets/partials/07-footer.partial.md +1 -1
  126. package/assets/partials-agents/02-operating-mode.partial.md +1 -2
  127. package/assets/partials-agents/04-task-protocol.partial.md +14 -59
  128. package/assets/partials-agents/06-quality-safety.partial.md +11 -16
  129. package/assets/partials-agents/08-deep-docs.partial.md +1 -2
  130. package/assets/plan-map.json +11 -11
  131. package/assets/plugin-registry.v1.json +11 -3
  132. package/assets/plugin-trust-policy.v1.json +6 -0
  133. package/assets/recovery/ForgeRecovery.ps1 +100 -0
  134. package/assets/recovery/forge-wsl-rescue +271 -0
  135. package/assets/reference-configs/agentic-development-flow.md +25 -0
  136. package/assets/reference-configs/development-protocol.md +11 -15
  137. package/assets/reference-configs/document-generation.md +1 -1
  138. package/assets/reference-configs/global-working-rules.md +28 -1
  139. package/assets/reference-configs/harness-overview.md +22 -112
  140. package/assets/reference-configs/hook-operations.md +1 -1
  141. package/assets/reference-configs/sprint-contracts.md +2 -2
  142. package/assets/skill-version.json +2 -2
  143. package/assets/templates/helpers/architecture-event.ts +3 -34
  144. package/assets/templates/helpers/architecture-queue.sh +61 -10
  145. package/assets/templates/helpers/archive-workflow.sh +90 -180
  146. package/assets/templates/helpers/capability-config.ts +8 -11
  147. package/assets/templates/helpers/capability-resolver.ts +37 -19
  148. package/assets/templates/helpers/capture-plan.sh +2 -2
  149. package/assets/templates/helpers/check-architecture-sync.sh +1 -9
  150. package/assets/templates/helpers/check-task-sync.sh +82 -11
  151. package/assets/templates/helpers/check-task-workflow.sh +1 -3
  152. package/assets/templates/helpers/context-contract-sync.sh +1 -12
  153. package/assets/templates/helpers/contract-worktree.sh +26 -24
  154. package/assets/templates/helpers/ensure-task-workflow.sh +6 -8
  155. package/assets/templates/helpers/inspect-project-state.ts +1 -1
  156. package/assets/templates/helpers/migrate-project-template.sh +1 -1
  157. package/assets/templates/helpers/migrate-workflow-docs.ts +53 -239
  158. package/assets/templates/helpers/new-plan.sh +2 -2
  159. package/assets/templates/helpers/plan-to-todo.sh +3 -31
  160. package/assets/templates/helpers/sprint-backlog.sh +1 -1
  161. package/assets/templates/helpers/switch-plan.sh +1 -1
  162. package/assets/templates/helpers/workflow-contract.ts +1 -0
  163. package/assets/workflow-contract.v1.json +63 -171
  164. package/assets/workflows/xiaohongshu/xiaohongshu.collect-metrics.v1.draft.json +118 -0
  165. package/assets/workflows/xiaohongshu/xiaohongshu.publish-image-note.v1.draft.json +316 -0
  166. package/assets/workflows/xiaohongshu/xiaohongshu.publish-long-text.v1.draft.json +324 -0
  167. package/docs/architecture/CURRENT.md +233 -28
  168. package/docs/architecture/EVOLUTION.md +13 -0
  169. package/docs/architecture/decisions/20260801-controller-home-sqlite-state.md +2 -1
  170. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +14 -5
  171. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +6 -3
  172. package/docs/architecture/decisions/20260831-independent-windows-wsl-host-rescue.md +59 -0
  173. package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +120 -0
  174. package/docs/architecture/decisions/20260904-repository-runtime-data-boundary.md +42 -0
  175. package/docs/architecture/decisions/20260907-assistant-learning-loop.md +37 -0
  176. package/docs/architecture/decisions/20260907-controller-round-transition-policy.md +311 -0
  177. package/docs/architecture/decisions/20260907-maintenance-diagnosis-bounded-read.md +39 -0
  178. package/docs/architecture/decisions/20260916-workflow-supervisor-authority.md +249 -0
  179. package/docs/architecture/decisions/20260917-cognitive-memory-foundation.md +82 -0
  180. package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +221 -0
  181. package/docs/architecture/decisions/20260919-autonomous-work-liveness.md +62 -0
  182. package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +72 -0
  183. package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +237 -0
  184. package/docs/architecture/decisions/20261005-forge-v3-desktop-client-boundary.md +77 -0
  185. package/docs/architecture/domains/verification.md +8 -0
  186. package/docs/architecture/domains/workflow-engine.md +7 -1
  187. package/docs/architecture/index.md +3 -3
  188. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +19 -8
  189. package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +124 -0
  190. package/docs/forge-chatgpt-mcp-setup.md +5 -3
  191. package/docs/forge-plugin-management.md +20 -4
  192. package/docs/forge-runtime-self-healing-loop.md +2 -0
  193. package/docs/operations/benchmark-cross-version-freeze.md +38 -0
  194. package/docs/operations/controller-browser-plugin.md +3 -1
  195. package/docs/operations/controller-performance-and-502.md +17 -0
  196. package/docs/operations/controller-reliability-runbook.md +2 -2
  197. package/docs/operations/features.md +1 -1
  198. package/docs/operations/features.zh-CN.md +1 -1
  199. package/docs/operations/homebrew.md +2 -2
  200. package/docs/operations/mcp-tool-exposure.md +2 -0
  201. package/docs/operations/platform-support.md +16 -8
  202. package/docs/operations/platform-support.zh-CN.md +2 -2
  203. package/docs/operations/releasing.md +2 -0
  204. package/docs/operations/standalone-disaster-recovery.md +38 -5
  205. package/docs/operations/uu-remote-rescue.md +41 -0
  206. package/docs/operations/windows-wsl-host-rescue.md +133 -0
  207. package/docs/public-usage-guide.md +1 -1
  208. package/docs/public-usage-guide.zh-CN.md +1 -1
  209. package/docs/tutorials/01-install-and-start.md +1 -1
  210. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  211. package/docs/tutorials/02-connect-chatgpt.md +4 -4
  212. package/docs/tutorials/02-connect-chatgpt.zh-CN.md +4 -4
  213. package/docs/wiki/Implementation.md +1 -1
  214. package/package.json +46 -26
  215. package/packages/kernel/cognition/api/index.ts +6 -0
  216. package/packages/kernel/cognition/application/activation-service.ts +451 -0
  217. package/packages/kernel/cognition/application/consolidation-service.ts +185 -0
  218. package/packages/kernel/cognition/application/legacy-experience-adapter.ts +38 -0
  219. package/packages/kernel/cognition/application/memory-service.ts +64 -0
  220. package/packages/kernel/cognition/domain/learning.ts +106 -0
  221. package/packages/kernel/cognition/domain/memory.ts +254 -0
  222. package/packages/kernel/controller/api/index.ts +3 -0
  223. package/packages/kernel/controller/application/continuation-service.ts +281 -0
  224. package/packages/kernel/controller/application/controller-round-service.ts +41 -0
  225. package/packages/kernel/controller/application/controller-service.ts +30 -0
  226. package/packages/kernel/controller/domain/controller-round-transition-policy.ts +529 -0
  227. package/packages/kernel/controller/domain/controller-round.ts +110 -0
  228. package/packages/kernel/controller/domain/execution-quality.ts +135 -0
  229. package/packages/kernel/controller/domain/types.ts +56 -0
  230. package/packages/kernel/controller/index.ts +1 -0
  231. package/packages/kernel/controller/infrastructure/controller-binding-store.ts +84 -0
  232. package/packages/kernel/controller/infrastructure/controller-round-store.ts +1970 -0
  233. package/packages/kernel/controller/infrastructure/controller-session-store.ts +804 -0
  234. package/packages/kernel/controller/ports/controller-host.ts +18 -0
  235. package/packages/kernel/goal/api/index.ts +1 -0
  236. package/packages/kernel/goal/domain/types.ts +40 -0
  237. package/packages/kernel/goal/index.ts +1 -0
  238. package/packages/kernel/identity/api/index.ts +15 -0
  239. package/packages/kernel/identity/application/identity-service.ts +67 -0
  240. package/packages/kernel/identity/domain/activity-projection.ts +43 -0
  241. package/packages/kernel/identity/domain/capability-broker.ts +60 -0
  242. package/packages/kernel/identity/domain/grant.ts +72 -0
  243. package/packages/kernel/identity/domain/owned-resource.ts +116 -0
  244. package/packages/kernel/identity/domain/scope.ts +63 -0
  245. package/packages/kernel/identity/domain/semantic-sync.ts +81 -0
  246. package/packages/kernel/identity/domain/types.ts +44 -0
  247. package/packages/kernel/identity/domain/user-request.ts +93 -0
  248. package/packages/kernel/identity/domain/workspace.ts +56 -0
  249. package/packages/kernel/identity/index.ts +1 -0
  250. package/packages/kernel/identity/infrastructure/activity-projection-store.ts +77 -0
  251. package/packages/kernel/identity/infrastructure/capability-broker.ts +81 -0
  252. package/packages/kernel/identity/infrastructure/grant-store.ts +284 -0
  253. package/packages/kernel/identity/infrastructure/identity-store.ts +75 -0
  254. package/packages/kernel/identity/infrastructure/owned-resource-store.ts +259 -0
  255. package/packages/kernel/identity/infrastructure/user-request-store.ts +182 -0
  256. package/packages/kernel/memory/api/index.ts +5 -0
  257. package/packages/kernel/memory/application/experience-service.ts +118 -0
  258. package/packages/kernel/memory/application/outcome-service.ts +77 -0
  259. package/packages/kernel/memory/domain/experience.ts +103 -0
  260. package/packages/kernel/memory/domain/operational-prior.ts +92 -0
  261. package/packages/kernel/memory/index.ts +1 -0
  262. package/packages/kernel/scheduler/api/index.ts +4 -0
  263. package/packages/kernel/scheduler/application/eligibility.ts +252 -0
  264. package/packages/kernel/scheduler/application/schedule-service.ts +27 -0
  265. package/packages/kernel/scheduler/application/settlement.ts +182 -0
  266. package/packages/kernel/scheduler/domain/schedule.ts +236 -0
  267. package/packages/kernel/scheduler/index.ts +1 -0
  268. package/packages/kernel/scheduler/infrastructure/schedule-store.ts +805 -0
  269. package/packages/kernel/work/api/index.ts +16 -0
  270. package/packages/kernel/work/application/work-service.ts +123 -0
  271. package/packages/kernel/work/domain/check-receipt.ts +43 -0
  272. package/packages/kernel/work/domain/context-closure-receipt.ts +32 -0
  273. package/packages/kernel/work/domain/current-task-lineage.ts +104 -0
  274. package/packages/kernel/work/domain/engineering-contracts.ts +157 -0
  275. package/packages/kernel/work/domain/engineering-design.ts +286 -0
  276. package/packages/kernel/work/domain/engineering-profile.ts +238 -0
  277. package/packages/kernel/work/domain/evaluation-promotion-receipt.ts +186 -0
  278. package/packages/kernel/work/domain/execution-concurrency.ts +247 -0
  279. package/packages/kernel/work/domain/execution-snapshot.ts +2 -0
  280. package/packages/kernel/work/domain/implementation-review.ts +193 -0
  281. package/packages/kernel/work/domain/objective-graph.ts +119 -0
  282. package/packages/kernel/work/domain/project-engineering-contract.ts +224 -0
  283. package/packages/kernel/work/domain/repository-completion-receipt.ts +73 -0
  284. package/{src/runtime/control-plane/facade/work-state-machine.ts → packages/kernel/work/domain/state-machine.ts} +37 -66
  285. package/packages/kernel/work/domain/types.ts +534 -0
  286. package/packages/kernel/work/index.ts +1 -0
  287. package/packages/kernel/work/infrastructure/work-contract-store.ts +1967 -0
  288. package/packages/kernel/work/ports/work-contract-store.ts +21 -0
  289. package/packages/plugin-runtime/browser/index.ts +3 -0
  290. package/packages/plugin-runtime/browser/provider-registry.ts +110 -0
  291. package/packages/plugin-runtime/browser/runtime-contract.ts +150 -0
  292. package/packages/plugin-runtime/browser/session-authority.ts +62 -0
  293. package/packages/plugin-runtime/computer/chatgpt-identity.ts +24 -0
  294. package/packages/plugin-runtime/computer/conversation-target.ts +143 -0
  295. package/packages/plugin-runtime/computer/index.ts +8 -0
  296. package/packages/plugin-runtime/computer/provider-error.ts +26 -0
  297. package/packages/plugin-runtime/computer/provider-registration.ts +52 -0
  298. package/packages/plugin-runtime/computer/provider-registry.ts +63 -0
  299. package/packages/plugin-runtime/computer/provider.ts +19 -0
  300. package/packages/plugin-runtime/computer/target-authority.ts +215 -0
  301. package/packages/plugin-runtime/computer/target-persistence.ts +54 -0
  302. package/packages/plugin-runtime/external/index.ts +1 -0
  303. package/packages/plugin-runtime/external/unix-jsonl-transport.ts +581 -0
  304. package/packages/plugin-runtime/index.ts +3 -0
  305. package/packages/protocols/browser/index.ts +1 -0
  306. package/packages/protocols/browser/session.ts +98 -0
  307. package/packages/protocols/computer/contract.ts +225 -0
  308. package/packages/protocols/computer/index.ts +1 -0
  309. package/packages/protocols/failure/contract.ts +158 -0
  310. package/packages/protocols/failure/index.ts +1 -0
  311. package/packages/protocols/handoff/index.ts +1 -0
  312. package/packages/protocols/handoff/status.ts +22 -0
  313. package/packages/protocols/mcp/execution-context.ts +11 -0
  314. package/packages/protocols/mcp/index.ts +2 -0
  315. package/packages/protocols/mcp/tool-contract.ts +42 -0
  316. package/packages/workflow-runtime/api/index.ts +2 -0
  317. package/packages/workflow-runtime/application/interpreter.ts +342 -0
  318. package/packages/workflow-runtime/domain/workflow-asset.ts +223 -0
  319. package/packages/workflow-runtime/index.ts +1 -0
  320. package/scripts/activate-source-baseline.ts +7 -9
  321. package/scripts/assemble-template.ts +1 -1
  322. package/scripts/bootstrap-runtime-maintenance-recovery.sh +4 -1
  323. package/scripts/canary-chatgpt-provider-delivery.ts +97 -0
  324. package/scripts/canary-controller-semantic-wait.ts +183 -0
  325. package/scripts/canary-global-hook.sh +2 -2
  326. package/scripts/check-mcp-compatibility.ts +85 -7
  327. package/scripts/check-platform-support.mjs +28 -2
  328. package/scripts/check-public-docs.sh +1 -1
  329. package/scripts/check-repository-hygiene.mjs +92 -0
  330. package/scripts/check-runtime-architecture.mjs +1643 -127
  331. package/scripts/check-source-duplication.mjs +9 -1
  332. package/scripts/controller-ngrok-rotation.sh +5 -4
  333. package/scripts/create-project-dirs.sh +10 -74
  334. package/scripts/forge-local-recovery-helper.mjs +282 -0
  335. package/scripts/forge-uu-remote-rescue-helper.d.mts +31 -0
  336. package/scripts/forge-uu-remote-rescue-helper.mjs +409 -0
  337. package/scripts/forge-windows-host-recovery-helper.mjs +136 -0
  338. package/scripts/hook-shim.sh +2 -2
  339. package/scripts/init-project.sh +12 -70
  340. package/scripts/install-desktop-operator-registration.ts +4 -2
  341. package/scripts/install-independent-windows-wsl-recovery.ts +125 -0
  342. package/scripts/install-standalone-recovery.ts +122 -10
  343. package/scripts/install-uu-remote-rescue-registration.ts +74 -0
  344. package/scripts/install-windows-host-recovery-registration.ts +22 -0
  345. package/scripts/lib/controller-home-env.sh +6 -3
  346. package/scripts/lib/project-init-lib.sh +42 -582
  347. package/scripts/migrate-chatgpt-automation-schedules.ts +2 -2
  348. package/scripts/migrate-windows-wsl-controller-home.ts +306 -0
  349. package/scripts/open-source-audit-allowlist.txt +11 -1
  350. package/scripts/portability-bindings.mjs +70 -0
  351. package/scripts/run-bounded-command.mjs +60 -0
  352. package/scripts/run-bun-test-file.ts +22 -6
  353. package/scripts/run-governed-gate.ts +4 -2
  354. package/scripts/run-mobile-monitor-bridge.ts +0 -1
  355. package/scripts/smoke-control-plane-failure-domain.ts +94 -0
  356. package/scripts/smoke-mcp-http-runtime.ts +6 -1
  357. package/scripts/smoke-runtime-recovery.ts +13 -11
  358. package/scripts/smoke-schedule-engine.ts +24 -16
  359. package/scripts/smoke-unattended-transport-recovery.ts +223 -0
  360. package/scripts/smoke-user-request-continuation-restart.ts +232 -0
  361. package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +92 -0
  362. package/scripts/smoke-workflow-supervisor-process-restart.ts +276 -0
  363. package/scripts/smoke-workflow-supervisor-restart-recovery.ts +150 -0
  364. package/scripts/smoke-workflow-supervisor.ts +103 -0
  365. package/scripts/test-governance.ts +51 -2
  366. package/scripts/verify-forge-runtime.sh +2 -1
  367. package/src/cli/agent-jobs/executor-health.ts +11 -34
  368. package/src/cli/agent-jobs/integration.ts +18 -49
  369. package/src/cli/agent-jobs/job-manager.ts +17 -4
  370. package/src/cli/chatgpt-browser/bind-server.ts +2 -1
  371. package/src/cli/chatgpt-browser/binding.ts +20 -10
  372. package/src/cli/chatgpt-browser/bridge-extension.ts +75 -0
  373. package/src/cli/chatgpt-browser/bridge-provider.ts +376 -11
  374. package/src/cli/chatgpt-browser/engine.ts +10 -11
  375. package/src/cli/chatgpt-browser/native-provider.ts +24 -83
  376. package/src/cli/chatgpt-browser/oracle-provider.ts +5 -3
  377. package/src/cli/chatgpt-browser/types.ts +1 -0
  378. package/src/cli/commands/bootstrap-control.ts +334 -0
  379. package/src/cli/commands/brain-assistant.ts +52 -0
  380. package/src/cli/commands/brain.ts +191 -40
  381. package/src/cli/commands/chatgpt.ts +32 -67
  382. package/src/cli/commands/computer.ts +437 -0
  383. package/src/cli/commands/doctor.ts +18 -1
  384. package/src/cli/commands/init-hook.ts +71 -61
  385. package/src/cli/commands/init.ts +3 -1
  386. package/src/cli/commands/mcp.ts +16 -10
  387. package/src/cli/commands/plugin.ts +38 -12
  388. package/src/cli/commands/recovery.ts +507 -80
  389. package/src/cli/commands/runtime.ts +12 -7
  390. package/src/cli/commands/setup-profile.ts +97 -39
  391. package/src/cli/commands/status.ts +2 -1
  392. package/src/cli/controller/check-runner.ts +500 -100
  393. package/src/cli/controller/completion-backlog.ts +3 -3
  394. package/src/cli/controller/completion-orchestrator.ts +4 -43
  395. package/src/cli/controller/completion-target.ts +3 -72
  396. package/src/cli/controller/context/source-materializer.ts +81 -100
  397. package/src/cli/controller/context/types.ts +7 -1
  398. package/src/cli/controller/context-pack.ts +28 -6
  399. package/src/cli/controller/execution-completion.ts +4 -7
  400. package/src/cli/controller/execution-policy.ts +36 -153
  401. package/src/cli/controller/governance.ts +2 -3
  402. package/src/cli/controller/issue-store.ts +4 -99
  403. package/src/cli/controller/launch-agents.ts +59 -18
  404. package/src/cli/controller/operational-plan.ts +24 -51
  405. package/src/cli/controller/runtime-config.ts +5 -1
  406. package/src/cli/controller/stuck-state-migration.ts +2 -3
  407. package/src/cli/controller/systemd-user.ts +148 -0
  408. package/src/cli/controller/types.ts +10 -70
  409. package/src/cli/controller/worklog.ts +67 -13
  410. package/src/cli/editing/edit-operation-contract.ts +92 -0
  411. package/src/cli/editing/edit-session.ts +290 -20
  412. package/src/cli/github/plugin.ts +17 -12
  413. package/src/cli/hook/route-registry.ts +3 -5
  414. package/src/cli/hook/runtime.ts +49 -3
  415. package/src/cli/index.ts +3 -0
  416. package/src/cli/local-bridge/connector-freshness.ts +2 -2
  417. package/src/cli/local-bridge/console-automations.ts +20 -15
  418. package/src/cli/local-bridge/console-view-models.ts +3 -12
  419. package/src/cli/local-bridge/facade-api.ts +105 -205
  420. package/src/cli/local-bridge/job-store.ts +1 -1
  421. package/src/cli/local-bridge/server.ts +48 -174
  422. package/src/cli/local-bridge/types.ts +0 -1
  423. package/src/cli/mcp/access-mode.ts +2 -132
  424. package/src/cli/mcp/access-tools.ts +2 -387
  425. package/src/cli/mcp/audit.ts +2 -32
  426. package/src/cli/mcp/auth.ts +2 -404
  427. package/src/cli/mcp/instructions.ts +2 -34
  428. package/src/cli/mcp/legacy-context.ts +2 -16
  429. package/src/cli/mcp/legacy-tool-service.ts +2 -5308
  430. package/src/cli/mcp/multi-repository.ts +2 -436
  431. package/src/cli/mcp/oauth-client-retirement.ts +74 -0
  432. package/src/cli/mcp/oauth.ts +2 -375
  433. package/src/cli/mcp/openai-secure-tunnel.ts +2 -0
  434. package/src/cli/mcp/paths.ts +2 -140
  435. package/src/cli/mcp/policy.ts +2 -232
  436. package/src/cli/mcp/post-finalize-work-attribution.ts +2 -150
  437. package/src/cli/mcp/proxy-env.ts +2 -62
  438. package/src/cli/mcp/readiness-projection.ts +2 -24
  439. package/src/cli/mcp/redaction.ts +2 -21
  440. package/src/cli/mcp/repo.ts +2 -24
  441. package/src/cli/mcp/repository-tools.ts +2 -1613
  442. package/src/cli/mcp/server.ts +2 -951
  443. package/src/cli/mcp/setup.ts +2 -737
  444. package/src/cli/mcp/tool-capability-cache.ts +2 -87
  445. package/src/cli/mcp/tool-contract.ts +2 -14
  446. package/src/cli/mcp/tools.ts +2 -9
  447. package/src/cli/mcp/toolset-names.ts +2 -70
  448. package/src/cli/mcp/toolset-selection.ts +2 -41
  449. package/src/cli/mcp/toolset.ts +2 -336
  450. package/src/cli/mcp/transports/AGENTS.md +1 -10
  451. package/src/cli/mcp/transports/CLAUDE.md +1 -10
  452. package/src/cli/mcp/transports/http.ts +2 -1414
  453. package/src/cli/mcp/transports/session-registry.ts +2 -490
  454. package/src/cli/mcp/transports/stdio.ts +2 -8
  455. package/src/cli/mcp/types.ts +2 -41
  456. package/src/cli/mcp/umbrella-tools.ts +2 -117
  457. package/src/cli/repositories/command-executor.ts +91 -39
  458. package/src/cli/repositories/command-scope.ts +139 -4
  459. package/src/cli/repositories/controller-home.ts +167 -1
  460. package/src/cli/repositories/ephemeral-workspace.ts +20 -30
  461. package/src/cli/repositories/git-command-executor.ts +2 -2
  462. package/src/cli/repositories/identity.ts +13 -0
  463. package/src/cli/repositories/registry.ts +145 -60
  464. package/src/cli/repositories/repository-snapshot.ts +53 -7
  465. package/src/cli/repositories/runtime-storage.ts +27 -2
  466. package/src/cli/repositories/safe-patch.ts +34 -38
  467. package/src/cli/repositories/selected-path-actions.ts +22 -2
  468. package/src/cli/repositories/structured-git.ts +167 -27
  469. package/src/cli/repository/session-cache.ts +92 -6
  470. package/src/cli/runtime/helper-runner.ts +7 -0
  471. package/src/cli/runtime-invocation.ts +8 -1
  472. package/src/cli/tools/codegraph.ts +44 -25
  473. package/src/core/adoption/manifest-templates.ts +1 -1
  474. package/src/core/adoption/plan.ts +3 -26
  475. package/src/core/adoption/workflow-contract-plan.ts +17 -9
  476. package/src/effects/process-runner.ts +8 -14
  477. package/src/runtime/context/assistant-context.ts +167 -0
  478. package/src/runtime/context/assistant-work-context.ts +209 -0
  479. package/src/runtime/context/automatic-learning.ts +565 -0
  480. package/src/runtime/context/codegraph-cache-boundary.ts +255 -0
  481. package/src/runtime/context/codegraph-read-provider.ts +55 -22
  482. package/src/runtime/context/codegraph-sidecar.cjs +8 -1
  483. package/src/runtime/context/cognitive-attention.ts +173 -0
  484. package/src/runtime/context/cognitive-skill-canary.ts +158 -0
  485. package/src/runtime/context/context-closure.ts +305 -0
  486. package/src/runtime/context/context-pack-process.ts +103 -0
  487. package/src/runtime/context/context-plane.ts +203 -0
  488. package/src/runtime/context/direct-controller-learning.ts +298 -0
  489. package/src/runtime/context/project-engineering-contract.ts +60 -0
  490. package/src/runtime/context/semantic-navigation-contract.ts +9 -0
  491. package/src/runtime/context/semantic-navigation.ts +6 -34
  492. package/src/runtime/context/typescript-navigation-process.ts +145 -0
  493. package/src/runtime/context/typescript-navigation.ts +89 -3
  494. package/src/runtime/control-plane/bootstrap/capability-provider.ts +74 -0
  495. package/src/runtime/control-plane/bootstrap/control-api.ts +57 -0
  496. package/src/runtime/control-plane/bootstrap/index.ts +5 -0
  497. package/src/runtime/control-plane/bootstrap/state-machine.ts +141 -0
  498. package/src/runtime/control-plane/bootstrap/store.ts +21 -0
  499. package/src/runtime/control-plane/bootstrap/types.ts +87 -0
  500. package/src/runtime/control-plane/cleanup-artifact-retention.ts +531 -0
  501. package/src/runtime/control-plane/codegraph-cache-retention.ts +164 -0
  502. package/src/runtime/control-plane/concurrency/work-execution-concurrency.ts +282 -0
  503. package/src/runtime/control-plane/execution/controller-authority-recovery.ts +380 -0
  504. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +113 -16
  505. package/src/runtime/control-plane/execution/execution-identity.ts +23 -3
  506. package/src/runtime/control-plane/execution/implementation-review-content.ts +95 -0
  507. package/src/runtime/control-plane/execution/repository-work-attribution.ts +7 -30
  508. package/src/runtime/control-plane/execution/retained-work-resume.ts +439 -102
  509. package/src/runtime/control-plane/execution/session-store.ts +31 -1
  510. package/src/runtime/control-plane/execution/validation.ts +34 -11
  511. package/src/runtime/control-plane/execution/work-authority-reconciler.ts +87 -0
  512. package/src/runtime/control-plane/execution/work-check-evidence.ts +137 -0
  513. package/src/runtime/control-plane/execution/work-completion-authority.ts +31 -44
  514. package/src/runtime/control-plane/execution/work-delivery-service.ts +497 -0
  515. package/src/runtime/control-plane/execution/work-evidence-policy.ts +46 -53
  516. package/src/runtime/control-plane/execution/work-execution-support.ts +85 -164
  517. package/src/runtime/control-plane/execution/work-handle-authority.ts +349 -73
  518. package/src/runtime/control-plane/execution/work-handle-store.ts +55 -2
  519. package/src/runtime/control-plane/execution/work-head-settlement.ts +108 -0
  520. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +173 -28
  521. package/src/runtime/control-plane/execution/work-operation-service.ts +92 -40
  522. package/src/runtime/control-plane/execution/work-preparation-service.ts +145 -269
  523. package/src/runtime/control-plane/execution/work-remote-delivery.ts +2 -3
  524. package/src/runtime/control-plane/execution/work-revision-diff.ts +39 -0
  525. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +543 -225
  526. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +59 -45
  527. package/src/runtime/control-plane/execution/work-verification-context.ts +1 -1
  528. package/src/runtime/control-plane/execution/work-verification-service.ts +662 -52
  529. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +205 -16
  530. package/src/runtime/control-plane/facade/capability-registry.ts +380 -44
  531. package/src/runtime/control-plane/facade/check-normalization.ts +3 -9
  532. package/src/runtime/control-plane/facade/codex-delegation.ts +10 -11
  533. package/src/runtime/control-plane/facade/controller-connection.ts +135 -0
  534. package/src/runtime/control-plane/facade/controller-session-store.ts +2 -488
  535. package/src/runtime/control-plane/facade/goal-workloop-access.ts +6 -22
  536. package/src/runtime/control-plane/facade/goal-workloop.ts +565 -1202
  537. package/src/runtime/control-plane/facade/handoff-inbox-application.ts +145 -0
  538. package/src/runtime/control-plane/facade/handoff-inbox-store.ts +266 -52
  539. package/src/runtime/control-plane/facade/index.ts +4 -3
  540. package/src/runtime/control-plane/facade/operation-digest.ts +34 -73
  541. package/src/runtime/control-plane/facade/plan-contract-store.ts +671 -245
  542. package/src/runtime/control-plane/facade/policy-gate.ts +0 -23
  543. package/src/runtime/control-plane/facade/repository-work-admission.ts +31 -23
  544. package/src/runtime/control-plane/facade/requirement-authority.ts +121 -0
  545. package/src/runtime/control-plane/facade/requirement-board.ts +9 -4
  546. package/src/runtime/control-plane/facade/rh-work-operation-contract.ts +85 -0
  547. package/src/runtime/control-plane/facade/self-healing-loop.ts +18 -53
  548. package/src/runtime/control-plane/facade/semantic-admission.ts +8 -16
  549. package/src/runtime/control-plane/facade/suggested-actions.ts +15 -15
  550. package/src/runtime/control-plane/facade/types.ts +218 -620
  551. package/src/runtime/control-plane/facade/work-continuation.ts +8 -8
  552. package/src/runtime/control-plane/facade/work-contract-store.ts +2 -1043
  553. package/src/runtime/control-plane/generated-cache-retention.ts +336 -19
  554. package/src/runtime/control-plane/global-scheduler/autonomous-continuation.ts +248 -0
  555. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +13 -2
  556. package/src/runtime/control-plane/global-scheduler/maintenance.ts +274 -84
  557. package/src/runtime/control-plane/global-scheduler/periodic-cleanup-entry.ts +55 -0
  558. package/src/runtime/control-plane/global-scheduler/periodic-cleanup-process.ts +98 -0
  559. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -2
  560. package/src/runtime/control-plane/global-scheduler/scheduler-worker-entry.ts +10 -0
  561. package/src/runtime/control-plane/global-scheduler/scheduler.ts +227 -109
  562. package/src/runtime/control-plane/global-scheduler/wake-signal.ts +42 -2
  563. package/src/runtime/control-plane/global-scheduler/worker-launch.ts +18 -6
  564. package/src/runtime/control-plane/governance/access-policy.ts +19 -2
  565. package/src/runtime/control-plane/governance/authorization.ts +20 -3
  566. package/src/runtime/control-plane/launcher/chatgpt-round-continuation.ts +160 -0
  567. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +2 -177
  568. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +139 -670
  569. package/src/runtime/control-plane/launcher/local-conversation-controller.ts +238 -0
  570. package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +25 -6
  571. package/src/runtime/control-plane/launcher/thin-launcher.ts +96 -194
  572. package/src/runtime/control-plane/lifecycle-retention-metrics.ts +60 -0
  573. package/src/runtime/control-plane/persistence/cognition-store.ts +820 -0
  574. package/src/runtime/control-plane/persistence/experience-store.ts +302 -0
  575. package/src/runtime/control-plane/persistence/operational-prior-store.ts +348 -0
  576. package/src/runtime/control-plane/persistence/requirement-store.ts +230 -80
  577. package/src/runtime/control-plane/persistence/sqlite-store.ts +654 -78
  578. package/src/runtime/control-plane/persistence/workflow-content-store.ts +98 -0
  579. package/src/runtime/control-plane/persistence/workflow-registry-store.ts +186 -0
  580. package/src/runtime/control-plane/persistence/workflow-run-store.ts +123 -0
  581. package/src/runtime/control-plane/release-retention.ts +154 -26
  582. package/src/runtime/control-plane/repository-namespace-retention.ts +208 -0
  583. package/src/runtime/control-plane/runtime-cleanup.ts +844 -38
  584. package/src/runtime/control-plane/runtime-generation.ts +6 -0
  585. package/src/runtime/control-plane/workspace/project-onboarding.ts +80 -0
  586. package/src/runtime/control-plane/workspace/semantic-sync-service.ts +479 -0
  587. package/src/runtime/control-plane/workspace/workspace-store.ts +249 -0
  588. package/src/runtime/diagnostics/incident-repair.ts +573 -0
  589. package/src/runtime/diagnostics/mcp-timing.ts +239 -6
  590. package/src/runtime/diagnostics/performance.ts +6 -14
  591. package/src/runtime/diagnostics/read-only-tool.ts +1 -1
  592. package/src/runtime/evidence/artifact-store.ts +216 -35
  593. package/src/runtime/evidence/evidence-store.ts +112 -8
  594. package/src/runtime/evidence/operational-shadow.ts +38 -0
  595. package/src/runtime/evidence/output-handle-index.ts +225 -0
  596. package/src/runtime/evidence/process-check-receipt.ts +5 -43
  597. package/src/runtime/evidence/result-store.ts +158 -44
  598. package/src/runtime/evidence/sensitive-output.ts +4 -4
  599. package/src/runtime/execution/jobs/receipt-recovery.ts +1 -1
  600. package/src/runtime/execution/jobs/store.ts +124 -6
  601. package/src/runtime/execution/managed-workspace.ts +112 -5
  602. package/src/runtime/execution/process-runtime/canary.ts +19 -5
  603. package/src/runtime/execution/process-runtime/check-classification.ts +48 -0
  604. package/src/runtime/execution/process-runtime/check-facade.ts +24 -10
  605. package/src/runtime/execution/process-runtime/check-receipt.ts +1 -1
  606. package/src/runtime/execution/process-runtime/check-result-retention.ts +132 -0
  607. package/src/runtime/execution/process-runtime/check-result.ts +177 -2
  608. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +58 -9
  609. package/src/runtime/execution/process-runtime/check-storage.ts +187 -0
  610. package/src/runtime/execution/process-runtime/command-facade.ts +117 -114
  611. package/src/runtime/execution/process-runtime/gc.ts +15 -2
  612. package/src/runtime/execution/process-runtime/handle-index.ts +176 -0
  613. package/src/runtime/execution/process-runtime/host-command-lane.ts +139 -0
  614. package/src/runtime/execution/process-runtime/index.ts +4 -0
  615. package/src/runtime/execution/process-runtime/interactive-admission.ts +2 -1
  616. package/src/runtime/execution/process-runtime/lightweight-managed.ts +283 -73
  617. package/src/runtime/execution/process-runtime/persisted-check.ts +130 -23
  618. package/src/runtime/execution/process-runtime/process-runner-entry.ts +47 -5
  619. package/src/runtime/execution/process-runtime/process-scope.ts +160 -0
  620. package/src/runtime/execution/process-runtime/resource-claims.ts +125 -64
  621. package/src/runtime/execution/process-runtime/runtime.ts +167 -17
  622. package/src/runtime/execution/process-runtime/store.ts +30 -2
  623. package/src/runtime/execution/process-runtime/types.ts +27 -0
  624. package/src/runtime/execution/workers/executor.ts +18 -19
  625. package/src/runtime/execution/workers/worker-entry.ts +1 -1
  626. package/src/runtime/gateway/mcp/execution-tools.ts +2 -207
  627. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +2 -61
  628. package/src/runtime/gateway/mcp/persisted-check-process.ts +2 -3
  629. package/src/runtime/gateway/mcp/process-tools.ts +2 -289
  630. package/src/runtime/gateway/mcp/router.ts +2 -1125
  631. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +2 -565
  632. package/src/runtime/gateway/mcp/runtime-tools.ts +2 -6655
  633. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +2 -1
  634. package/src/runtime/maintenance/cleanup.ts +38 -24
  635. package/src/runtime/platform/browser-product-discovery.ts +174 -0
  636. package/src/runtime/platform/computer-platform.ts +14 -0
  637. package/src/runtime/platform/executable-discovery.ts +54 -0
  638. package/src/runtime/platform/fetch-http-server.ts +90 -0
  639. package/src/runtime/platform/service-inventory.ts +130 -0
  640. package/src/runtime/platform/service-manager.ts +146 -0
  641. package/src/runtime/platform/windows-host-discovery.ts +118 -0
  642. package/src/runtime/platform/windows-recovery-deployment.ts +67 -0
  643. package/src/runtime/plugins/action-application.ts +192 -0
  644. package/src/runtime/plugins/app-store-connect-adapter.ts +47 -35
  645. package/src/runtime/plugins/browser-adapter.ts +898 -135
  646. package/src/runtime/plugins/browser-automation-service.ts +33 -34
  647. package/src/runtime/plugins/browser-macos-bridge.ts +386 -46
  648. package/src/runtime/plugins/browser-manifest-surface.ts +75 -17
  649. package/src/runtime/plugins/browser-native-session-service.ts +64 -14
  650. package/src/runtime/plugins/browser-provider-registry.ts +2 -109
  651. package/src/runtime/plugins/browser-registration.ts +32 -0
  652. package/src/runtime/plugins/browser-runtime-contract.ts +2 -154
  653. package/src/runtime/plugins/browser-runtime.ts +55 -10
  654. package/src/runtime/plugins/browser-session-authority.ts +101 -291
  655. package/src/runtime/plugins/browser-session-legacy-migration.ts +203 -0
  656. package/src/runtime/plugins/browser-session-store.ts +243 -80
  657. package/src/runtime/plugins/browser-session-types.ts +2 -98
  658. package/src/runtime/plugins/capability-authorization-grants.ts +272 -59
  659. package/src/runtime/plugins/catalog-trust.ts +82 -0
  660. package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +219 -0
  661. package/src/runtime/plugins/computer-chatgpt-macos-target.ts +891 -0
  662. package/src/runtime/plugins/computer-registration.ts +1050 -0
  663. package/src/runtime/plugins/config-store.ts +119 -0
  664. package/src/runtime/plugins/desktop-operator-external-policy.ts +964 -0
  665. package/src/runtime/plugins/desktop-operator-registration.ts +123 -12
  666. package/src/runtime/plugins/errors.ts +8 -1
  667. package/src/runtime/plugins/external-adapter.ts +82 -323
  668. package/src/runtime/plugins/external-provider-policy.ts +48 -0
  669. package/src/runtime/plugins/external-registration.ts +30 -2
  670. package/src/runtime/plugins/external-unix-socket-probe.cjs +9 -3
  671. package/src/runtime/plugins/external-unix-socket.ts +64 -193
  672. package/src/runtime/plugins/first-party-external-registration.ts +163 -0
  673. package/src/runtime/plugins/first-party-registry.ts +3 -1
  674. package/src/runtime/plugins/github-adapter.ts +1 -2
  675. package/src/runtime/plugins/gmail-adapter.ts +29 -8
  676. package/src/runtime/plugins/google-calendar-adapter.ts +8 -8
  677. package/src/runtime/plugins/google-shared.ts +45 -53
  678. package/src/runtime/plugins/google-tasks-adapter.ts +8 -8
  679. package/src/runtime/plugins/ios-adapter.ts +2 -3
  680. package/src/runtime/plugins/lightweight-action.ts +208 -2
  681. package/src/runtime/plugins/local-system-adapter.ts +192 -2
  682. package/src/runtime/plugins/macos-capability-broker.ts +15 -105
  683. package/src/runtime/plugins/managed-process-adapter.ts +12 -5
  684. package/src/runtime/plugins/plugin-action-sidecar.ts +17 -13
  685. package/src/runtime/plugins/plugin-management-adapter.ts +1 -1
  686. package/src/runtime/plugins/resend-adapter.ts +15 -24
  687. package/src/runtime/plugins/store.ts +1003 -387
  688. package/src/runtime/plugins/types.ts +28 -1
  689. package/src/runtime/plugins/uu-remote-rescue-registration.ts +155 -0
  690. package/src/runtime/plugins/windows-host-recovery-install.ts +68 -0
  691. package/src/runtime/plugins/windows-host-recovery-registration.ts +85 -0
  692. package/src/runtime/plugins/xiaohongshu-publish.ts +102 -430
  693. package/src/runtime/projections/controller-context.ts +12 -6
  694. package/src/runtime/projections/git-status-sampler.ts +73 -22
  695. package/src/runtime/projections/invalidation.ts +7 -3
  696. package/src/runtime/projections/materialized-view.ts +93 -43
  697. package/src/runtime/recovery/actions.ts +3 -3
  698. package/src/runtime/recovery/capability-action-application.ts +229 -0
  699. package/src/runtime/recovery/index.ts +3 -0
  700. package/src/runtime/recovery/lifecycle-inventory.ts +338 -0
  701. package/src/runtime/recovery/local-jobs-repair.ts +14 -14
  702. package/src/runtime/recovery/maintenance-executor.ts +701 -168
  703. package/src/runtime/recovery/quarantine-retention.ts +256 -0
  704. package/src/runtime/recovery/store.ts +29 -11
  705. package/src/runtime/release/identity.ts +6 -0
  706. package/src/runtime/release/release-coordinator.ts +296 -0
  707. package/src/runtime/release/release-session.ts +716 -0
  708. package/src/runtime/release/release-state-migration.ts +25 -0
  709. package/src/runtime/resources/leases/store.ts +56 -25
  710. package/src/runtime/root/active-execution-power-assertion.ts +139 -0
  711. package/src/runtime/root/assistant-learning-loop.ts +45 -0
  712. package/src/runtime/root/browser-session-composition.ts +59 -0
  713. package/src/runtime/root/computer-composition.ts +376 -0
  714. package/src/runtime/root/computer-target-composition.ts +16 -0
  715. package/src/runtime/root/computer-target-persistence.ts +39 -0
  716. package/src/runtime/root/controller-progression-composition.ts +192 -0
  717. package/src/runtime/root/controller-round-composition.ts +126 -0
  718. package/src/runtime/root/controller-services.ts +2 -0
  719. package/src/runtime/root/deployment-topology.ts +106 -0
  720. package/src/runtime/root/entry.ts +8 -6
  721. package/src/runtime/root/gateway-adapter.ts +3 -3
  722. package/src/runtime/root/known-good-recovery.ts +184 -0
  723. package/src/runtime/root/local-bridge.ts +5 -3
  724. package/src/runtime/root/mcp-transport.ts +116 -23
  725. package/src/runtime/root/ownership.ts +261 -9
  726. package/src/runtime/root/package-connector-service.ts +212 -74
  727. package/src/runtime/root/package-runtime-release.ts +199 -33
  728. package/src/runtime/root/package-runtime-service.ts +77 -108
  729. package/src/runtime/root/release-execution-canary.ts +103 -0
  730. package/src/runtime/root/release-loader.ts +150 -0
  731. package/src/runtime/root/release-manifest.ts +185 -7
  732. package/src/runtime/root/release-materialize.ts +474 -31
  733. package/src/runtime/root/release-store.ts +425 -37
  734. package/src/runtime/root/runtime-lane.ts +191 -0
  735. package/src/runtime/root/runtime.ts +379 -32
  736. package/src/runtime/root/scheduled-controller-composition.ts +76 -0
  737. package/src/runtime/root/scheduler.ts +12 -4
  738. package/src/runtime/root/service-runner.ts +3 -3
  739. package/src/runtime/root/service.ts +73 -45
  740. package/src/runtime/root/status.ts +49 -0
  741. package/src/runtime/root/types.ts +55 -3
  742. package/src/runtime/root/workflow-supervisor-composition.ts +493 -0
  743. package/src/runtime/root/workflow-supervisor-runtime.ts +209 -0
  744. package/src/runtime/root/write-fence.ts +50 -2
  745. package/src/runtime/safe-tooling/google-credential-store.ts +76 -21
  746. package/src/runtime/safe-tooling/google-oauth-broker.ts +25 -7
  747. package/src/runtime/safe-tooling/workspace-auth.ts +1 -1
  748. package/src/runtime/shared/bounded-child-supervisor.ts +9 -1
  749. package/src/runtime/shared/local-bridge-surface.ts +1 -1
  750. package/src/runtime/shared/process-environment.ts +36 -10
  751. package/src/runtime/shared/response-budget.ts +1 -3
  752. package/src/runtime/shared/storage-capacity.ts +68 -0
  753. package/src/runtime/standalone-recovery/controller-home-migration.ts +1108 -0
  754. package/src/runtime/standalone-recovery/core.ts +4609 -495
  755. package/src/runtime/standalone-recovery/entry.ts +642 -81
  756. package/src/runtime/standalone-recovery/independent-host-rescue.ts +172 -0
  757. package/src/runtime/standalone-recovery/installer.ts +399 -177
  758. package/src/runtime/standalone-recovery/mcp-server.ts +79 -0
  759. package/src/runtime/standalone-recovery/mutation-identity-contract.ts +21 -0
  760. package/src/runtime/standalone-recovery/operation-lock.ts +160 -0
  761. package/src/runtime/standalone-recovery/performance.ts +173 -0
  762. package/src/runtime/standalone-recovery/release.ts +58 -5
  763. package/src/runtime/standalone-recovery/service-labels.ts +5 -2
  764. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +5 -2
  765. package/src/runtime/watchdog/workflow-watchdog.ts +1 -1
  766. package/src/runtime/workflow/schedules/browser-probe.ts +17 -6
  767. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +11 -4
  768. package/src/runtime/workflow/schedules/engine.ts +298 -484
  769. package/src/runtime/workflow/schedules/pending-activation.ts +1 -1
  770. package/src/runtime/workflow/schedules/settlement.ts +2 -191
  771. package/src/runtime/workflow/schedules/store.ts +2 -607
  772. package/src/runtime/workflow/schedules/types.ts +2 -127
  773. package/src/runtime/workflow/schedules/work-continuation.ts +220 -16
  774. package/src/runtime/workflows/first-party/xiaohongshu.ts +122 -0
  775. package/src/runtime/workflows/runtime.ts +482 -0
  776. package/src/testing/test-governance.ts +95 -30
  777. package/supervisor/chatgpt-conversation.ts +23 -0
  778. package/supervisor/client.ts +201 -0
  779. package/supervisor/control-plane.ts +868 -0
  780. package/supervisor/entry.ts +15 -0
  781. package/supervisor/forge-validators.ts +80 -0
  782. package/supervisor/index.ts +5 -0
  783. package/supervisor/native-browser-adapter.ts +786 -0
  784. package/supervisor/native-messaging/host.ts +39 -0
  785. package/supervisor/native-messaging/manifest.ts +26 -0
  786. package/supervisor/native-messaging/protocol.ts +27 -0
  787. package/supervisor/paths.ts +18 -0
  788. package/supervisor/protocol.ts +202 -0
  789. package/supervisor/server.ts +404 -0
  790. package/supervisor/store.ts +1451 -0
  791. package/supervisor/tsconfig.json +8 -0
  792. package/supervisor/types.ts +255 -0
  793. package/assets/templates/helpers/archive-architecture-request.sh +0 -244
  794. package/docs/architecture/history/README.md +0 -7
  795. package/docs/architecture/history/chatgpt-handoff-facade.md +0 -397
  796. package/docs/architecture/history/global-hook-runtime.md +0 -166
  797. package/docs/architecture/history/ios-semantic-automation-provider-v2.md +0 -170
  798. package/docs/architecture/requests/archive/2026/20260528-154740-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
  799. package/docs/architecture/requests/archive/2026/20260528-154816-assets-hooks-assets-hooks-hook-input-sh.md +0 -59
  800. package/docs/architecture/requests/archive/2026/20260528-161851-ai-harness-workflow-contract-json-ai-harness-workflow-contract-json.md +0 -59
  801. package/docs/architecture/requests/archive/2026/20260528-161853-scripts-check-task-workflow-sh-scripts-check-task-workflow-sh.md +0 -59
  802. package/docs/architecture/requests/archive/2026/20260528-161911-assets-workflow-contract-v1-json-assets-workflow-contract-v1-json.md +0 -59
  803. package/docs/architecture/requests/archive/2026/20260528-222416-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
  804. package/docs/architecture/requests/archive/2026/20260528-222444-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
  805. package/docs/architecture/requests/archive/2026/20260528-222457-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
  806. package/docs/architecture/requests/archive/2026/20260528-222648-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
  807. package/docs/architecture/requests/archive/2026/20260528-222705-ai-hooks-ai-hooks-worktree-guard-sh.md +0 -59
  808. package/docs/architecture/requests/archive/2026/20260528-222726-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
  809. package/docs/architecture/requests/archive/2026/20260528-222727-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
  810. package/docs/architecture/requests/archive/2026/20260528-224736-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
  811. package/docs/architecture/requests/archive/2026/20260528-233259-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
  812. package/docs/architecture/requests/archive/2026/20260528-233335-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
  813. package/docs/architecture/requests/archive/2026/20260528-233456-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
  814. package/docs/architecture/requests/archive/2026/20260528-233457-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
  815. package/docs/architecture/requests/archive/2026/20260528-233637-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
  816. package/docs/architecture/requests/archive/2026/20260528-233638-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
  817. package/docs/architecture/requests/archive/2026/20260529-000322-root-package-json.md +0 -59
  818. package/docs/architecture/requests/archive/2026/20260529-004238-ai-hooks-ai-hooks-lib-workflow-state-sh.md +0 -59
  819. package/docs/architecture/requests/archive/2026/20260529-020654-ai-harness-policy-json-ai-harness-policy-json.md +0 -63
  820. package/docs/architecture/requests/archive/2026/20260529-094236-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -60
  821. package/docs/architecture/requests/archive/2026/20260529-094323-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -60
  822. package/docs/architecture/requests/archive/2026/20260529-094331-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -60
  823. package/docs/architecture/requests/archive/2026/20260529-094339-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -60
  824. package/docs/architecture/requests/archive/2026/20260529-094430-assets-hooks-assets-hooks-session-start-context-sh.md +0 -60
  825. package/docs/architecture/requests/archive/2026/20260529-094446-ai-hooks-ai-hooks-session-start-context-sh.md +0 -60
  826. package/docs/architecture/requests/archive/2026/20260613-002040-runtime-harness-hook-adapters.md +0 -68
  827. package/docs/architecture/requests/archive/2026/root.md +0 -68
  828. package/docs/architecture/requests/archive/2026/runtime-harness-hook-adapters.md +0 -76
  829. package/docs/architecture/requests/archive/2026/verification-evals-checks.md +0 -68
  830. package/docs/architecture/requests/archive/2026/workflow-engine-contract-assets.md +0 -69
  831. package/scripts/archive-architecture-request.sh +0 -4
  832. package/scripts/benchmark-background-check-overlap.ts +0 -228
  833. package/scripts/benchmark-controller-hotpath.ts +0 -212
  834. package/scripts/benchmark-controller-round2.ts +0 -139
  835. package/scripts/benchmark-ios-agent-device.ts +0 -514
  836. package/scripts/benchmark-mcp-host.sh +0 -117
  837. package/scripts/benchmark-mcp-transport.ts +0 -170
  838. package/scripts/benchmark-quality-harness.ts +0 -146
  839. package/scripts/benchmark-route-session-concurrency.ts +0 -915
  840. package/scripts/benchmark-thin-harness-gateway-ab.ts +0 -632
  841. package/scripts/benchmark-thin-harness-mcp-e2e.ts +0 -498
  842. package/scripts/benchmark-thin-harness.ts +0 -251
  843. package/scripts/benchmark-typescript-navigation.ts +0 -186
  844. package/scripts/benchmark-work-efficiency.ts +0 -334
  845. package/scripts/build-controller-ui.ts +0 -28
  846. package/scripts/check-npm-release.sh +0 -32
  847. package/scripts/check-public-release-surface.sh +0 -19
  848. package/scripts/check-release-published.sh +0 -57
  849. package/scripts/check-release-readiness.sh +0 -37
  850. package/scripts/check-release-version.mjs +0 -74
  851. package/scripts/check-tarball-install-smoke.sh +0 -67
  852. package/scripts/package-source-archive.sh +0 -147
  853. package/scripts/public-release-files.txt +0 -39
  854. package/scripts/public-release.ts +0 -174
  855. package/scripts/publish-release-tarball.sh +0 -27
  856. package/scripts/route-nl-vs-ts-eval.ts +0 -679
  857. package/scripts/run-skill-evals.ts +0 -1022
  858. package/scripts/stage-runtime-release.ts +0 -39
  859. package/src/cli/controller/progress.ts +0 -656
  860. package/src/cli/controller/work-mode.ts +0 -181
  861. package/src/cli/local-bridge/console-assets.ts +0 -17
  862. package/src/cli/local-bridge/dashboard.ts +0 -17
  863. package/src/cli/local-bridge/frontend/src/App.tsx +0 -10
  864. package/src/cli/local-bridge/frontend/src/components/Button.tsx +0 -1
  865. package/src/cli/local-bridge/frontend/src/components/DetailPane.tsx +0 -3
  866. package/src/cli/local-bridge/frontend/src/components/SearchField.tsx +0 -1
  867. package/src/cli/local-bridge/frontend/src/components/SectionHeader.tsx +0 -1
  868. package/src/cli/local-bridge/frontend/src/components/Segmented.tsx +0 -1
  869. package/src/cli/local-bridge/frontend/src/components/StatusText.tsx +0 -3
  870. package/src/cli/local-bridge/frontend/src/design/icons.tsx +0 -11
  871. package/src/cli/local-bridge/frontend/src/design/tokens.css +0 -8
  872. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +0 -153
  873. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +0 -37
  874. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +0 -183
  875. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +0 -2
  876. package/src/cli/local-bridge/frontend/src/features/system/SystemView.tsx +0 -2
  877. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +0 -4
  878. package/src/cli/local-bridge/frontend/src/lib/api.ts +0 -30
  879. package/src/cli/local-bridge/frontend/src/lib/format.ts +0 -4
  880. package/src/cli/local-bridge/frontend/src/lib/routes.ts +0 -7
  881. package/src/cli/local-bridge/frontend/src/main.tsx +0 -1
  882. package/src/cli/local-bridge/frontend/src/shell/AppShell.tsx +0 -1
  883. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +0 -2
  884. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +0 -4
  885. package/src/cli/local-bridge/frontend/src/styles.css +0 -28
  886. package/src/cli/local-bridge/frontend/src/types.ts +0 -15
  887. package/src/cli/local-bridge/frontend/src/vite-env.d.ts +0 -1
  888. package/src/cli/local-bridge/frontend/vite.config.ts +0 -27
  889. package/src/cli/local-bridge/ui-dist/app.css +0 -1
  890. package/src/cli/local-bridge/ui-dist/app.js +0 -11228
  891. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +0 -385
  892. package/src/runtime/control-plane/execution/work-finalization-service.ts +0 -1913
  893. package/src/runtime/control-plane/execution/work-path-scope.ts +0 -37
  894. package/src/runtime/control-plane/execution/work-task-receipt.ts +0 -449
  895. package/src/runtime/control-plane/facade/controller-round-relay.ts +0 -1118
  896. package/src/runtime/control-plane/facade/work-admission-policy.ts +0 -93
  897. package/src/runtime/control-plane/facade/work-task-projection.ts +0 -66
  898. package/src/runtime/control-plane/routing/index.ts +0 -1
  899. package/src/runtime/control-plane/routing/route-policy.ts +0 -382
  900. package/src/runtime/control-plane/routing/workspace-admission.ts +0 -70
@@ -1,30 +1,33 @@
1
- import { createHash } from 'crypto';
1
+ import { createHash, randomUUID } from 'crypto';
2
2
  import { join } from 'path';
3
3
  import type { RepositoryRecord } from '../../cli/repositories/types';
4
- import { CONTROLLER_SCOPE_REPO_ID, controllerSystemRoot, ensureControllerHome, repositoryControllerRoot } from '../../cli/repositories/controller-home';
5
- import { existsSync, mkdirSync, readdirSync, rmSync } from 'fs';
4
+ import { CONTROLLER_SCOPE_REPO_ID, FORGE_INSTANCE_SCOPE_KEY, controllerSystemRoot, ensureControllerHome, repositoryControllerRoot } from '../../cli/repositories/controller-home';
5
+ import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, rmSync } from 'fs';
6
6
  import type { ExecutionJob, ResourceClaimSpec } from '../execution/jobs/types';
7
7
  import { appendRuntimeEvent } from '../evidence/event-ledger';
8
8
  import { readJsonFile, sanitizeFileComponent, writeJsonAtomic } from '../shared/json-files';
9
9
  import { createFirstPartyPluginAdapterMap } from './first-party-registry';
10
+ import { reconcileFirstPartyExternalPluginRegistration, reconcileFirstPartyExternalPluginRegistrations } from './first-party-external-registration';
11
+ import { REPOSITORY_PLUGIN_CONFIG_IDS, repositoryPluginConfigFileName, repositoryPluginConfigPath } from './config-store';
10
12
  import { getExternalPluginAdapter, listExternalPluginAdapters } from './external-adapter';
11
- import { AssistantPluginError, toAssistantPluginError } from './errors';
13
+ import { AssistantPluginError, isAssistantPluginError, toAssistantPluginError, type AssistantPluginEffectOutcome } from './errors';
12
14
  import {
15
+ findActivePluginCapabilityAuthorizationById,
13
16
  findActivePluginCapabilityAuthorization,
14
17
  pluginCapabilityAuthorizationOwnerScope,
15
18
  recordPluginCapabilityAuthorization,
16
19
  type PluginCapabilityAuthorizationGrant,
17
20
  } from './capability-authorization-grants';
18
21
  import { markControllerContextProjectionDirty } from '../projections/controller-context';
19
- import { classifyRepositoryCommand } from '../../cli/repositories/command-classifier';
22
+ import { acquireExecutionLeases, releaseExactExecutionLeases } from '../resources/leases/store';
23
+ import { isRuntimeWriteFenceError } from '../root/write-fence';
20
24
  import {
21
- acceptSubmittedWorkContract,
22
25
  appendWorkEvidence,
23
26
  getWorkContract,
24
27
  recordWorkCompletionReceipt,
25
28
  updateWorkContract,
26
- } from '../control-plane/facade/work-contract-store';
27
- import { isTerminalWorkContractStatus, type LocalEffectCompletionReceipt, type RemoteEffectCompletionReceipt, type WorkContract, type WorkRisk } from '../control-plane/facade/types';
29
+ } from '../../../packages/kernel/work/api/index';
30
+ import { isTerminalSemanticWorkState, type RemoteEffectCompletionReceipt, type WorkContract } from '../control-plane/facade/types';
28
31
  import type {
29
32
  AssistantPluginAdapter,
30
33
  AssistantPluginActionDescriptor,
@@ -43,8 +46,10 @@ function resolvePluginAdapter(controllerHome: string, pluginId: string): Assista
43
46
  }
44
47
 
45
48
  function listPluginAdapters(controllerHome: string): AssistantPluginAdapter[] {
46
- const external = listExternalPluginAdapters(controllerHome).filter((adapter) => !PLUGIN_ADAPTERS.has(adapter.pluginId));
47
- return [...PLUGIN_ADAPTERS.values(), ...external];
49
+ const firstParty = [...PLUGIN_ADAPTERS.values()].filter((adapter) => adapter.exposure !== 'internal');
50
+ const external = listExternalPluginAdapters(controllerHome)
51
+ .filter((adapter) => !PLUGIN_ADAPTERS.has(adapter.pluginId) && adapter.exposure !== 'internal');
52
+ return [...firstParty, ...external];
48
53
  }
49
54
 
50
55
  export function assistantPluginScope(pluginId: string, controllerHome?: string): AssistantPluginAdapter['scope'] | undefined {
@@ -67,40 +72,200 @@ function now(): string {
67
72
  return new Date().toISOString();
68
73
  }
69
74
 
70
- 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);
71
144
  const root = controllerSystemRoot(controllerHome);
72
- 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);
73
152
  return {
153
+ ...built,
154
+ revision: previous ? (changed ? previous.revision + 1 : previous.revision) : 1,
155
+ updatedAt: changed ? now() : previous?.updatedAt ?? built.updatedAt,
156
+ };
157
+ }
158
+
159
+ export function readControllerStoredPluginManifest(controllerHome: string, pluginId: string): AssistantPluginManifest | undefined {
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();
172
+ }
173
+
174
+ export function listControllerPluginManifests(
175
+ controllerHome: string,
176
+ options: ListAssistantPluginManifestsOptions = {},
177
+ ): AssistantPluginManifest[] {
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)));
210
+ }
211
+
212
+ export function getControllerPluginManifest(controllerHome: string, pluginId: string): AssistantPluginManifest {
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 = {
74
220
  schemaVersion: 1,
75
- repoId: CONTROLLER_SCOPE_REPO_ID,
76
- displayName: 'Controller local system',
77
- canonicalRoot: root,
78
- localRoot: root,
79
- activeCheckoutId: 'controller',
80
- checkouts: [],
81
- defaultBranch: 'none',
82
- repositoryType: 'local-git',
83
- enabled: true,
84
- createdAt: timestamp,
85
- updatedAt: timestamp,
86
- lastSeenAt: timestamp,
87
- configurationPath: join(root, 'local-system', 'targets.json'),
88
- stateStorageStrategy: 'controller-home',
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)),
89
226
  };
227
+ writeJsonAtomic(controllerIndexPath(controllerHome), index);
228
+ return index;
90
229
  }
91
230
 
92
- function adapterMatchesRepository(adapter: AssistantPluginAdapter, repository: RepositoryRecord): boolean {
93
- const scope = adapter.scope ?? 'repository';
94
- if (scope === 'controller_with_repository_overlay') return true;
95
- return repository.repoId === CONTROLLER_SCOPE_REPO_ID ? scope === 'controller' : scope === 'repository';
231
+ export function syncControllerPluginRegistry(controllerHome: string) {
232
+ reconcileFirstPartyExternalPluginRegistrations(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) };
237
+ }
238
+
239
+ export function syncControllerPluginManifest(controllerHome: string, pluginId: string) {
240
+ reconcileFirstPartyExternalPluginRegistration(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) };
251
+ }
252
+
253
+ export function removeControllerPluginManifestProjection(controllerHome: string, pluginId: string) {
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;
96
260
  }
97
261
 
98
262
  function cloneCacheValue<T>(value: T): T {
99
263
  return structuredClone(value);
100
264
  }
101
265
 
102
- function listCacheKey(controllerHome: string, repoId: string, preferStored: boolean): string {
103
- return `${controllerHome}::${repoId}::list::${preferStored ? 'stored' : 'live'}`;
266
+ function listCacheKey(controllerHome: string, repoId: string, preferStored: boolean, fallbackToLive: boolean): string {
267
+ const mode = preferStored ? (fallbackToLive ? 'stored-with-live-fallback' : 'stored-only') : 'live';
268
+ return `${controllerHome}::${repoId}::list::${mode}`;
104
269
  }
105
270
 
106
271
  function itemCacheKey(controllerHome: string, repoId: string, pluginId: string, preferStored: boolean): string {
@@ -184,7 +349,13 @@ export function clearAssistantPluginManifestCacheForTest(): void {
184
349
  }
185
350
 
186
351
  function pluginsRoot(controllerHome: string, repoId: string): string {
187
- 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');
188
359
  }
189
360
 
190
361
  function manifestPath(controllerHome: string, repoId: string, pluginId: string): string {
@@ -229,13 +400,28 @@ function pluginIndexEntry(controllerHome: string, repoId: string, manifest: Assi
229
400
  }
230
401
 
231
402
  function readStoredManifest(controllerHome: string, repoId: string, pluginId: string): AssistantPluginManifest | undefined {
403
+ const canonicalPath = manifestPath(controllerHome, repoId, pluginId);
232
404
  try {
233
- 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;
234
411
  } catch {
235
412
  return undefined;
236
413
  }
237
414
  }
238
415
 
416
+ /** Read-only persisted plugin projection. This never probes or rebuilds a provider manifest. */
417
+ export function readStoredAssistantPluginManifest(
418
+ controllerHome: string,
419
+ repository: RepositoryRecord,
420
+ pluginId: string,
421
+ ): AssistantPluginManifest | undefined {
422
+ return readStoredManifest(controllerHome, repository.repoId, pluginId);
423
+ }
424
+
239
425
  function cachedManifestForRepository(
240
426
  controllerHome: string,
241
427
  repoId: string,
@@ -248,12 +434,48 @@ function cachedManifestForRepository(
248
434
  ) ?? readPluginManifestCache(pluginManifestItemCache, itemCacheKey(controllerHome, repoId, pluginId, false));
249
435
  }
250
436
 
437
+ export function migrateRepositoryPluginConfigLegacyFiles(
438
+ controllerHome: string,
439
+ repository: Pick<RepositoryRecord, 'repoId' | 'canonicalRoot'>,
440
+ ): string {
441
+ const targetRoot = join(repositoryControllerRoot(controllerHome, repository.repoId), 'plugins', 'config');
442
+ const legacyRoots = [
443
+ join(repository.canonicalRoot, '.forge', 'plugins'),
444
+ join(repository.canonicalRoot, '.repo-harness', 'plugins'),
445
+ ];
446
+ mkdirSync(targetRoot, { recursive: true });
447
+ for (const legacyRoot of legacyRoots) {
448
+ if (!existsSync(legacyRoot)) continue;
449
+ const stat = lstatSync(legacyRoot);
450
+ // Historical compatibility symlinks may point at another Controller Home.
451
+ // Never traverse them; unlink only the retired repository-local compatibility entry.
452
+ if (stat.isSymbolicLink()) {
453
+ rmSync(legacyRoot, { force: true });
454
+ continue;
455
+ }
456
+ if (!stat.isDirectory()) throw new Error(`PLUGIN_CONFIG_LEGACY_PATH_INVALID: ${legacyRoot}`);
457
+ for (const pluginId of REPOSITORY_PLUGIN_CONFIG_IDS) {
458
+ const fileName = repositoryPluginConfigFileName(pluginId);
459
+ const source = join(legacyRoot, fileName);
460
+ if (!existsSync(source)) continue;
461
+ const sourceStat = lstatSync(source);
462
+ if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) continue;
463
+ const target = repositoryPluginConfigPath({ controllerHome, repoId: repository.repoId }, pluginId);
464
+ if (!existsSync(target)) cpSync(source, target, { force: false, errorOnExist: false });
465
+ rmSync(source, { force: true });
466
+ }
467
+ if (readdirSync(legacyRoot).length === 0) rmSync(legacyRoot, { recursive: true, force: true });
468
+ }
469
+ return targetRoot;
470
+ }
471
+
251
472
  function computeManifest(
252
473
  controllerHome: string,
253
474
  repository: RepositoryRecord,
254
475
  pluginId: string,
255
476
  resolvedAdapter?: AssistantPluginAdapter,
256
477
  ): AssistantPluginManifest {
478
+ migrateRepositoryPluginConfigLegacyFiles(controllerHome, repository);
257
479
  const adapter = resolvedAdapter ?? resolvePluginAdapter(controllerHome, pluginId);
258
480
  if (!adapter || !adapterMatchesRepository(adapter, repository)) throw new Error(`PLUGIN_NOT_FOUND: ${pluginId}`);
259
481
  const previous = readStoredManifest(controllerHome, repository.repoId, pluginId);
@@ -261,7 +483,7 @@ function computeManifest(
261
483
  controllerHome,
262
484
  repoId: repository.repoId,
263
485
  repoRoot: repository.canonicalRoot,
264
- controllerScoped: repository.repoId === CONTROLLER_SCOPE_REPO_ID,
486
+ controllerScoped: false,
265
487
  });
266
488
  const changed = !previous || fingerprintManifest(previous) !== fingerprintManifest(built);
267
489
  return {
@@ -283,33 +505,126 @@ function writeRegistry(controllerHome: string, repoId: string, manifests: Assist
283
505
  return index;
284
506
  }
285
507
 
286
- export function claimsForAssistantPluginAction(
508
+ function claimsForPluginActionScope(
287
509
  action: AssistantPluginActionDescriptor,
288
- repository: RepositoryRecord,
510
+ scope: PluginActionScopeContext,
289
511
  trustedPluginId: string,
290
512
  ): ResourceClaimSpec[] {
291
- const controllerScoped = repository.repoId === CONTROLLER_SCOPE_REPO_ID;
292
513
  const pluginId = trustedPluginId.trim().replace(/[^a-zA-Z0-9._-]+/g, '-');
293
514
  if (!pluginId) throw new Error('PLUGIN_ID_REQUIRED_FOR_RESOURCE_CLAIMS');
294
515
  return action.resourceClaims.map((claim) => ({
295
516
  resourceKey: claim.resource === 'remote' || claim.resource === 'provider-state'
296
- // The manifest/plugin id was resolved from the trusted registration; a
297
- // provider never supplies arbitrary resource-key text.
298
517
  ? `provider-state:${pluginId}`
299
- : controllerScoped
518
+ : scope.kind === 'controller'
300
519
  ? `controller-system:${claim.resource}`
301
520
  : claim.resource === 'workspace'
302
- ? `workspace:${repository.activeCheckoutId}`
521
+ ? `workspace:${scope.checkoutId}`
303
522
  : claim.resource === 'git-refs'
304
- ? `git-refs:${repository.repoId}`
305
- : `repo-state:${repository.repoId}`,
523
+ ? `git-refs:${scope.scopeKey}`
524
+ : `repo-state:${scope.scopeKey}`,
306
525
  mode: claim.mode,
307
526
  }));
308
527
  }
309
528
 
310
- function semanticKey(repository: RepositoryRecord, pluginId: string, actionId: string, args: Record<string, unknown>): string {
529
+ export function claimsForAssistantPluginAction(
530
+ action: AssistantPluginActionDescriptor,
531
+ repository: RepositoryRecord,
532
+ trustedPluginId: string,
533
+ ): ResourceClaimSpec[] {
534
+ return claimsForPluginActionScope(action, repositoryPluginActionScope(repository), trustedPluginId);
535
+ }
536
+
537
+ async function withAssistantPluginResourceLeases<T>(
538
+ controllerHome: string,
539
+ scope: PluginActionScopeContext,
540
+ action: AssistantPluginActionDescriptor,
541
+ request: AssistantPluginActionRequest,
542
+ operation: () => Promise<T>,
543
+ ): Promise<T> {
544
+ const claims = claimsForPluginActionScope(action, scope, request.pluginId).map((claim) => ({
545
+ ...claim,
546
+ repoId: scope.scopeKey,
547
+ ...(scope.checkoutId ? { checkoutId: scope.checkoutId } : {}),
548
+ }));
549
+ if (claims.length === 0) return operation();
550
+
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()}`;
557
+ const timeoutMs = Math.max(5_000, Math.min(10 * 60_000, request.timeoutMs ?? action.defaultTimeoutMs));
558
+ const acquisition = acquireExecutionLeases(controllerHome, scope.scopeKey, ownerJobId, claims, {
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',
566
+ ownerIdentity: {
567
+ repositoryId: scope.scopeKey,
568
+ checkoutId: scope.checkoutId ?? CONTROLLER_PLUGIN_SCOPE_KEY,
569
+ worktreeId: scope.checkoutId ?? CONTROLLER_PLUGIN_SCOPE_KEY,
570
+ branch: 'workflow-plugin-action',
571
+ principalId: request.origin.actor?.trim() || 'workflow-plugin-action',
572
+ controllerInstanceId: process.env.FORGE_RUNTIME_INSTANCE_ID?.trim()
573
+ || process.env.FORGE_WRITER_INSTANCE_ID?.trim()
574
+ || process.env.FORGE_DAEMON_INSTANCE_ID?.trim()
575
+ || `process:${process.pid}`,
576
+ controllerGeneration: process.env.FORGE_RUNTIME_INCARNATION_GENERATION?.trim()
577
+ || process.env.FORGE_WRITER_GENERATION?.trim()
578
+ || process.env.FORGE_ACTIVE_RUNTIME_REVISION?.trim()
579
+ || 'unbound',
580
+ },
581
+ });
582
+ if (!acquisition.acquired) {
583
+ throw new AssistantPluginError(
584
+ 'PLUGIN_RESOURCE_CONTENTION',
585
+ `${request.pluginId}/${request.actionId} could not acquire its declared resource claims before dispatch.`,
586
+ { retryable: true, details: { blockers: acquisition.blockers } },
587
+ );
588
+ }
589
+ const expected = acquisition.leases.map((lease) => ({
590
+ leaseId: lease.leaseId,
591
+ fencingToken: lease.fencingToken,
592
+ repoId: lease.repoId,
593
+ checkoutId: lease.checkoutId,
594
+ resourceKey: lease.resourceKey,
595
+ ownerIdentityDigest: lease.ownerIdentityDigest,
596
+ }));
597
+ let retainForReconciliation = false;
598
+ try {
599
+ const result = await operation();
600
+ const nestedResult = result && typeof result === 'object' && !Array.isArray(result)
601
+ ? (result as Record<string, unknown>).result
602
+ : undefined;
603
+ retainForReconciliation = Boolean(nestedResult && typeof nestedResult === 'object' && !Array.isArray(nestedResult)
604
+ && (nestedResult as Record<string, unknown>).outcome === 'outcome_unknown');
605
+ return result;
606
+ } catch (error) {
607
+ retainForReconciliation = isAssistantPluginError(error) && error.effectOutcome === 'outcome_unknown';
608
+ throw error;
609
+ } finally {
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
+ }
622
+ }
623
+ }
624
+
625
+ function semanticKey(scope: PluginActionScopeContext, pluginId: string, actionId: string, args: Record<string, unknown>): string {
311
626
  const digest = createHash('sha256').update(JSON.stringify(canonical(args))).digest('hex').slice(0, 20);
312
- return `plugin-action:${repository.repoId}:${pluginId}:${actionId}:${digest}`;
627
+ return `plugin-action:${scope.scopeKey}:${pluginId}:${actionId}:${digest}`;
313
628
  }
314
629
 
315
630
  function validatePrimitive(type: string, value: unknown): boolean {
@@ -408,6 +723,92 @@ function originMayEstablishCapabilityAuthorization(origin: AssistantPluginAction
408
723
  return INTERACTIVE_PLUGIN_AUTHORIZATION_SURFACES.has(origin.surface);
409
724
  }
410
725
 
726
+ function requiresAutomatedWriteAuthorization(origin: AssistantPluginActionExecutionInput['origin']): boolean {
727
+ return ['schedule', 'reconciliation', 'system'].includes(origin.surface);
728
+ }
729
+
730
+ const AUTHORIZATION_RISK_RANK: Record<AssistantPluginActionDescriptor['risk'], number> = {
731
+ readonly: 0,
732
+ workspace_write: 1,
733
+ remote_write: 2,
734
+ destructive: 3,
735
+ };
736
+
737
+ function authorizationTargetMatches(
738
+ left: PluginCapabilityAuthorizationGrant['target'],
739
+ right: PluginCapabilityAuthorizationGrant['target'],
740
+ ): boolean {
741
+ return left.kind === right.kind
742
+ && left.id === right.id
743
+ && (left.identityFingerprint ?? '') === (right.identityFingerprint ?? '');
744
+ }
745
+
746
+ async function resolveAutomatedWriteAuthorization(input: {
747
+ controllerHome: string;
748
+ scope: PluginActionScopeContext;
749
+ adapter: AssistantPluginAdapter;
750
+ manifest: AssistantPluginManifest;
751
+ action: AssistantPluginActionDescriptor;
752
+ args: Record<string, unknown>;
753
+ origin: AssistantPluginActionExecutionInput['origin'];
754
+ requestId: string;
755
+ jobId?: string;
756
+ authorizationGrantRefs?: readonly string[];
757
+ }): Promise<PluginCapabilityAuthorizationGrant | undefined> {
758
+ if (input.action.readOnly) return undefined;
759
+ if (input.action.confirmation === 'strong_confirmation') {
760
+ throw new AssistantPluginError(
761
+ 'EXTERNAL_EFFECT_AUTHORIZATION_REQUIRED',
762
+ `${input.manifest.pluginId}/${input.action.actionId} requires an interactive strong confirmation.`,
763
+ { retryable: false },
764
+ );
765
+ }
766
+ const refs = [...new Set((input.authorizationGrantRefs ?? []).map((ref) => ref.trim()).filter(Boolean))];
767
+ if (refs.length === 0 || !input.adapter.resolveAuthorizationContext) {
768
+ throw new AssistantPluginError(
769
+ 'EXTERNAL_EFFECT_AUTHORIZATION_REQUIRED',
770
+ `${input.manifest.pluginId}/${input.action.actionId} requires an active Workflow capability grant.`,
771
+ { retryable: false },
772
+ );
773
+ }
774
+ const targetContext = await input.adapter.resolveAuthorizationContext({
775
+ controllerHome: input.controllerHome,
776
+ ...pluginActionExecutionFields(input.scope),
777
+ pluginId: input.manifest.pluginId,
778
+ actionId: input.action.actionId,
779
+ requestId: input.requestId,
780
+ args: input.args,
781
+ origin: input.origin,
782
+ jobId: input.jobId,
783
+ authorizationGrantRefs: refs,
784
+ });
785
+ if (!targetContext) {
786
+ throw new AssistantPluginError(
787
+ 'PLUGIN_CAPABILITY_GRANT_TARGET_UNAVAILABLE',
788
+ `${input.manifest.pluginId}/${input.action.actionId} did not resolve an exact authorization target.`,
789
+ { retryable: false },
790
+ );
791
+ }
792
+ const capabilityId = reusableCapabilityId(input.manifest, input.action);
793
+ const grant = refs
794
+ .map((ref) => findActivePluginCapabilityAuthorizationById(input.controllerHome, ref))
795
+ .find((candidate) => candidate
796
+ && candidate.repoId === input.scope.grantRepoId
797
+ && candidate.pluginId === input.manifest.pluginId
798
+ && candidate.capabilityId === capabilityId
799
+ && authorizationTargetMatches(candidate.target, targetContext.target)
800
+ && input.action.scopes.every((scope) => candidate.scopes.includes(scope))
801
+ && AUTHORIZATION_RISK_RANK[candidate.riskCeiling] >= AUTHORIZATION_RISK_RANK[input.action.risk]);
802
+ if (!grant) {
803
+ throw new AssistantPluginError(
804
+ 'PLUGIN_CAPABILITY_GRANT_NOT_FOUND',
805
+ `${input.manifest.pluginId}/${input.action.actionId} has no active grant for this exact target.`,
806
+ { retryable: false },
807
+ );
808
+ }
809
+ return grant;
810
+ }
811
+
411
812
  function authorizationTargetSummary(context: AssistantPluginAuthorizationContext): { kind: string; id: string } {
412
813
  return { kind: context.target.kind, id: context.target.id };
413
814
  }
@@ -429,19 +830,15 @@ function initialAuthorizationEvidence(
429
830
  return { source: 'host_permission_model', reusable: false, capabilityId };
430
831
  }
431
832
 
432
- function denyAutomatedWrite(manifest: AssistantPluginManifest, action: AssistantPluginActionDescriptor, origin: AssistantPluginActionExecutionInput['origin']): void {
433
- if (!['schedule', 'reconciliation', 'system'].includes(origin.surface)) return;
434
- if (action.readOnly) return;
435
- throw new Error(`EXTERNAL_EFFECT_AUTHORIZATION_REQUIRED: ${manifest.pluginId}/${action.actionId} cannot run from ${origin.surface}`);
436
- }
437
-
438
833
  export type ListAssistantPluginManifestsOptions = {
439
834
  /**
440
835
  * Prefer previously persisted manifests for connector/status hot paths.
441
836
  * Live adapter rebuild (including host probes such as Xcode) runs only when
442
- * no stored manifest exists, or when forceRefresh is true.
837
+ * no stored manifest exists, unless fallbackToLive=false, or when forceRefresh is true.
443
838
  */
444
839
  preferStored?: boolean;
840
+ /** Materialized projections set this false so a cold summary never synchronously probes providers. */
841
+ fallbackToLive?: boolean;
445
842
  forceRefresh?: boolean;
446
843
  };
447
844
 
@@ -458,7 +855,8 @@ export function listAssistantPluginManifests(
458
855
  options: ListAssistantPluginManifestsOptions = {},
459
856
  ): AssistantPluginManifest[] {
460
857
  const preferStored = options.preferStored === true && options.forceRefresh !== true;
461
- const cacheKey = listCacheKey(controllerHome, repository.repoId, preferStored);
858
+ const fallbackToLive = options.fallbackToLive !== false || !preferStored;
859
+ const cacheKey = listCacheKey(controllerHome, repository.repoId, preferStored, fallbackToLive);
462
860
  if (options.forceRefresh !== true) {
463
861
  // Stored manifests are persisted snapshots. Their freshness is governed by
464
862
  // explicit registry/config mutation invalidation, not a wall-clock trust
@@ -475,9 +873,11 @@ export function listAssistantPluginManifests(
475
873
  if (preferStored) {
476
874
  const stored = readStoredManifest(controllerHome, repository.repoId, pluginId);
477
875
  if (stored) return stored;
876
+ if (!fallbackToLive) return undefined;
478
877
  }
479
878
  return computeManifest(controllerHome, repository, pluginId);
480
879
  })
880
+ .filter((manifest): manifest is AssistantPluginManifest => Boolean(manifest))
481
881
  .sort((left, right) => left.pluginId.localeCompare(right.pluginId));
482
882
  primePluginManifestItemCache(controllerHome, repository.repoId, manifests, preferStored);
483
883
  return writePluginManifestCache(pluginManifestListCache, cacheKey, manifests);
@@ -533,7 +933,7 @@ export function syncAssistantPluginRegistry(
533
933
  };
534
934
  }
535
935
 
536
- function syncAssistantPluginManifest(
936
+ export function syncAssistantPluginManifest(
537
937
  controllerHome: string,
538
938
  repository: RepositoryRecord,
539
939
  pluginId: string,
@@ -558,19 +958,144 @@ function syncAssistantPluginManifest(
558
958
  };
559
959
  }
560
960
 
961
+ /** Remove one persisted plugin projection without probing or rebuilding unrelated providers. */
962
+ export function removeAssistantPluginManifestProjection(
963
+ controllerHome: string,
964
+ repository: RepositoryRecord,
965
+ pluginId: string,
966
+ ): AssistantPluginRegistryIndex {
967
+ invalidateAssistantPluginManifestCache(controllerHome, repository.repoId, pluginId);
968
+ rmSync(manifestPath(controllerHome, repository.repoId, pluginId), { force: true });
969
+ const current = readJsonFile<AssistantPluginRegistryIndex>(indexPath(controllerHome, repository.repoId), {
970
+ schemaVersion: 1,
971
+ updatedAt: now(),
972
+ plugins: [],
973
+ });
974
+ const next: AssistantPluginRegistryIndex = {
975
+ ...current,
976
+ updatedAt: now(),
977
+ plugins: current.plugins.filter((entry) => entry.pluginId !== pluginId),
978
+ };
979
+ writeJsonAtomic(indexPath(controllerHome, repository.repoId), next);
980
+ markControllerContextProjectionDirty(repository.canonicalRoot, `plugin:${pluginId}:removed`);
981
+ return next;
982
+ }
983
+
561
984
  export function isDirectPluginReadAction(action: AssistantPluginActionDescriptor): boolean {
562
- return action.readOnly === true
985
+ return action.executionMode !== 'direct_non_persistent'
986
+ && action.readOnly === true
563
987
  && action.risk === 'readonly'
564
988
  && action.confirmation === 'none'
565
989
  && action.idempotent === true;
566
990
  }
567
991
 
568
- export async function executeAssistantPluginReadDirect(
992
+ export function isDirectNonPersistentPluginAction(action: AssistantPluginActionDescriptor): boolean {
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');
1013
+ }
1014
+
1015
+ async function executePluginDirectNonPersistentInScope(
569
1016
  controllerHome: string,
570
- repository: RepositoryRecord,
1017
+ scope: PluginActionScopeContext,
571
1018
  request: AssistantPluginActionRequest,
572
1019
  ): Promise<{ manifest: AssistantPluginManifest; action: AssistantPluginActionDescriptor; result: Record<string, unknown> }> {
573
- const manifest = getAssistantPluginManifest(controllerHome, repository, request.pluginId);
1020
+ const adapter = resolvePluginAdapter(controllerHome, request.pluginId);
1021
+ if (!adapter) throw new Error(`PLUGIN_NOT_FOUND: ${request.pluginId}`);
1022
+ if (PLUGIN_ADAPTERS.get(request.pluginId) !== adapter) {
1023
+ throw new Error(`PLUGIN_DIRECT_NON_PERSISTENT_FIRST_PARTY_REQUIRED: ${request.pluginId}/${request.actionId}`);
1024
+ }
1025
+ const manifestLookup = scope.kind === 'controller'
1026
+ ? getControllerPluginManifestForExecution(controllerHome, request.pluginId, adapter)
1027
+ : getAssistantPluginManifestForExecution(controllerHome, scope.repository!, request.pluginId, adapter);
1028
+ const manifest = manifestLookup.manifest;
1029
+ const action = actionForManifest(manifest, request.actionId);
1030
+ if (!manifest.enabled && action.actionId !== 'configure') {
1031
+ throw new Error(`PLUGIN_DISABLED: ${request.pluginId} is disabled`);
1032
+ }
1033
+ if (action.executionMode !== 'direct_non_persistent') {
1034
+ throw new Error(`PLUGIN_DIRECT_NON_PERSISTENT_NOT_ALLOWED: ${request.pluginId}/${request.actionId}`);
1035
+ }
1036
+ if (!isDirectNonPersistentPluginAction(action)) {
1037
+ throw new Error(`PLUGIN_DIRECT_NON_PERSISTENT_CONTRACT_INVALID: ${request.pluginId}/${request.actionId}`);
1038
+ }
1039
+ if (request.workId || request.workRepoId) {
1040
+ throw new Error(`PLUGIN_DIRECT_NON_PERSISTENT_WORK_ATTRIBUTION_FORBIDDEN: ${request.pluginId}/${request.actionId}`);
1041
+ }
1042
+ if (action.confirmation === 'authorization' && request.confirmAuthorization !== true) {
1043
+ throw new Error(`PLUGIN_DIRECT_NON_PERSISTENT_AUTHORIZATION_REQUIRED: ${request.pluginId}/${request.actionId} requires confirm_authorization=true for this invocation`);
1044
+ }
1045
+ const normalizedArgs = validateActionArguments(action, request.args ?? {});
1046
+ enforceConfirmation(action, { ...request, args: normalizedArgs });
1047
+ if (requiresAutomatedWriteAuthorization(request.origin)) {
1048
+ await resolveAutomatedWriteAuthorization({
1049
+ controllerHome,
1050
+ scope,
1051
+ adapter,
1052
+ manifest,
1053
+ action,
1054
+ args: normalizedArgs,
1055
+ origin: request.origin,
1056
+ requestId: request.requestId,
1057
+ authorizationGrantRefs: request.authorizationGrantRefs,
1058
+ });
1059
+ }
1060
+ const result = await adapter.executeAction({
1061
+ controllerHome,
1062
+ ...pluginActionExecutionFields(scope),
1063
+ pluginId: request.pluginId,
1064
+ actionId: request.actionId,
1065
+ requestId: request.requestId,
1066
+ args: normalizedArgs,
1067
+ origin: request.origin,
1068
+ timeoutMs: request.timeoutMs,
1069
+ signal: request.signal,
1070
+ authorizationGrantRefs: request.authorizationGrantRefs,
1071
+ providerIdentityPrevalidated: manifestLookup.providerIdentityPrevalidated,
1072
+ });
1073
+ return { manifest, action, result };
1074
+ }
1075
+
1076
+ export function executeAssistantPluginDirectNonPersistent(
1077
+ controllerHome: string,
1078
+ repository: RepositoryRecord,
1079
+ request: AssistantPluginActionRequest,
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);
574
1099
  const action = actionForManifest(manifest, request.actionId);
575
1100
  if (!manifest.enabled && action.actionId !== 'configure') {
576
1101
  throw new Error(`PLUGIN_DISABLED: ${request.pluginId} is disabled`);
@@ -580,53 +1105,110 @@ export async function executeAssistantPluginReadDirect(
580
1105
  }
581
1106
  const normalizedArgs = validateActionArguments(action, request.args ?? {});
582
1107
  enforceConfirmation(action, { ...request, args: normalizedArgs });
583
- const result = await executeAssistantPluginAction({
1108
+ const result = await executePluginActionInScope(scope, {
584
1109
  controllerHome,
585
- repoId: repository.repoId,
586
- repoRoot: repository.canonicalRoot,
1110
+ ...pluginActionExecutionFields(scope),
587
1111
  pluginId: request.pluginId,
588
1112
  actionId: request.actionId,
589
1113
  requestId: request.requestId,
590
1114
  args: normalizedArgs,
591
1115
  origin: request.origin,
592
1116
  });
593
- 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);
594
1160
  }
595
1161
 
596
1162
  export interface PluginActionReceipt {
597
1163
  schemaVersion: 1;
598
1164
  receiptId: string;
599
1165
  requestId: string;
600
- 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;
601
1170
  /** Work authority repository when it differs from controller/provider receipt scope. */
602
1171
  workRepoId?: string;
603
1172
  pluginId: string;
604
1173
  actionId: string;
605
1174
  semanticKey: string;
606
1175
  status: 'succeeded' | 'failed';
1176
+ /** Durable semantic disposition for an attempted external/local effect. */
1177
+ effectOutcome?: AssistantPluginEffectOutcome;
607
1178
  createdAt: string;
608
1179
  workId?: string;
609
1180
  origin?: { surface: string; actor?: string; correlationId?: string };
610
1181
  authorization?: PluginActionAuthorizationEvidence;
611
1182
  result?: Record<string, unknown>;
612
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;
613
1188
  }
614
1189
 
615
1190
  interface PluginActionRequestIndex {
616
1191
  requestId: string;
617
- repoId: string;
1192
+ scopeKey: string;
1193
+ repoId?: string;
618
1194
  workRepoId?: string;
619
1195
  receiptId: string;
620
1196
  semanticKey: string;
621
1197
  createdAt: string;
622
1198
  }
623
1199
 
624
- function pluginActionReceiptRoot(controllerHome: string, repoId: string): string {
625
- 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');
626
1204
  mkdirSync(root, { recursive: true });
627
1205
  return root;
628
1206
  }
629
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
+
630
1212
  function pluginActionRequestPath(controllerHome: string, requestId: string): string {
631
1213
  const hash = createHash('sha256').update(requestId).digest('hex');
632
1214
  const root = join(ensureControllerHome(controllerHome), 'indexes', 'plugin-actions', 'requests');
@@ -634,14 +1216,30 @@ function pluginActionRequestPath(controllerHome: string, requestId: string): str
634
1216
  return join(root, `${hash}.json`);
635
1217
  }
636
1218
 
637
- function pluginActionReceiptPath(controllerHome: string, repoId: string, receiptId: string): string {
638
- 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`);
639
1221
  }
640
1222
 
641
- function readPluginActionReceipt(controllerHome: string, repoId: string, receiptId: string): PluginActionReceipt | undefined {
642
- const path = pluginActionReceiptPath(controllerHome, repoId, receiptId);
643
- if (!existsSync(path)) return undefined;
644
- 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;
645
1243
  }
646
1244
 
647
1245
  export function findPluginActionReceipt(
@@ -649,6 +1247,12 @@ export function findPluginActionReceipt(
649
1247
  receiptId: string,
650
1248
  ): PluginActionReceipt | undefined {
651
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;
652
1256
  const repositoriesRoot = join(home, 'repositories');
653
1257
  try {
654
1258
  for (const repoId of readdirSync(repositoriesRoot)) {
@@ -661,6 +1265,19 @@ export function findPluginActionReceipt(
661
1265
  return undefined;
662
1266
  }
663
1267
 
1268
+ /** Exact request lookup for subordinate Workflow recovery; never dispatches an action. */
1269
+ export function readPluginActionReceiptForRequest(controllerHome: string, requestId: string): PluginActionReceipt | undefined {
1270
+ const path = pluginActionRequestPath(controllerHome, requestId);
1271
+ if (!existsSync(path)) return undefined;
1272
+ const index = readJsonFile<PluginActionRequestIndex>(path);
1273
+ if (index.requestId !== requestId) throw new Error('PLUGIN_RECEIPT_REQUEST_MISMATCH');
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);
1277
+ if (!receipt || receipt.requestId !== requestId || receipt.semanticKey !== index.semanticKey) throw new Error('PLUGIN_RECEIPT_LOST');
1278
+ return receipt;
1279
+ }
1280
+
664
1281
  export function compatibilityPluginJobFromReceipt(
665
1282
  receipt: PluginActionReceipt,
666
1283
  checkoutId = 'unknown',
@@ -668,86 +1285,20 @@ export function compatibilityPluginJobFromReceipt(
668
1285
  return compatibilityJobFromReceipt(receipt, checkoutId);
669
1286
  }
670
1287
 
671
- const LOCAL_SYSTEM_MUTATION_ACTIONS = new Set([
672
- 'authorize_target',
673
- 'revoke_target',
674
- 'create_directory',
675
- 'write_text',
676
- 'delete_file',
677
- 'delete_empty_directory',
678
- 'initialize_git',
679
- 'execute_project_script',
680
- 'copy_file',
681
- 'move_file',
682
- 'rename_file',
683
- 'open_application',
684
- 'reveal_in_finder',
685
- 'open_file',
686
- ]);
687
-
688
- function localSystemActionRequiresWork(
689
- repository: RepositoryRecord,
690
- pluginId: string,
691
- actionId: string,
692
- args: Record<string, unknown>,
693
- ): boolean {
694
- if (repository.repoId !== CONTROLLER_SCOPE_REPO_ID || pluginId !== 'local_system') return false;
695
- if (LOCAL_SYSTEM_MUTATION_ACTIONS.has(actionId)) return true;
696
- if (actionId !== 'execute_command' || !Array.isArray(args.command)) return false;
697
- return classifyRepositoryCommand(args.command.map(String)).risk !== 'readonly';
698
- }
699
-
700
- function workRiskForPluginAction(action: AssistantPluginActionDescriptor): WorkRisk {
701
- if (action.risk === 'readonly') return 'readonly';
702
- if (action.risk === 'workspace_write') return 'medium';
703
- if (action.risk === 'remote_write') return 'high';
704
- return 'destructive';
705
- }
706
-
707
- function localEffectTarget(
708
- args: Record<string, unknown>,
709
- result: Record<string, unknown>,
710
- ): LocalEffectCompletionReceipt['target'] {
711
- const inner = result.result && typeof result.result === 'object' && !Array.isArray(result.result)
712
- ? result.result as Record<string, unknown>
713
- : result;
714
- const target = inner.target && typeof inner.target === 'object' && !Array.isArray(inner.target)
715
- ? inner.target as Record<string, unknown>
716
- : undefined;
717
- const id = String(
718
- target?.workspaceId
719
- ?? inner.workspaceId
720
- ?? args.target_key
721
- ?? args.destination_target_key
722
- ?? args.source_target_key
723
- ?? 'controller-local',
724
- );
725
- const identityFingerprint = typeof target?.identityFingerprint === 'string'
726
- ? target.identityFingerprint
727
- : typeof inner.identityFingerprint === 'string'
728
- ? inner.identityFingerprint
729
- : undefined;
730
- return {
731
- kind: id.startsWith('workspace_') || args.target_key || args.destination_target_key || args.source_target_key
732
- ? 'workspace_target'
733
- : 'controller_local',
734
- id,
735
- ...(identityFingerprint ? { identityFingerprint } : {}),
736
- };
737
- }
738
-
739
- function workAttributionRepoId(repository: RepositoryRecord, request: AssistantPluginActionRequest): string {
1288
+ function workAttributionRepoId(scope: PluginActionScopeContext, request: AssistantPluginActionRequest): string {
740
1289
  const explicit = request.workRepoId?.trim();
741
1290
  if (!request.workId?.trim()) {
742
1291
  if (explicit) throw new Error(`WORK_PLUGIN_ATTRIBUTION_REPO_WITHOUT_WORK: ${explicit}`);
743
- return repository.repoId;
1292
+ return scope.repository?.repoId ?? '';
744
1293
  }
745
- 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`);
746
1297
  }
747
1298
 
748
1299
  function remoteEffectWorkForPluginAction(
749
1300
  controllerHome: string,
750
- repository: RepositoryRecord,
1301
+ scope: PluginActionScopeContext,
751
1302
  action: AssistantPluginActionDescriptor,
752
1303
  request: AssistantPluginActionRequest,
753
1304
  receiptId?: string,
@@ -757,7 +1308,7 @@ function remoteEffectWorkForPluginAction(
757
1308
  if (action.risk !== 'remote_write') {
758
1309
  throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_REQUIRES_REMOTE_WRITE: ${action.actionId}`);
759
1310
  }
760
- const workRepoId = workAttributionRepoId(repository, request);
1311
+ const workRepoId = workAttributionRepoId(scope, request);
761
1312
  const work = getWorkContract({ controllerHome, repoId: workRepoId }, workId);
762
1313
  if (!work) throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_NOT_FOUND: ${workId}`);
763
1314
  if (work.workKind !== 'remote_effect') {
@@ -766,49 +1317,49 @@ function remoteEffectWorkForPluginAction(
766
1317
  if (work.checks.length > 0) {
767
1318
  throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_CHECKS_PRESENT: ${workId} declares repository checks and cannot be completed by a plugin receipt alone`);
768
1319
  }
769
- if (isTerminalWorkContractStatus(work.status)) {
1320
+ if (isTerminalSemanticWorkState(work.semanticState)) {
770
1321
  const receiptAlreadyBound = Boolean(receiptId) && (
771
1322
  work.completionReceipt?.receiptId === receiptId
772
1323
  || work.evidenceRefs.some((evidence) => evidence.evidenceId === receiptId)
773
1324
  );
774
- if (work.status === 'completed' && receiptAlreadyBound) return work;
775
- 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}`);
776
1327
  }
777
1328
  return work;
778
1329
  }
779
1330
 
780
1331
  function attributedWorkForPluginAction(
781
1332
  controllerHome: string,
782
- repository: RepositoryRecord,
1333
+ scope: PluginActionScopeContext,
783
1334
  action: AssistantPluginActionDescriptor,
784
1335
  request: AssistantPluginActionRequest,
785
1336
  ): WorkContract | undefined {
786
1337
  const workId = request.workId?.trim();
787
1338
  if (!workId || action.risk === 'remote_write') return undefined;
788
- const workRepoId = workAttributionRepoId(repository, request);
1339
+ const workRepoId = workAttributionRepoId(scope, request);
789
1340
  const work = getWorkContract({ controllerHome, repoId: workRepoId }, workId);
790
1341
  if (!work) throw new Error(`WORK_PLUGIN_ATTRIBUTION_NOT_FOUND: ${workRepoId}:${workId}`);
791
- if (isTerminalWorkContractStatus(work.status)) {
792
- 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}`);
793
1344
  }
794
1345
  return work;
795
1346
  }
796
1347
 
797
1348
  function bindLocalEffectReceiptToAttributedWork(
798
1349
  controllerHome: string,
799
- repository: RepositoryRecord,
1350
+ scope: PluginActionScopeContext,
800
1351
  action: AssistantPluginActionDescriptor,
801
1352
  request: AssistantPluginActionRequest,
802
1353
  receipt: PluginActionReceipt,
803
1354
  work?: WorkContract,
804
1355
  ): WorkContract | undefined {
805
1356
  if (!request.workId || receipt.status !== 'succeeded') return work;
806
- const workRepoId = workAttributionRepoId(repository, request);
1357
+ const workRepoId = workAttributionRepoId(scope, request);
807
1358
  const attributed = work ?? getWorkContract({ controllerHome, repoId: workRepoId }, request.workId.trim());
808
1359
  if (!attributed) throw new Error(`WORK_PLUGIN_ATTRIBUTION_NOT_FOUND: ${workRepoId}:${request.workId.trim()}`);
809
1360
  if (attributed.workKind !== 'local_effect' || action.readOnly || action.risk === 'readonly') return attributed;
810
1361
  if (attributed.evidenceRefs.some((evidence) => evidence.evidenceId === receipt.receiptId)) return attributed;
811
- if (isTerminalWorkContractStatus(attributed.status)) return attributed;
1362
+ if (isTerminalSemanticWorkState(attributed.semanticState)) return attributed;
812
1363
  return appendWorkEvidence({ controllerHome, repoId: workRepoId }, attributed.workId, {
813
1364
  evidenceId: receipt.receiptId,
814
1365
  title: 'typed local plugin effect completed',
@@ -817,7 +1368,7 @@ function bindLocalEffectReceiptToAttributedWork(
817
1368
  });
818
1369
  }
819
1370
 
820
- export function finalizeRemoteEffectWorkFromActionReceipt(
1371
+ export function recordRemoteEffectWorkActionReceipt(
821
1372
  controllerHome: string,
822
1373
  repoId: string,
823
1374
  workId: string,
@@ -827,14 +1378,12 @@ export function finalizeRemoteEffectWorkFromActionReceipt(
827
1378
  if (work.workKind !== 'remote_effect') {
828
1379
  throw new Error(`WORK_REMOTE_EFFECT_FINALIZE_KIND_MISMATCH: ${workId} is ${work.workKind}, expected remote_effect`);
829
1380
  }
830
- if (work.status === 'completed' && work.completionReceipt?.source === 'remote_effect') return work;
831
- if (isTerminalWorkContractStatus(work.status)) {
832
- throw new Error(`WORK_REMOTE_EFFECT_FINALIZE_TERMINAL: ${workId} is ${work.status}`);
833
- }
1381
+ if (work.completionReceipt?.source === 'remote_effect') return work;
834
1382
  const receipt = work.evidenceRefs
835
1383
  .map((evidence) => {
836
1384
  if (!evidence.evidenceId) return undefined;
837
1385
  return readPluginActionReceipt(controllerHome, repoId, evidence.evidenceId)
1386
+ ?? readPluginActionReceipt(controllerHome, CONTROLLER_PLUGIN_SCOPE_KEY, evidence.evidenceId)
838
1387
  ?? readPluginActionReceipt(controllerHome, CONTROLLER_SCOPE_REPO_ID, evidence.evidenceId);
839
1388
  })
840
1389
  .find((candidate) => candidate?.workId === workId && candidate.status === 'succeeded');
@@ -864,17 +1413,17 @@ export function finalizeRemoteEffectWorkFromActionReceipt(
864
1413
 
865
1414
  function bindRemoteEffectReceiptToWork(
866
1415
  controllerHome: string,
867
- repository: RepositoryRecord,
1416
+ scope: PluginActionScopeContext,
868
1417
  action: AssistantPluginActionDescriptor,
869
1418
  request: AssistantPluginActionRequest,
870
1419
  receipt: PluginActionReceipt,
871
1420
  ): WorkContract | undefined {
872
1421
  if (!request.workId) return undefined;
873
- if (receipt.status !== 'succeeded') return remoteEffectWorkForPluginAction(controllerHome, repository, action, request);
874
- 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);
875
1424
  if (!work) return undefined;
876
- if (work.status === 'completed') return work;
877
- const workRepoId = workAttributionRepoId(repository, request);
1425
+ if (work.completionReceipt?.source === 'remote_effect') return work;
1426
+ const workRepoId = workAttributionRepoId(scope, request);
878
1427
  let updated = work;
879
1428
  if (!work.evidenceRefs.some((evidence) => evidence.evidenceId === receipt.receiptId)) {
880
1429
  updated = appendWorkEvidence({ controllerHome, repoId: workRepoId }, work.workId, {
@@ -911,10 +1460,12 @@ function compatibilityJobFromReceipt(receipt: PluginActionReceipt, checkoutId: s
911
1460
  schemaVersion: 1,
912
1461
  revision: 1,
913
1462
  jobId: receipt.receiptId,
914
- repoId: receipt.repoId,
1463
+ repoId: receipt.repoId ?? receipt.scopeKey,
915
1464
  checkoutId,
916
1465
  type: 'plugin-action',
917
- status: receipt.status === 'succeeded' ? 'succeeded' : 'failed',
1466
+ status: receipt.effectOutcome === 'outcome_unknown'
1467
+ ? 'human_attention_required'
1468
+ : receipt.status === 'succeeded' ? 'succeeded' : 'failed',
918
1469
  priority: 'P2',
919
1470
  requestId: receipt.requestId,
920
1471
  semanticKey: receipt.semanticKey,
@@ -941,9 +1492,9 @@ function compatibilityJobFromReceipt(receipt: PluginActionReceipt, checkoutId: s
941
1492
  * Deterministic plugin action path: validate → authorize/confirm → invoke adapter → receipt.
942
1493
  * Does not create ExecutionJobs or call a model.
943
1494
  */
944
- export async function submitAssistantPluginAction(
1495
+ async function submitPluginAction(
945
1496
  controllerHome: string,
946
- repository: RepositoryRecord,
1497
+ scope: PluginActionScopeContext,
947
1498
  request: AssistantPluginActionRequest,
948
1499
  ): Promise<{
949
1500
  manifest: AssistantPluginManifest;
@@ -957,44 +1508,51 @@ export async function submitAssistantPluginAction(
957
1508
  }> {
958
1509
  const adapter = resolvePluginAdapter(controllerHome, request.pluginId);
959
1510
  if (!adapter) throw new Error(`PLUGIN_NOT_FOUND: ${request.pluginId}`);
960
- 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);
961
1514
  const action = actionForManifest(manifest, request.actionId);
962
1515
  if (!manifest.enabled && action.actionId !== 'configure') {
963
1516
  throw new Error(`PLUGIN_DISABLED: ${request.pluginId} is disabled`);
964
1517
  }
1518
+ if (action.executionMode === 'direct_non_persistent') {
1519
+ throw new Error(`PLUGIN_DIRECT_NON_PERSISTENT_RECEIPT_PATH_FORBIDDEN: ${request.pluginId}/${request.actionId}`);
1520
+ }
965
1521
  const normalizedArgs = validateActionArguments(action, request.args ?? {});
966
1522
  enforceConfirmation(action, { ...request, args: normalizedArgs });
967
1523
 
968
- const key = semanticKey(repository, request.pluginId, request.actionId, normalizedArgs);
1524
+ const key = semanticKey(scope, request.pluginId, request.actionId, normalizedArgs);
969
1525
  const requestPath = pluginActionRequestPath(controllerHome, request.requestId);
970
1526
  if (existsSync(requestPath)) {
971
1527
  const index = readJsonFile<PluginActionRequestIndex>(requestPath);
972
1528
  if (index.semanticKey !== key) {
973
1529
  throw new Error(`REQUEST_ID_CONFLICT: ${request.requestId} already belongs to ${index.semanticKey}`);
974
1530
  }
975
- if (index.repoId !== repository.repoId) {
976
- 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}`);
977
1535
  }
978
- const existing = readPluginActionReceipt(controllerHome, index.repoId, index.receiptId);
1536
+ const existing = readPluginActionReceipt(controllerHome, existingScopeKey, index.receiptId);
979
1537
  if (!existing) {
980
1538
  throw new Error(`PLUGIN_RECEIPT_LOST: ${request.requestId}`);
981
1539
  }
982
1540
  if (request.workId && existing.workId !== request.workId) {
983
1541
  throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_CONFLICT: ${request.requestId} already belongs to Work ${existing.workId ?? 'none'}`);
984
1542
  }
985
- const requestedWorkRepoId = request.workId ? workAttributionRepoId(repository, request) : undefined;
986
- if (requestedWorkRepoId && (index.workRepoId ?? index.repoId) !== requestedWorkRepoId) {
987
- 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'}`);
988
1546
  }
989
1547
  if (request.workId && action.risk === 'remote_write') {
990
- bindRemoteEffectReceiptToWork(controllerHome, repository, action, request, existing);
1548
+ bindRemoteEffectReceiptToWork(controllerHome, scope, action, request, existing);
991
1549
  } else if (request.workId) {
992
- bindLocalEffectReceiptToAttributedWork(controllerHome, repository, action, request, existing);
1550
+ bindLocalEffectReceiptToAttributedWork(controllerHome, scope, action, request, existing);
993
1551
  }
994
1552
  return {
995
1553
  manifest,
996
1554
  action,
997
- job: compatibilityJobFromReceipt(existing, repository.activeCheckoutId),
1555
+ job: compatibilityJobFromReceipt(existing, scope.checkoutId ?? CONTROLLER_PLUGIN_SCOPE_KEY),
998
1556
  deduplicated: true,
999
1557
  result: existing.result,
1000
1558
  receipt: existing,
@@ -1009,74 +1567,56 @@ export async function submitAssistantPluginAction(
1009
1567
  let authorization = initialAuthorizationEvidence(manifest, action);
1010
1568
  let authorizationContext: AssistantPluginAuthorizationContext | undefined;
1011
1569
  let activeAuthorizationGrant: PluginCapabilityAuthorizationGrant | undefined;
1012
- const requiresLocalEffectWork = localSystemActionRequiresWork(
1013
- repository,
1014
- request.pluginId,
1015
- request.actionId,
1016
- normalizedArgs,
1017
- );
1570
+ if (requiresAutomatedWriteAuthorization(request.origin)) {
1571
+ activeAuthorizationGrant = await resolveAutomatedWriteAuthorization({
1572
+ controllerHome,
1573
+ scope,
1574
+ adapter,
1575
+ manifest,
1576
+ action,
1577
+ args: normalizedArgs,
1578
+ origin: request.origin,
1579
+ requestId: request.requestId,
1580
+ jobId: receiptId,
1581
+ authorizationGrantRefs: request.authorizationGrantRefs,
1582
+ });
1583
+ if (activeAuthorizationGrant) {
1584
+ authorization = {
1585
+ source: 'capability_grant',
1586
+ reusable: true,
1587
+ capabilityId,
1588
+ target: activeAuthorizationGrant.target,
1589
+ grantId: activeAuthorizationGrant.grantId,
1590
+ };
1591
+ }
1592
+ }
1018
1593
  const boundRemoteWork = action.risk === 'remote_write'
1019
- ? remoteEffectWorkForPluginAction(controllerHome, repository, action, request)
1020
- : undefined;
1021
- const attributedWork = !requiresLocalEffectWork
1022
- ? attributedWorkForPluginAction(controllerHome, repository, action, request)
1594
+ ? remoteEffectWorkForPluginAction(controllerHome, scope, action, request)
1023
1595
  : undefined;
1024
- const acceptedWork = requiresLocalEffectWork
1025
- ? acceptSubmittedWorkContract(controllerHome, {
1026
- requestId: request.requestId,
1027
- repoId: repository.repoId,
1028
- semanticKey: `local-effect:${key}`,
1029
- operation: {
1030
- name: `plugin:${request.pluginId}/${request.actionId}`,
1031
- semanticKey: key,
1032
- argumentHash: createHash('sha256').update(JSON.stringify(normalizedArgs)).digest('hex'),
1033
- mode: 'mutating',
1034
- idempotent: action.idempotent,
1035
- replayable: action.idempotent,
1036
- resourceClaims: claimsForAssistantPluginAction(action, repository, manifest.pluginId),
1037
- },
1038
- objective: `Execute bounded controller-local effect ${request.pluginId}/${request.actionId}`,
1039
- mode: 'direct_control',
1040
- requestedBy: 'chatgpt',
1041
- principalId: request.origin.actor,
1042
- controllerInstanceId: process.env.FORGE_RUNTIME_INSTANCE_ID?.trim()
1043
- || process.env.FORGE_WRITER_INSTANCE_ID?.trim()
1044
- || process.env.FORGE_DAEMON_INSTANCE_ID?.trim(),
1045
- workKind: 'local_effect',
1046
- risk: workRiskForPluginAction(action),
1047
- acceptanceCriteria: ['The requested local effect completes within its authorized Target Grant boundary.'],
1048
- constraints: {
1049
- requireHandoffOnAmbiguity: true,
1050
- allowDestructive: action.risk === 'destructive'
1051
- && request.confirmAuthorization === true
1052
- && request.confirmationText === action.requiredConfirmationText,
1053
- },
1054
- }).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)
1055
1601
  : undefined;
1056
- if (acceptedWork) {
1057
- updateWorkContract({ controllerHome, repoId: repository.repoId }, acceptedWork.workId, {
1058
- status: 'running',
1059
- workKind: 'local_effect',
1060
- dispatchState: 'running',
1061
- 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),
1617
+ },
1062
1618
  });
1063
1619
  }
1064
- appendRuntimeEvent(controllerHome, {
1065
- repoId: repository.repoId,
1066
- entityType: 'plugin',
1067
- entityId: manifest.pluginId,
1068
- eventType: 'plugin_action_requested',
1069
- requestId: request.requestId,
1070
- revision: manifest.revision,
1071
- data: {
1072
- actionId: action.actionId,
1073
- receiptId,
1074
- risk: action.risk,
1075
- confirmation: action.confirmation,
1076
- capabilityId,
1077
- authorizationReuseSupported: action.confirmation === 'authorization' && Boolean(adapter.resolveAuthorizationContext),
1078
- },
1079
- });
1080
1620
 
1081
1621
  try {
1082
1622
  if (action.confirmation === 'authorization'
@@ -1084,8 +1624,7 @@ export async function submitAssistantPluginAction(
1084
1624
  && originMayEstablishCapabilityAuthorization(request.origin)) {
1085
1625
  authorizationContext = await adapter.resolveAuthorizationContext({
1086
1626
  controllerHome,
1087
- repoId: repository.repoId,
1088
- repoRoot: repository.canonicalRoot,
1627
+ ...pluginActionExecutionFields(scope),
1089
1628
  pluginId: request.pluginId,
1090
1629
  actionId: request.actionId,
1091
1630
  requestId: request.requestId,
@@ -1105,7 +1644,7 @@ export async function submitAssistantPluginAction(
1105
1644
  try {
1106
1645
  activeAuthorizationGrant = findActivePluginCapabilityAuthorization(controllerHome, {
1107
1646
  ownerScope: pluginCapabilityAuthorizationOwnerScope(request.origin),
1108
- repoId: repository.repoId,
1647
+ repoId: scope.grantRepoId,
1109
1648
  pluginId: request.pluginId,
1110
1649
  capabilityId,
1111
1650
  target: authorizationContext.target,
@@ -1130,22 +1669,53 @@ export async function submitAssistantPluginAction(
1130
1669
  }
1131
1670
  }
1132
1671
 
1133
- const result = await executeAssistantPluginAction({
1672
+ const result = await withAssistantPluginResourceLeases(
1134
1673
  controllerHome,
1135
- repoId: repository.repoId,
1136
- repoRoot: repository.canonicalRoot,
1137
- pluginId: request.pluginId,
1138
- actionId: request.actionId,
1139
- requestId: request.requestId,
1140
- args: normalizedArgs,
1141
- origin: request.origin,
1142
- jobId: receiptId,
1143
- timeoutMs: request.timeoutMs,
1144
- signal: request.signal,
1145
- deadlineAtMs: typeof request.timeoutMs === 'number'
1146
- ? Date.now() + Math.max(1, Math.trunc(request.timeoutMs))
1147
- : undefined,
1148
- });
1674
+ scope,
1675
+ action,
1676
+ request,
1677
+ () => executePluginActionInScope(scope, {
1678
+ controllerHome,
1679
+ ...pluginActionExecutionFields(scope),
1680
+ pluginId: request.pluginId,
1681
+ actionId: request.actionId,
1682
+ requestId: request.requestId,
1683
+ args: normalizedArgs,
1684
+ origin: request.origin,
1685
+ jobId: receiptId,
1686
+ timeoutMs: request.timeoutMs,
1687
+ signal: request.signal,
1688
+ authorizationGrantRefs: request.authorizationGrantRefs,
1689
+ deadlineAtMs: typeof request.timeoutMs === 'number'
1690
+ ? Date.now() + Math.max(1, Math.trunc(request.timeoutMs))
1691
+ : undefined,
1692
+ }),
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
+
1149
1719
  if (action.confirmation === 'authorization'
1150
1720
  && authorizationContext
1151
1721
  && !activeAuthorizationGrant
@@ -1153,7 +1723,7 @@ export async function submitAssistantPluginAction(
1153
1723
  try {
1154
1724
  const grant = recordPluginCapabilityAuthorization(controllerHome, {
1155
1725
  ownerScope: pluginCapabilityAuthorizationOwnerScope(request.origin),
1156
- repoId: repository.repoId,
1726
+ repoId: scope.grantRepoId,
1157
1727
  pluginId: request.pluginId,
1158
1728
  capabilityId,
1159
1729
  target: authorizationContext.target,
@@ -1187,42 +1757,8 @@ export async function submitAssistantPluginAction(
1187
1757
  work: {
1188
1758
  workId: boundRemoteWork.workId,
1189
1759
  workKind: 'remote_effect',
1760
+ semanticState: 'open' as const,
1190
1761
  ...(terminalRemoteEffect ? { completionOutcome: 'completed_remote' as const } : {}),
1191
- status: terminalRemoteEffect ? 'completed' : 'running',
1192
- },
1193
- };
1194
- }
1195
- if (acceptedWork) {
1196
- const recordedAt = new Date().toISOString();
1197
- const target = localEffectTarget(normalizedArgs, result);
1198
- appendWorkEvidence({ controllerHome, repoId: repository.repoId }, acceptedWork.workId, {
1199
- title: 'controller-local effect completed',
1200
- summary: `${request.pluginId}/${request.actionId} completed for ${target.id}.`,
1201
- detailLevel: 'summary',
1202
- });
1203
- recordWorkCompletionReceipt(
1204
- { controllerHome, repoId: repository.repoId },
1205
- acceptedWork.workId,
1206
- {
1207
- schemaVersion: 1,
1208
- receiptId: `LFX-${Date.now()}-${createHash('sha256').update(`${request.requestId}:${acceptedWork.workId}`).digest('hex').slice(0, 8)}`,
1209
- source: 'local_effect',
1210
- workId: acceptedWork.workId,
1211
- operation: `${request.pluginId}/${request.actionId}`,
1212
- target,
1213
- changed: true,
1214
- recordedAt,
1215
- },
1216
- 'completed_local',
1217
- 'local_effect',
1218
- );
1219
- resultWithLineage = {
1220
- ...result,
1221
- work: {
1222
- workId: acceptedWork.workId,
1223
- workKind: 'local_effect',
1224
- completionOutcome: 'completed_local',
1225
- status: 'completed',
1226
1762
  },
1227
1763
  };
1228
1764
  }
@@ -1230,157 +1766,213 @@ export async function submitAssistantPluginAction(
1230
1766
  schemaVersion: 1,
1231
1767
  receiptId,
1232
1768
  requestId: request.requestId,
1233
- repoId: repository.repoId,
1234
- ...(request.workId ? { workRepoId: workAttributionRepoId(repository, request) } : {}),
1769
+ scopeKey: scope.scopeKey,
1770
+ ...(scope.repository ? { repoId: scope.repository.repoId } : {}),
1771
+ ...(request.workId ? { workRepoId: workAttributionRepoId(scope, request) } : {}),
1235
1772
  pluginId: request.pluginId,
1236
1773
  actionId: request.actionId,
1237
1774
  semanticKey: key,
1238
1775
  status: 'succeeded',
1239
1776
  createdAt,
1240
- ...(acceptedWork
1241
- ? { workId: acceptedWork.workId }
1242
- : boundRemoteWork
1243
- ? { workId: boundRemoteWork.workId }
1244
- : attributedWork
1245
- ? { workId: attributedWork.workId }
1246
- : {}),
1777
+ ...(boundRemoteWork
1778
+ ? { workId: boundRemoteWork.workId }
1779
+ : attributedWork
1780
+ ? { workId: attributedWork.workId }
1781
+ : {}),
1247
1782
  origin: request.origin,
1248
1783
  authorization,
1249
1784
  result: resultWithLineage,
1250
1785
  };
1251
- writeJsonAtomic(pluginActionReceiptPath(controllerHome, repository.repoId, receiptId), receipt);
1786
+ writeJsonAtomic(pluginActionReceiptPath(controllerHome, scope.scopeKey, receiptId), receipt);
1252
1787
  writeJsonAtomic(requestPath, {
1253
1788
  requestId: request.requestId,
1254
- repoId: repository.repoId,
1255
- ...(request.workId ? { workRepoId: workAttributionRepoId(repository, request) } : {}),
1789
+ scopeKey: scope.scopeKey,
1790
+ ...(scope.repository ? { repoId: scope.repository.repoId } : {}),
1791
+ ...(request.workId ? { workRepoId: workAttributionRepoId(scope, request) } : {}),
1256
1792
  receiptId,
1257
1793
  semanticKey: key,
1258
1794
  createdAt,
1259
1795
  } satisfies PluginActionRequestIndex);
1260
1796
  if (boundRemoteWork) {
1261
- bindRemoteEffectReceiptToWork(controllerHome, repository, action, request, receipt);
1797
+ bindRemoteEffectReceiptToWork(controllerHome, scope, action, request, receipt);
1262
1798
  } else if (attributedWork) {
1263
- bindLocalEffectReceiptToAttributedWork(controllerHome, repository, action, request, receipt, attributedWork);
1799
+ bindLocalEffectReceiptToAttributedWork(controllerHome, scope, action, request, receipt, attributedWork);
1264
1800
  }
1265
- 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);
1266
1804
  return {
1267
1805
  manifest: nextManifest,
1268
1806
  action,
1269
- job: compatibilityJobFromReceipt(receipt, repository.activeCheckoutId),
1807
+ job: compatibilityJobFromReceipt(receipt, scope.checkoutId ?? CONTROLLER_PLUGIN_SCOPE_KEY),
1270
1808
  deduplicated: false,
1271
1809
  result: resultWithLineage,
1272
1810
  receipt,
1273
1811
  authorization,
1274
- workId: acceptedWork?.workId ?? boundRemoteWork?.workId ?? attributedWork?.workId,
1812
+ workId: boundRemoteWork?.workId ?? attributedWork?.workId,
1275
1813
  };
1276
1814
  } catch (error) {
1277
1815
  const message = error instanceof Error ? error.message : String(error);
1278
- const code = /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'PLUGIN_ACTION_FAILED';
1816
+ const code = isAssistantPluginError(error)
1817
+ ? error.code
1818
+ : /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'PLUGIN_ACTION_FAILED';
1819
+ const effectOutcome: AssistantPluginEffectOutcome = isAssistantPluginError(error)
1820
+ ? error.effectOutcome
1821
+ : 'failed';
1822
+ const outcomeUnknown = effectOutcome === 'outcome_unknown';
1823
+ const outcomeResult = outcomeUnknown
1824
+ ? { outcome: 'outcome_unknown', error: { code, message } }
1825
+ : undefined;
1279
1826
  if (boundRemoteWork) {
1280
- appendWorkEvidence({ controllerHome, repoId: workAttributionRepoId(repository, request) }, boundRemoteWork.workId, {
1281
- title: 'typed remote plugin effect failed',
1827
+ appendWorkEvidence({ controllerHome, repoId: workAttributionRepoId(scope, request) }, boundRemoteWork.workId, {
1828
+ title: outcomeUnknown ? 'typed remote plugin effect outcome unknown' : 'typed remote plugin effect failed',
1282
1829
  summary: `${request.pluginId}/${request.actionId}: ${message}`.slice(0, 1_000),
1283
1830
  detailLevel: 'summary',
1284
1831
  });
1285
1832
  }
1286
- if (acceptedWork) {
1287
- const current = getWorkContract({ controllerHome, repoId: repository.repoId }, acceptedWork.workId);
1288
- updateWorkContract({ controllerHome, repoId: repository.repoId }, acceptedWork.workId, {
1289
- status: 'failed',
1290
- workKind: 'local_effect',
1291
- dispatchState: 'terminal',
1292
- evidenceState: 'failed',
1293
- evidenceRefs: [{
1294
- title: 'controller-local effect failed',
1295
- summary: `${request.pluginId}/${request.actionId}: ${message}`.slice(0, 1_000),
1296
- detailLevel: 'summary',
1297
- }, ...(current?.evidenceRefs ?? [])],
1298
- });
1299
- }
1300
1833
  const receipt: PluginActionReceipt = {
1301
1834
  schemaVersion: 1,
1302
1835
  receiptId,
1303
1836
  requestId: request.requestId,
1304
- repoId: repository.repoId,
1305
- ...(request.workId ? { workRepoId: workAttributionRepoId(repository, request) } : {}),
1837
+ scopeKey: scope.scopeKey,
1838
+ ...(scope.repository ? { repoId: scope.repository.repoId } : {}),
1839
+ ...(request.workId ? { workRepoId: workAttributionRepoId(scope, request) } : {}),
1306
1840
  pluginId: request.pluginId,
1307
1841
  actionId: request.actionId,
1308
1842
  semanticKey: key,
1309
1843
  status: 'failed',
1844
+ effectOutcome,
1310
1845
  createdAt,
1311
- ...(acceptedWork
1312
- ? { workId: acceptedWork.workId }
1313
- : boundRemoteWork
1314
- ? { workId: boundRemoteWork.workId }
1315
- : attributedWork
1316
- ? { workId: attributedWork.workId }
1317
- : {}),
1846
+ ...(boundRemoteWork
1847
+ ? { workId: boundRemoteWork.workId }
1848
+ : attributedWork
1849
+ ? { workId: attributedWork.workId }
1850
+ : {}),
1318
1851
  origin: request.origin,
1319
1852
  authorization,
1853
+ ...(outcomeResult ? { result: outcomeResult } : {}),
1320
1854
  error: { code, message },
1321
1855
  };
1322
- writeJsonAtomic(pluginActionReceiptPath(controllerHome, repository.repoId, receiptId), receipt);
1856
+ writeJsonAtomic(pluginActionReceiptPath(controllerHome, scope.scopeKey, receiptId), receipt);
1323
1857
  writeJsonAtomic(requestPath, {
1324
1858
  requestId: request.requestId,
1325
- repoId: repository.repoId,
1859
+ scopeKey: scope.scopeKey,
1860
+ ...(scope.repository ? { repoId: scope.repository.repoId } : {}),
1861
+ ...(request.workId ? { workRepoId: workAttributionRepoId(scope, request) } : {}),
1326
1862
  receiptId,
1327
1863
  semanticKey: key,
1328
1864
  createdAt,
1329
1865
  } satisfies PluginActionRequestIndex);
1866
+ if (outcomeUnknown) {
1867
+ return {
1868
+ manifest,
1869
+ action,
1870
+ job: compatibilityJobFromReceipt(receipt, scope.checkoutId ?? CONTROLLER_PLUGIN_SCOPE_KEY),
1871
+ deduplicated: false,
1872
+ result: outcomeResult,
1873
+ receipt,
1874
+ authorization,
1875
+ workId: boundRemoteWork?.workId ?? attributedWork?.workId,
1876
+ };
1877
+ }
1330
1878
  throw error;
1331
1879
  }
1332
1880
  }
1333
1881
 
1334
- export async function executeAssistantPluginAction(
1882
+ export function submitAssistantPluginAction(
1883
+ controllerHome: string,
1884
+ repository: RepositoryRecord,
1885
+ request: AssistantPluginActionRequest,
1886
+ ) {
1887
+ return submitPluginAction(controllerHome, repositoryPluginActionScope(repository), request);
1888
+ }
1889
+
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,
1335
1899
  input: AssistantPluginActionExecutionInput,
1336
1900
  ): Promise<Record<string, unknown>> {
1337
1901
  const adapter = resolvePluginAdapter(input.controllerHome, input.pluginId);
1338
1902
  if (!adapter) throw new Error(`PLUGIN_NOT_FOUND: ${input.pluginId}`);
1339
- const repository = {
1340
- repoId: input.repoId,
1341
- canonicalRoot: input.repoRoot,
1342
- activeCheckoutId: 'active',
1343
- } as RepositoryRecord;
1344
- 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);
1345
1906
  const manifest = manifestLookup.manifest;
1346
1907
  const action = actionForManifest(manifest, input.actionId);
1347
- denyAutomatedWrite(manifest, action, input.origin);
1908
+ if (action.executionMode === 'direct_non_persistent') {
1909
+ throw new Error(`PLUGIN_DIRECT_NON_PERSISTENT_APPLICATION_PATH_REQUIRED: ${input.pluginId}/${input.actionId}`);
1910
+ }
1348
1911
  const normalizedArgs = validateActionArguments(action, input.args);
1912
+ if (requiresAutomatedWriteAuthorization(input.origin)) {
1913
+ await resolveAutomatedWriteAuthorization({
1914
+ controllerHome: input.controllerHome,
1915
+ scope,
1916
+ adapter,
1917
+ manifest,
1918
+ action,
1919
+ args: normalizedArgs,
1920
+ origin: input.origin,
1921
+ requestId: input.requestId,
1922
+ jobId: input.jobId,
1923
+ authorizationGrantRefs: input.authorizationGrantRefs,
1924
+ });
1925
+ }
1349
1926
  try {
1350
1927
  const result = await adapter.executeAction({
1351
1928
  ...input,
1929
+ ...pluginActionExecutionFields(scope),
1352
1930
  args: normalizedArgs,
1353
1931
  providerIdentityPrevalidated: manifestLookup.providerIdentityPrevalidated,
1354
1932
  });
1355
1933
  for (const affectedPluginId of adapter.affectedPluginIdsAfterAction?.(input.actionId, result) ?? []) {
1356
1934
  if (!affectedPluginId || affectedPluginId === input.pluginId) continue;
1357
- invalidateAssistantPluginManifestCache(input.controllerHome, repository.repoId, affectedPluginId);
1358
1935
  const affectedAdapter = resolvePluginAdapter(input.controllerHome, affectedPluginId);
1359
- if (affectedAdapter && adapterMatchesRepository(affectedAdapter, repository)) {
1360
- 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
+ }
1361
1943
  } else {
1362
- 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
+ }
1363
1951
  }
1364
1952
  }
1365
1953
  const refreshManifest = adapter.shouldRefreshManifestAfterAction?.(input.actionId) ?? true;
1366
1954
  const nextManifest = refreshManifest
1367
- ? 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
1368
1958
  : manifest;
1369
- appendRuntimeEvent(input.controllerHome, {
1370
- repoId: input.repoId,
1371
- entityType: 'plugin',
1372
- entityId: input.pluginId,
1373
- eventType: 'plugin_action_succeeded',
1374
- requestId: input.requestId,
1375
- revision: nextManifest.revision,
1376
- data: {
1377
- actionId: input.actionId,
1378
- jobId: input.jobId,
1379
- resultKeys: Object.keys(result).slice(0, 20),
1380
- lifecycleState: nextManifest.lifecycle.state,
1381
- healthState: nextManifest.health.state,
1382
- },
1383
- });
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
+ }
1384
1976
  return {
1385
1977
  schemaVersion: 1,
1386
1978
  plugin: {
@@ -1408,25 +2000,49 @@ export async function executeAssistantPluginAction(
1408
2000
  actionId: input.actionId,
1409
2001
  },
1410
2002
  });
1411
- const refreshed = syncAssistantPluginManifest(input.controllerHome, repository, input.pluginId);
1412
- const nextManifest = refreshed.manifest;
1413
- appendRuntimeEvent(input.controllerHome, {
1414
- repoId: input.repoId,
1415
- entityType: 'plugin',
1416
- entityId: input.pluginId,
1417
- eventType: 'plugin_action_failed',
1418
- requestId: input.requestId,
1419
- revision: nextManifest.revision,
1420
- data: {
1421
- actionId: input.actionId,
1422
- jobId: input.jobId,
1423
- code: pluginError.code,
1424
- retryable: pluginError.retryable,
1425
- },
1426
- });
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
+ }
1427
2022
  throw new AssistantPluginError(pluginError.code, pluginError.message.replace(/^[^:]+:\s*/, ''), {
1428
2023
  retryable: pluginError.retryable,
1429
2024
  details: pluginError.details,
1430
2025
  });
1431
2026
  }
1432
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
+ }