@moretea-labs/forge 1.8.1 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (328) hide show
  1. package/AGENTS.md +5 -2
  2. package/CHANGELOG.md +20 -0
  3. package/README.en.md +24 -3
  4. package/README.md +10 -6
  5. package/README.zh-CN.md +5 -3
  6. package/THIRD_PARTY_NOTICES.md +1 -2
  7. package/adapters/chatgpt/browser-delivery-runtime.ts +5 -3
  8. package/adapters/chatgpt/provider-delivery.ts +1 -1
  9. package/adapters/chatgpt/work-conversation-binding-store.ts +20 -1
  10. package/adapters/computer/chatgpt-extension-target.ts +295 -50
  11. package/adapters/computer/chrome-extension/background.js +227 -71
  12. package/adapters/computer/chrome-extension/content.js +181 -157
  13. package/adapters/computer/chrome-extension/manifest.json +3 -1
  14. package/adapters/computer/desktop-operator-provider.ts +18 -6
  15. package/adapters/computer/interaction-target-authority.ts +39 -9
  16. package/adapters/mcp/access-mode.ts +18 -39
  17. package/adapters/mcp/auth.ts +2 -6
  18. package/adapters/mcp/instructions.ts +2 -2
  19. package/adapters/mcp/multi-repository.ts +7 -28
  20. package/adapters/mcp/runtime-gateway/context-adapter.ts +79 -40
  21. package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +7 -15
  22. package/adapters/mcp/runtime-gateway/execution-tools.ts +7 -13
  23. package/adapters/mcp/runtime-gateway/plugin-adapter.ts +16 -3
  24. package/adapters/mcp/runtime-gateway/process-tools.ts +3 -21
  25. package/adapters/mcp/runtime-gateway/router.ts +3 -5
  26. package/adapters/mcp/runtime-gateway/routing-policy.ts +17 -10
  27. package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1 -1
  28. package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +14 -9
  29. package/adapters/mcp/runtime-gateway/runtime-tools.ts +1 -5
  30. package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +36 -1
  31. package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +58 -4
  32. package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +7 -3
  33. package/adapters/mcp/runtime-gateway/work-adapter.ts +16 -16
  34. package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +0 -17
  35. package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +69 -12
  36. package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +26 -2
  37. package/adapters/mcp/server.ts +24 -3
  38. package/adapters/mcp/setup.ts +4 -10
  39. package/adapters/mcp/tool-mapping/access-tools.ts +0 -6
  40. package/adapters/mcp/tool-mapping/legacy-tool-service.ts +672 -2935
  41. package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +1 -1
  42. package/adapters/mcp/tool-mapping/repository-tools.ts +52 -86
  43. package/adapters/mcp/toolset-names.ts +11 -42
  44. package/adapters/mcp/toolset.ts +16 -90
  45. package/{src/cli → adapters}/mcp/transports/AGENTS.md +5 -5
  46. package/{src/cli → adapters}/mcp/transports/CLAUDE.md +5 -5
  47. package/adapters/mcp/transports/http-observation.ts +18 -6
  48. package/adapters/mcp/transports/http-post-lifecycle.ts +19 -0
  49. package/adapters/mcp/transports/http.ts +54 -14
  50. package/adapters/mcp/types.ts +0 -6
  51. package/assets/hooks/lib/session-state.sh +0 -38
  52. package/assets/hooks/post-bash.sh +1 -1
  53. package/assets/hooks/post-tool-observer.sh +0 -5
  54. package/assets/hooks/prompt-guard.sh +0 -55
  55. package/assets/partials/04-project-structure.partial.md +1 -1
  56. package/assets/partials/07-footer.partial.md +1 -1
  57. package/assets/partials-agents/02-operating-mode.partial.md +1 -1
  58. package/assets/partials-agents/08-deep-docs.partial.md +1 -1
  59. package/assets/reference-configs/agentic-development-flow.md +1 -1
  60. package/assets/reference-configs/external-tooling.md +0 -76
  61. package/assets/reference-configs/release-deploy.md +1 -1
  62. package/assets/templates/helpers/architecture-queue.sh +1 -1
  63. package/assets/templates/helpers/check-agent-tooling.sh +1 -327
  64. package/assets/templates/helpers/check-task-workflow.sh +1 -1
  65. package/assets/templates/helpers/ensure-task-workflow.sh +0 -14
  66. package/assets/templates/helpers/migrate-project-template.sh +2 -2
  67. package/assets/workflow-contract.v1.json +0 -2
  68. package/docs/README.md +5 -2
  69. package/docs/architecture/CURRENT.md +114 -30
  70. package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +42 -0
  71. package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +15 -2
  72. package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +4 -2
  73. package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +109 -4
  74. package/docs/architecture/decisions/20261009-exact-code-context-discovery.md +19 -0
  75. package/docs/architecture/decisions/20261009-generated-xcode-cache-retention.md +37 -0
  76. package/docs/architecture/decisions/20261011-autonomous-continuation-convergence.md +597 -0
  77. package/docs/architecture/decisions/20261017-edit-session-finalization-reconciliation.md +17 -0
  78. package/docs/architecture/decisions/20261019-retire-host-skills.md +16 -0
  79. package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +1 -1
  80. package/docs/architecture/domains/public-surface.md +3 -5
  81. package/docs/architecture/domains/verification.md +5 -5
  82. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +2 -2
  83. package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +4 -4
  84. package/docs/architecture/reviews/20261015-code-search-runtime-acceptance.md +75 -0
  85. package/docs/forge-plugin-management.md +14 -2
  86. package/docs/operations/controller-browser-plugin.md +23 -0
  87. package/docs/operations/features.md +6 -7
  88. package/docs/operations/features.zh-CN.md +1 -1
  89. package/docs/operations/github-repository.md +4 -4
  90. package/docs/operations/github-semantic-release-projection.md +20 -0
  91. package/docs/operations/homebrew.md +2 -2
  92. package/docs/operations/mcp-tool-exposure.md +13 -54
  93. package/docs/operations/runtime-performance-diagnostics.md +8 -0
  94. package/docs/operations/standalone-disaster-recovery.md +1 -1
  95. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  96. package/docs/tutorials/03-first-repository-task.md +2 -0
  97. package/docs/wiki/Computer-and-Plugins.md +78 -0
  98. package/docs/wiki/Computer-and-Plugins.zh-CN.md +88 -0
  99. package/docs/wiki/Home.md +16 -2
  100. package/docs/wiki/Implementation.md +24 -12
  101. package/docs/wiki/Installation.md +28 -32
  102. package/docs/wiki/Integrations.md +4 -0
  103. package/docs/wiki/Quick-Start.md +35 -22
  104. package/docs/wiki/Releases-and-Upgrades.md +15 -25
  105. package/docs/wiki/_Sidebar.md +5 -1
  106. package/package.json +1 -12
  107. package/packages/kernel/controller/infrastructure/controller-session-store.ts +63 -38
  108. package/packages/kernel/identity/infrastructure/activity-projection-store.ts +10 -0
  109. package/packages/kernel/scheduler/application/eligibility.ts +4 -1
  110. package/packages/kernel/scheduler/infrastructure/schedule-store.ts +13 -10
  111. package/packages/kernel/work/application/work-service.ts +1 -0
  112. package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
  113. package/packages/kernel/work/domain/state-machine.ts +9 -2
  114. package/packages/kernel/work/infrastructure/work-contract-store.ts +28 -1
  115. package/packages/plugin-runtime/browser/provider-registry.ts +12 -36
  116. package/packages/plugin-runtime/computer/conversation-target.ts +17 -7
  117. package/scripts/assemble-template.ts +11 -72
  118. package/scripts/check-mcp-compatibility.ts +55 -74
  119. package/scripts/check-package-identity.mjs +0 -5
  120. package/scripts/check-runtime-architecture.mjs +305 -28
  121. package/scripts/init-project.sh +0 -46
  122. package/scripts/install-ios-hid-toolchain.sh +60 -0
  123. package/scripts/install-standalone-recovery.ts +1 -1
  124. package/scripts/lib/project-init-lib.sh +3 -3
  125. package/scripts/migrate-windows-wsl-controller-home.ts +2 -0
  126. package/scripts/open-source-audit-allowlist.txt +0 -1
  127. package/scripts/run-governed-gate.ts +2 -0
  128. package/scripts/smoke-ios-physical-device.ts +38 -10
  129. package/scripts/smoke-mcp-http-runtime.ts +7 -8
  130. package/scripts/smoke-mcp-tool-surface.ts +27 -41
  131. package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +6 -3
  132. package/scripts/sync-generated-authority.mjs +0 -1
  133. package/src/cli/agent-jobs/executor-health.ts +1 -1
  134. package/src/cli/agent-jobs/integration.ts +2 -2
  135. package/src/cli/chatgpt-browser/file-policy.ts +2 -2
  136. package/src/cli/commands/doctor.ts +1 -143
  137. package/src/cli/commands/global-runtime.ts +3 -196
  138. package/src/cli/commands/init-hook.ts +6 -23
  139. package/src/cli/commands/init.ts +3 -252
  140. package/src/cli/commands/mcp.ts +0 -5
  141. package/src/cli/commands/prompt-guard-decision.ts +0 -2
  142. package/src/cli/commands/recovery.ts +2 -14
  143. package/src/cli/commands/repository.ts +17 -1
  144. package/src/cli/commands/setup-execution.ts +41 -0
  145. package/src/cli/controller/completion-orchestrator.ts +1 -1
  146. package/src/cli/controller/composite-operations.ts +1 -1
  147. package/src/cli/controller/context/instruction-context.ts +1 -1
  148. package/src/cli/controller/context/known-paths.ts +2 -2
  149. package/src/cli/controller/context/query-planning.ts +11 -4
  150. package/src/cli/controller/context/rule-resolution.ts +2 -2
  151. package/src/cli/controller/context/source-materializer.ts +2 -2
  152. package/src/cli/controller/context/structural-provider.ts +84 -0
  153. package/src/cli/controller/context/structural.ts +6 -6
  154. package/src/cli/controller/context/types.ts +8 -9
  155. package/src/cli/controller/context-pack.ts +97 -76
  156. package/src/cli/controller/operational-plan.ts +1 -1
  157. package/src/cli/controller/systemd-user.ts +4 -10
  158. package/src/cli/editing/edit-session.ts +17 -5
  159. package/src/cli/github/github.ts +4 -4
  160. package/src/cli/github/semantic-projection.ts +174 -0
  161. package/src/cli/hook/prompt-intents.ts +0 -9
  162. package/src/cli/index.ts +1 -43
  163. package/src/cli/local-bridge/connector-freshness.ts +1 -12
  164. package/src/cli/local-bridge/console-automations.ts +1 -1
  165. package/src/cli/local-bridge/console-view-models.ts +1 -1
  166. package/src/cli/local-bridge/facade-api.ts +0 -2
  167. package/src/cli/repositories/command-classifier.ts +0 -9
  168. package/src/cli/repositories/command-scope.ts +2 -2
  169. package/src/cli/repositories/locks.ts +20 -0
  170. package/src/cli/repositories/registry.ts +2 -1
  171. package/src/cli/repositories/safe-patch.ts +2 -12
  172. package/src/cli/repositories/structured-git.ts +39 -3
  173. package/src/cli/repository/inspector.ts +185 -343
  174. package/src/effects/process-runner.ts +17 -1
  175. package/src/runtime/context/generic-lsp-provider.ts +1 -1
  176. package/src/runtime/context/semantic-navigation-contract.ts +7 -0
  177. package/src/runtime/context/semantic-navigation.ts +1 -1
  178. package/src/runtime/control-plane/cleanup-artifact-retention.ts +4 -4
  179. package/src/runtime/control-plane/execution/work-delivery-service.ts +150 -31
  180. package/src/runtime/control-plane/execution/work-handle-authority.ts +12 -4
  181. package/src/runtime/control-plane/execution/work-handle-store.ts +53 -1
  182. package/src/runtime/control-plane/execution/work-operation-service.ts +20 -10
  183. package/src/runtime/control-plane/execution/work-preparation-service.ts +108 -4
  184. package/src/runtime/control-plane/execution/work-process-evidence.ts +2 -2
  185. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +28 -13
  186. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +5 -1
  187. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +1 -1
  188. package/src/runtime/control-plane/facade/capability-registry.ts +2 -0
  189. package/src/runtime/control-plane/facade/work-continuation.ts +1 -1
  190. package/src/runtime/control-plane/generated-cache-retention.ts +114 -63
  191. package/src/runtime/control-plane/global-scheduler/maintenance.ts +20 -8
  192. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +1 -11
  193. package/src/runtime/control-plane/governance/external-effects.ts +1 -1
  194. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +1 -0
  195. package/src/runtime/control-plane/runtime-cleanup.ts +12 -93
  196. package/src/runtime/evidence/sensitive-output.ts +22 -2
  197. package/src/runtime/execution/jobs/child-reference.ts +11 -0
  198. package/src/runtime/execution/jobs/receipt-recovery.ts +1 -11
  199. package/src/runtime/execution/process-runtime/command-facade.ts +3 -3
  200. package/src/runtime/execution/process-runtime/runtime.ts +20 -16
  201. package/src/runtime/execution/thin-harness/execution-router.ts +16 -16
  202. package/src/runtime/execution/thin-harness/fast-executor.ts +2 -2
  203. package/src/runtime/execution/thin-harness/search-worker.ts +2 -2
  204. package/src/runtime/execution/workers/executor.ts +1 -2
  205. package/src/runtime/maintenance/cleanup.ts +41 -5
  206. package/src/runtime/plugins/action-application.ts +12 -1
  207. package/src/runtime/plugins/app-store-connect-adapter.ts +252 -4
  208. package/src/runtime/plugins/browser-adapter.ts +381 -313
  209. package/src/runtime/plugins/browser-macos-bridge.ts +114 -112
  210. package/src/runtime/plugins/browser-manifest-surface.ts +2 -1
  211. package/src/runtime/plugins/browser-registration.ts +2 -0
  212. package/src/runtime/plugins/browser-runtime.ts +26 -35
  213. package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +11 -2
  214. package/src/runtime/plugins/computer-registration.ts +76 -9
  215. package/src/runtime/plugins/github-adapter.ts +30 -3
  216. package/src/runtime/plugins/gmail-adapter.ts +166 -1
  217. package/src/runtime/plugins/google-shared.ts +10 -5
  218. package/src/runtime/plugins/ios/remote-xpc-hid.ts +70 -30
  219. package/src/runtime/plugins/ios-adapter.ts +45 -40
  220. package/src/runtime/plugins/ios-agent-device.ts +54 -4
  221. package/src/runtime/plugins/ios-device-ownership.ts +103 -0
  222. package/src/runtime/plugins/ios-physical-device.ts +497 -79
  223. package/src/runtime/plugins/local-system-adapter.ts +1 -99
  224. package/src/runtime/plugins/store.ts +80 -7
  225. package/src/runtime/plugins/types.ts +4 -0
  226. package/src/runtime/projections/git-status-sampler.ts +2 -19
  227. package/src/runtime/recovery/actions.ts +1 -1
  228. package/src/runtime/recovery/lifecycle-inventory.ts +1 -11
  229. package/src/runtime/recovery/probe.ts +2 -2
  230. package/src/runtime/release/release-coordinator.ts +20 -4
  231. package/src/runtime/root/controller-progression-composition.ts +67 -5
  232. package/src/runtime/root/package-connector-service.ts +123 -11
  233. package/src/runtime/root/package-runtime-release.ts +6 -18
  234. package/src/runtime/root/package-runtime-service.ts +7 -13
  235. package/src/runtime/root/release-execution-canary.ts +18 -5
  236. package/src/runtime/root/release-manifest.ts +0 -34
  237. package/src/runtime/root/release-materialize.ts +0 -78
  238. package/src/runtime/root/runtime.ts +1 -2
  239. package/src/runtime/root/scheduled-controller-composition.ts +2 -1
  240. package/src/runtime/root/service.ts +90 -11
  241. package/src/runtime/root/types.ts +0 -6
  242. package/src/runtime/root/workflow-supervisor-composition.ts +207 -19
  243. package/src/runtime/root/workflow-supervisor-runtime.ts +365 -10
  244. package/src/runtime/safe-tooling/google-credential-store.ts +1 -11
  245. package/src/runtime/safe-tooling/ios-development.ts +22 -8
  246. package/src/runtime/safe-tooling/job-summary.ts +1 -1
  247. package/src/runtime/safe-tooling/workspace-auth.ts +4 -1
  248. package/src/runtime/shared/atomic-text-file.ts +21 -0
  249. package/src/runtime/shared/json-files.ts +8 -3
  250. package/src/runtime/standalone-recovery/core.ts +189 -174
  251. package/src/runtime/standalone-recovery/entry.ts +62 -30
  252. package/src/runtime/standalone-recovery/operation-lock.ts +11 -1
  253. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +1 -0
  254. package/src/runtime/workflow/schedules/engine.ts +36 -14
  255. package/src/runtime/workflow/schedules/pending-activation.ts +11 -4
  256. package/src/runtime/workflow/schedules/work-continuation.ts +51 -6
  257. package/supervisor/client.ts +31 -14
  258. package/supervisor/control-plane.ts +210 -115
  259. package/supervisor/forge-validators.ts +88 -21
  260. package/supervisor/native-browser-adapter.ts +393 -28
  261. package/supervisor/native-messaging/host.ts +21 -2
  262. package/supervisor/protocol.ts +74 -26
  263. package/supervisor/server.ts +16 -8
  264. package/supervisor/store.ts +476 -57
  265. package/supervisor/types.ts +19 -13
  266. package/SKILL.md +0 -427
  267. package/adapters/computer/chatgpt-target-router.ts +0 -103
  268. package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +0 -87
  269. package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +0 -1
  270. package/adapters/mcp/toolset-selection.ts +0 -41
  271. package/agents/openai.yaml +0 -4
  272. package/assets/skill-hooks.json +0 -41
  273. package/assets/skill-version.json +0 -189
  274. package/assets/skills/claude-review/SKILL.md +0 -231
  275. package/assets/skills/codex-review/SKILL.md +0 -103
  276. package/assets/templates/helpers/check-skill-version.ts +0 -193
  277. package/scripts/check-skill-version.ts +0 -5
  278. package/scripts/ensure-codegraph.sh +0 -16
  279. package/scripts/run-skill-hook.ts +0 -244
  280. package/scripts/sync-codex-installed-copies.sh +0 -182
  281. package/src/cli/commands/tools.ts +0 -167
  282. package/src/cli/mcp/access-mode.ts +0 -2
  283. package/src/cli/mcp/access-tools.ts +0 -2
  284. package/src/cli/mcp/audit.ts +0 -2
  285. package/src/cli/mcp/auth.ts +0 -2
  286. package/src/cli/mcp/instructions.ts +0 -2
  287. package/src/cli/mcp/legacy-context.ts +0 -2
  288. package/src/cli/mcp/legacy-tool-service.ts +0 -2
  289. package/src/cli/mcp/multi-repository.ts +0 -2
  290. package/src/cli/mcp/oauth.ts +0 -2
  291. package/src/cli/mcp/openai-secure-tunnel.ts +0 -2
  292. package/src/cli/mcp/paths.ts +0 -2
  293. package/src/cli/mcp/policy.ts +0 -2
  294. package/src/cli/mcp/post-finalize-work-attribution.ts +0 -2
  295. package/src/cli/mcp/proxy-env.ts +0 -2
  296. package/src/cli/mcp/readiness-projection.ts +0 -2
  297. package/src/cli/mcp/redaction.ts +0 -2
  298. package/src/cli/mcp/repo.ts +0 -2
  299. package/src/cli/mcp/repository-tools.ts +0 -2
  300. package/src/cli/mcp/server.ts +0 -2
  301. package/src/cli/mcp/setup.ts +0 -2
  302. package/src/cli/mcp/tool-capability-cache.ts +0 -2
  303. package/src/cli/mcp/tool-contract.ts +0 -2
  304. package/src/cli/mcp/tools.ts +0 -2
  305. package/src/cli/mcp/toolset-names.ts +0 -2
  306. package/src/cli/mcp/toolset-selection.ts +0 -2
  307. package/src/cli/mcp/toolset.ts +0 -2
  308. package/src/cli/mcp/transports/http.ts +0 -2
  309. package/src/cli/mcp/transports/session-registry.ts +0 -2
  310. package/src/cli/mcp/transports/stdio.ts +0 -2
  311. package/src/cli/mcp/types.ts +0 -2
  312. package/src/cli/mcp/umbrella-tools.ts +0 -2
  313. package/src/cli/tools/codegraph.ts +0 -695
  314. package/src/core/adoption/helper-wrapper-plan.ts +0 -159
  315. package/src/runtime/context/codegraph-cache-boundary.ts +0 -255
  316. package/src/runtime/context/codegraph-read-provider.ts +0 -611
  317. package/src/runtime/context/codegraph-sidecar.cjs +0 -248
  318. package/src/runtime/control-plane/codegraph-cache-retention.ts +0 -164
  319. package/src/runtime/gateway/mcp/execution-tools.ts +0 -2
  320. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +0 -2
  321. package/src/runtime/gateway/mcp/persisted-check-process.ts +0 -2
  322. package/src/runtime/gateway/mcp/process-tools.ts +0 -2
  323. package/src/runtime/gateway/mcp/router.ts +0 -2
  324. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +0 -2
  325. package/src/runtime/gateway/mcp/runtime-tools.ts +0 -2
  326. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +0 -2
  327. package/src/runtime/plugins/browser-provider-registry.ts +0 -2
  328. package/src/runtime/plugins/computer-chatgpt-macos-target.ts +0 -891
@@ -43,10 +43,6 @@ import type {
43
43
  AssistantPluginPermissionScope,
44
44
  } from './types';
45
45
  import { AssistantPluginError } from './errors';
46
- import {
47
- classifyRepositoryCommand,
48
- classifyRepositoryCommandReplay,
49
- } from '../../cli/repositories/command-classifier';
50
46
  import {
51
47
  assertCommandPathOperandsStayInRepository,
52
48
  assertRepositoryCommandInputAllowed,
@@ -58,7 +54,6 @@ const PLUGIN_ID = 'local_system';
58
54
  const MAX_OUTPUT_BYTES = 64 * 1024;
59
55
  const MAX_TEXT_CHARS = 100_000;
60
56
  const MAX_DIRECTORY_ENTRIES = 200;
61
- const TARGET_SAFE_FILESYSTEM_MUTATORS = new Set(['touch', 'mkdir', 'cp', 'mv', 'install', 'tee', 'truncate']);
62
57
  const PROJECT_SCRIPT_RUNTIMES = new Set(['node', 'bun', 'python3', 'ruby', 'bash', 'sh']);
63
58
  const AUTO_OPEN_DENIED_EXTENSIONS = new Set([
64
59
  '.command', '.sh', '.bash', '.zsh', '.fish', '.tool',
@@ -140,14 +135,6 @@ function optionalString(args: Record<string, unknown>, key: string): string | un
140
135
  return value || undefined;
141
136
  }
142
137
 
143
- function requiredStringArray(args: Record<string, unknown>, key: string): string[] {
144
- const value = args[key];
145
- if (!Array.isArray(value) || value.length === 0 || value.some((entry) => typeof entry !== 'string' || !entry.trim())) {
146
- throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', `${key} must be a non-empty string array.`, { retryable: false });
147
- }
148
- return value.map((entry) => String(entry));
149
- }
150
-
151
138
  function optionalStringArray(args: Record<string, unknown>, key: string): string[] {
152
139
  const value = args[key];
153
140
  if (value === undefined) return [];
@@ -831,7 +818,6 @@ function actions(): AssistantPluginActionDescriptor[] {
831
818
  { actionId: 'delete_file', title: 'Delete file', description: 'Delete one exact regular file below an authorized read-write target. Arbitrary recursive or directory deletion is never exposed.', readOnly: false, risk: 'destructive', confirmation: 'strong_confirmation', requiredConfirmationText: 'delete-local-system-file', defaultTimeoutMs: 15_000, cancellable: true, idempotent: false, scopes: ['local-system.files.write'], resourceClaims: controllerWrite, argumentsSchema: { type: 'object', properties: targetProperties, required: ['target_key', 'path'], additionalProperties: false } },
832
819
  { actionId: 'delete_empty_directory', title: 'Delete empty directory', description: 'Delete one exact empty directory below an authorized read-write target. Target roots and recursive directory deletion are never allowed.', readOnly: false, risk: 'destructive', confirmation: 'strong_confirmation', requiredConfirmationText: 'delete-local-system-empty-directory', defaultTimeoutMs: 15_000, cancellable: true, idempotent: false, scopes: ['local-system.files.write'], resourceClaims: controllerWrite, argumentsSchema: { type: 'object', properties: targetProperties, required: ['target_key', 'path'], additionalProperties: false } },
833
820
  { actionId: 'initialize_git', title: 'Initialize local Git repository', description: 'Run only git init inside an authorized read-write target without registering it in Repository Registry.', readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 30_000, cancellable: true, idempotent: true, scopes: ['local-system.files.write'], resourceClaims: controllerWrite, argumentsSchema: { type: 'object', properties: { target_key: { type: 'string' }, cwd: { type: 'string' }, initial_branch: { type: 'string' } }, required: ['target_key'], additionalProperties: false } },
834
- { actionId: 'execute_command', title: 'Execute target command', description: 'Execute one bounded typed-argv command inside an authorized filesystem target. This is the narrow target-scoped alternative to broad ForgeInstance process_exec: shell strings, target escapes, destructive/remote writes, and Git mutations other than the dedicated initialize_git action are denied.', readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 30_000, cancellable: true, idempotent: false, scopes: ['local-system.files.read', 'local-system.files.write'], resourceClaims: controllerWrite, argumentsSchema: { type: 'object', properties: { target_key: { type: 'string' }, command: { type: 'array', items: { type: 'string' } }, cwd: { type: 'string' }, max_output_bytes: { type: 'number' } }, required: ['target_key', 'command'], additionalProperties: false } },
835
821
  { actionId: 'execute_project_script', title: 'Execute verified project script', description: 'Execute one exact script below an authorized read-write target through a fixed interpreter after its SHA-256 digest and strong confirmation match. Eval flags and shell command strings are never accepted.', readOnly: false, risk: 'destructive', confirmation: 'strong_confirmation', requiredConfirmationText: 'execute-local-system-project-script', defaultTimeoutMs: 30_000, cancellable: true, idempotent: false, scopes: ['local-system.files.read', 'local-system.files.write'], resourceClaims: controllerWrite, argumentsSchema: { type: 'object', properties: { target_key: { type: 'string' }, runtime: { type: 'string', enum: ['node', 'bun', 'python3', 'ruby', 'bash', 'sh'] }, script_path: { type: 'string' }, expected_sha256: { type: 'string' }, arguments: { type: 'array', items: { type: 'string' } }, cwd: { type: 'string' }, max_output_bytes: { type: 'number' } }, required: ['target_key', 'runtime', 'script_path', 'expected_sha256'], additionalProperties: false } },
836
822
  { actionId: 'create_directory', title: 'Create directory', description: 'Create a directory below an authorized target without leaving that root.', readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 15_000, cancellable: true, idempotent: true, scopes: ['local-system.files.write'], resourceClaims: controllerWrite, argumentsSchema: { type: 'object', properties: targetProperties, required: ['target_key', 'path'], additionalProperties: false } },
837
823
  { actionId: 'copy_file', title: 'Copy file', description: 'Copy a file between authorized targets without overwriting.', readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 30_000, cancellable: true, idempotent: false, scopes: ['local-system.files.write'], resourceClaims: controllerWrite, argumentsSchema: { type: 'object', properties: { source_target_key: { type: 'string' }, source_path: { type: 'string' }, destination_target_key: { type: 'string' }, destination_path: { type: 'string' } }, required: ['source_target_key', 'source_path', 'destination_target_key', 'destination_path'], additionalProperties: false } },
@@ -879,7 +865,7 @@ function capabilities(): AssistantPluginCapability[] {
879
865
  { capabilityId: 'local-system-recovery-tunnel-repair', title: 'Managed standalone Recovery tunnel repair', description: 'Repair only the installed Recovery public tunnel through the existing Recovery tunnel repair authority when the Recovery transport itself is unreachable.', scopes: ['local-system.process'], actions: ['repair_standalone_recovery_tunnel'] },
880
866
  { capabilityId: 'local-system-workflow-supervisor-browser-adapter', title: 'Workflow Supervisor browser adapter', description: 'Inspect or install the stateless Chrome adapter projection used by the Goal-level Workflow Supervisor authority.', scopes: ['local-system.browser-adapter'], actions: ['workflow_supervisor_browser_adapter_status', 'install_workflow_supervisor_browser_adapter'] },
881
867
  { capabilityId: 'local-system-open', title: 'Open local applications and files', description: 'Open applications and authorized files without arbitrary shell access.', scopes: ['local-system.open'], actions: ['open_application', 'reveal_in_finder', 'open_file'] },
882
- { capabilityId: 'local-system-files', title: 'Authorized local files', description: 'Use expiring target grants for bounded local file operations and typed-argv commands without repository registration.', scopes: ['local-system.files.read', 'local-system.files.write'], actions: ['list_targets', 'authorize_target', 'revoke_target', 'list_directory', 'read_text', 'write_text', 'delete_file', 'delete_empty_directory', 'initialize_git', 'execute_command', 'execute_project_script', 'create_directory', 'copy_file', 'move_file', 'rename_file'] },
868
+ { capabilityId: 'local-system-files', title: 'Authorized local files', description: 'Use expiring target grants for bounded local file operations without repository registration; terminal commands use the single execute_command MCP tool.', scopes: ['local-system.files.read', 'local-system.files.write'], actions: ['list_targets', 'authorize_target', 'revoke_target', 'list_directory', 'read_text', 'write_text', 'delete_file', 'delete_empty_directory', 'initialize_git', 'execute_project_script', 'create_directory', 'copy_file', 'move_file', 'rename_file'] },
883
869
  ];
884
870
  }
885
871
 
@@ -1093,90 +1079,6 @@ export async function executeLocalSystemPluginAction(input: AssistantPluginActio
1093
1079
  return { initialized: true, targetKey: resolved.target.targetKey, workspaceId: resolved.target.workspaceId, cwd: resolved.relativePath || '.', repositoryRegistered: false, command, stdout: executed.stdout, stderr: executed.stderr };
1094
1080
  });
1095
1081
  }
1096
- case 'execute_command': {
1097
- const targetKey = requiredString(input.args, 'target_key');
1098
- const argv = requiredStringArray(input.args, 'command');
1099
- const canonical = assertRepositoryCommandInputAllowed(argv);
1100
- if (canonical.kind !== 'argv') {
1101
- throw new AssistantPluginError('LOCAL_SYSTEM_COMMAND_TYPED_ARGV_REQUIRED', 'Target commands must use typed argv and cannot use a shell string.', { retryable: false });
1102
- }
1103
- const classification = classifyRepositoryCommand(canonical);
1104
- if (classification.risk === 'remote_write' || classification.risk === 'destructive') {
1105
- throw new AssistantPluginError('LOCAL_SYSTEM_COMMAND_RISK_DENIED', `Target command risk ${classification.risk} is not supported without repository registration.`, { retryable: false });
1106
- }
1107
- const executable = canonical.executable!.split(/[\\/]/).at(-1)?.toLowerCase() ?? '';
1108
- if (executable === 'git' && classification.risk !== 'readonly') {
1109
- throw new AssistantPluginError('LOCAL_SYSTEM_GIT_MUTATION_REQUIRES_REPOSITORY', 'Git mutations require a registered repository/checkout so refs and worktree ownership remain fenced.', { retryable: false });
1110
- }
1111
- if (classification.risk === 'workspace_write') {
1112
- const replay = classifyRepositoryCommandReplay(canonical);
1113
- const boundedFilesystemMutation = TARGET_SAFE_FILESYSTEM_MUTATORS.has(executable);
1114
- if (!boundedFilesystemMutation && !replay.replayable) {
1115
- throw new AssistantPluginError(
1116
- 'LOCAL_SYSTEM_COMMAND_REPOSITORY_REQUIRED',
1117
- `Target command ${executable || '<unknown>'} has unbounded or unclassified side effects; register/promote the target before executing it.`,
1118
- { retryable: false },
1119
- );
1120
- }
1121
- }
1122
- const operation: WorkspaceTargetOperation = classification.risk === 'readonly' ? 'read' : 'write';
1123
- const execute = async () => {
1124
- let resolved;
1125
- try {
1126
- resolved = resolveWorkspaceTargetCwd(
1127
- input.controllerHome,
1128
- targetKey,
1129
- requestOwnerScope(input),
1130
- optionalString(input.args, 'cwd') ?? '.',
1131
- operation,
1132
- currentDate(),
1133
- );
1134
- } catch (error) {
1135
- return rethrowTargetError(error);
1136
- }
1137
- assertCommandPathOperandsStayInRepository(canonical, resolved.path, resolved.root, []);
1138
- const maxOutputBytes = Math.max(1_024, Math.min(Math.trunc(Number(input.args.max_output_bytes ?? MAX_OUTPUT_BYTES)), 1024 * 1024));
1139
- const timeoutMs = Math.max(1_000, Math.min(Math.trunc(input.timeoutMs ?? 30_000), 30_000));
1140
- const executed = await runCanonicalCommand(canonical, resolved.path, timeoutMs, maxOutputBytes, { signal: input.signal });
1141
- if (!executed.ok) {
1142
- const code = executed.timedOut
1143
- ? 'LOCAL_SYSTEM_COMMAND_TIMED_OUT'
1144
- : executed.cancelled
1145
- ? 'LOCAL_SYSTEM_COMMAND_CANCELLED'
1146
- : 'LOCAL_SYSTEM_COMMAND_FAILED';
1147
- const message = executed.stderr.trim()
1148
- || executed.stdout.trim()
1149
- || `Target command exited with code ${executed.exitCode}.`;
1150
- throw new AssistantPluginError(code, bounded(message, maxOutputBytes).content, {
1151
- retryable: executed.timedOut,
1152
- details: {
1153
- command: argv,
1154
- exitCode: executed.exitCode,
1155
- timedOut: executed.timedOut,
1156
- cancelled: executed.cancelled,
1157
- },
1158
- });
1159
- }
1160
- return {
1161
- targetKey: resolved.target.targetKey,
1162
- workspaceId: resolved.target.workspaceId,
1163
- identityFingerprint: resolved.target.identityFingerprint,
1164
- repositoryRegistered: false,
1165
- cwd: resolved.relativePath || '.',
1166
- command: argv,
1167
- classification,
1168
- ok: executed.ok,
1169
- exitCode: executed.exitCode,
1170
- timedOut: executed.timedOut,
1171
- cancelled: executed.cancelled,
1172
- stdout: executed.stdout,
1173
- stderr: executed.stderr,
1174
- };
1175
- };
1176
- return operation === 'write'
1177
- ? await withTargetMutation(input, [targetKey], execute)
1178
- : await execute();
1179
- }
1180
1082
  case 'execute_project_script': {
1181
1083
  const targetKey = requiredString(input.args, 'target_key');
1182
1084
  const runtime = requiredString(input.args, 'runtime').toLowerCase();
@@ -2,9 +2,10 @@ import { createHash, randomUUID } from 'crypto';
2
2
  import { join } from 'path';
3
3
  import type { RepositoryRecord } from '../../cli/repositories/types';
4
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';
5
+ import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'fs';
6
6
  import type { ExecutionJob, ResourceClaimSpec } from '../execution/jobs/types';
7
7
  import { appendRuntimeEvent } from '../evidence/event-ledger';
8
+ import { redactSensitiveText, redactSensitiveValue } from '../evidence/sensitive-output';
8
9
  import { readJsonFile, sanitizeFileComponent, writeJsonAtomic } from '../shared/json-files';
9
10
  import { createFirstPartyPluginAdapterMap } from './first-party-registry';
10
11
  import { reconcileFirstPartyExternalPluginRegistration, reconcileFirstPartyExternalPluginRegistrations } from './first-party-external-registration';
@@ -444,6 +445,20 @@ export function migrateRepositoryPluginConfigLegacyFiles(
444
445
  join(repository.canonicalRoot, '.repo-harness', 'plugins'),
445
446
  ];
446
447
  mkdirSync(targetRoot, { recursive: true });
448
+ // Upgrade an already-adopted v1/v2 Controller Home config in place. The
449
+ // adapter owns schema semantics; this store remains the sole file writer.
450
+ const browserTarget = repositoryPluginConfigPath({ controllerHome, repoId: repository.repoId }, 'browser');
451
+ const browserNormalizer = PLUGIN_ADAPTERS.get('browser')?.normalizeLegacyConfig;
452
+ if (!browserNormalizer) throw new Error('PLUGIN_CONFIG_MIGRATION_INVALID: browser schema owner missing');
453
+ if (existsSync(browserTarget)) {
454
+ const stat = lstatSync(browserTarget);
455
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('PLUGIN_CONFIG_AUTHORITY_PATH_INVALID: browser config');
456
+ let current: unknown;
457
+ try { current = JSON.parse(readFileSync(browserTarget, 'utf8')); }
458
+ catch { throw new Error('PLUGIN_CONFIG_AUTHORITY_INVALID: browser config JSON'); }
459
+ const normalized = browserNormalizer(current);
460
+ if ((current as { schemaVersion?: unknown }).schemaVersion !== 3) writeJsonAtomic(browserTarget, normalized);
461
+ }
447
462
  for (const legacyRoot of legacyRoots) {
448
463
  if (!existsSync(legacyRoot)) continue;
449
464
  const stat = lstatSync(legacyRoot);
@@ -461,7 +476,16 @@ export function migrateRepositoryPluginConfigLegacyFiles(
461
476
  const sourceStat = lstatSync(source);
462
477
  if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) continue;
463
478
  const target = repositoryPluginConfigPath({ controllerHome, repoId: repository.repoId }, pluginId);
464
- if (!existsSync(target)) cpSync(source, target, { force: false, errorOnExist: false });
479
+ if (!existsSync(target)) {
480
+ if (pluginId === 'browser') {
481
+ let parsed: unknown;
482
+ try { parsed = JSON.parse(readFileSync(source, 'utf8')); }
483
+ catch { throw new Error('PLUGIN_CONFIG_MIGRATION_INVALID: legacy browser JSON'); }
484
+ writeJsonAtomic(target, browserNormalizer(parsed));
485
+ } else {
486
+ cpSync(source, target, { force: false, errorOnExist: false });
487
+ }
488
+ }
465
489
  rmSync(source, { force: true });
466
490
  }
467
491
  if (readdirSync(legacyRoot).length === 0) rmSync(legacyRoot, { recursive: true, force: true });
@@ -475,7 +499,6 @@ function computeManifest(
475
499
  pluginId: string,
476
500
  resolvedAdapter?: AssistantPluginAdapter,
477
501
  ): AssistantPluginManifest {
478
- migrateRepositoryPluginConfigLegacyFiles(controllerHome, repository);
479
502
  const adapter = resolvedAdapter ?? resolvePluginAdapter(controllerHome, pluginId);
480
503
  if (!adapter || !adapterMatchesRepository(adapter, repository)) throw new Error(`PLUGIN_NOT_FOUND: ${pluginId}`);
481
504
  const previous = readStoredManifest(controllerHome, repository.repoId, pluginId);
@@ -678,7 +701,18 @@ function enforceConfirmation(action: AssistantPluginActionDescriptor, request: A
678
701
  // Ordinary authorization is delegated to the host AI/tool permission model.
679
702
  // Forge keeps only the explicit strong-confirmation boundary for
680
703
  // destructive or irreversible plugin operations.
681
- if (action.confirmation === 'none' || action.confirmation === 'authorization') return;
704
+ if (action.confirmation === 'none') return;
705
+ if (action.confirmation === 'authorization') {
706
+ if (action.requiredConfirmationText) {
707
+ if (request.confirmAuthorization !== true) {
708
+ throw new Error(`PLUGIN_CONFIRMATION_REQUIRED: ${request.pluginId}/${request.actionId} requires confirmAuthorization=true`);
709
+ }
710
+ if (request.confirmationText !== action.requiredConfirmationText) {
711
+ throw new Error(`PLUGIN_CONFIRMATION_TEXT_REQUIRED: provide confirmationText=${action.requiredConfirmationText}`);
712
+ }
713
+ }
714
+ return;
715
+ }
682
716
  if (action.confirmation === 'strong_confirmation') {
683
717
  if (request.confirmAuthorization !== true) {
684
718
  throw new Error(`PLUGIN_CONFIRMATION_REQUIRED: ${request.pluginId}/${request.actionId} requires confirmAuthorization=true`);
@@ -922,6 +956,7 @@ export function syncAssistantPluginRegistry(
922
956
  repository: RepositoryRecord,
923
957
  ): { manifests: AssistantPluginManifest[]; index: AssistantPluginRegistryIndex } {
924
958
  invalidateAssistantPluginManifestCache(controllerHome, repository.repoId);
959
+ migrateRepositoryPluginConfigLegacyFiles(controllerHome, repository);
925
960
  const manifests = listAssistantPluginManifests(controllerHome, repository, { forceRefresh: true });
926
961
  for (const manifest of manifests) {
927
962
  writeJsonAtomic(manifestPath(controllerHome, repository.repoId, manifest.pluginId), manifest);
@@ -939,6 +974,7 @@ export function syncAssistantPluginManifest(
939
974
  pluginId: string,
940
975
  ): { manifest: AssistantPluginManifest; index: AssistantPluginRegistryIndex } {
941
976
  invalidateAssistantPluginManifestCache(controllerHome, repository.repoId, pluginId);
977
+ migrateRepositoryPluginConfigLegacyFiles(controllerHome, repository);
942
978
  const manifest = computeManifest(controllerHome, repository, pluginId);
943
979
  writeJsonAtomic(manifestPath(controllerHome, repository.repoId, manifest.pluginId), manifest);
944
980
  markControllerContextProjectionDirty(repository.canonicalRoot, `plugin:${pluginId}:synced`);
@@ -1180,7 +1216,7 @@ export interface PluginActionReceipt {
1180
1216
  origin?: { surface: string; actor?: string; correlationId?: string };
1181
1217
  authorization?: PluginActionAuthorizationEvidence;
1182
1218
  result?: Record<string, unknown>;
1183
- error?: { code: string; message: string };
1219
+ error?: { code: string; message: string; retryable?: boolean; details?: Record<string, unknown> };
1184
1220
  /** True for a compact direct-read observation; it is evidence, never an effect-completion authority. */
1185
1221
  observationOnly?: boolean;
1186
1222
  /** SHA-256 of the direct-read result. Result content is intentionally not persisted for observation-only receipts. */
@@ -1345,6 +1381,21 @@ function attributedWorkForPluginAction(
1345
1381
  return work;
1346
1382
  }
1347
1383
 
1384
+ /** Reuse the canonical Work binding check before a Process request ID is reserved. */
1385
+ export function preflightPluginActionWorkBinding(
1386
+ controllerHome: string,
1387
+ pluginScope: { kind: 'controller' } | { kind: 'repository'; repository: RepositoryRecord },
1388
+ action: AssistantPluginActionDescriptor,
1389
+ request: AssistantPluginActionRequest,
1390
+ ): void {
1391
+ if (!request.workId?.trim()) return;
1392
+ const scope = pluginScope.kind === 'controller'
1393
+ ? controllerPluginActionScope(controllerHome)
1394
+ : repositoryPluginActionScope(pluginScope.repository);
1395
+ if (action.risk === 'remote_write') remoteEffectWorkForPluginAction(controllerHome, scope, action, request);
1396
+ else attributedWorkForPluginAction(controllerHome, scope, action, request);
1397
+ }
1398
+
1348
1399
  function bindLocalEffectReceiptToAttributedWork(
1349
1400
  controllerHome: string,
1350
1401
  scope: PluginActionScopeContext,
@@ -1669,6 +1720,13 @@ async function submitPluginAction(
1669
1720
  }
1670
1721
  }
1671
1722
 
1723
+ if (action.requiresExplicitCapabilityGrant && !activeAuthorizationGrant) {
1724
+ throw new AssistantPluginError(
1725
+ 'PLUGIN_CAPABILITY_GRANT_REQUIRED',
1726
+ `${manifest.pluginId}/${action.actionId} requires an explicit reusable capability grant for its exact target. Run the dedicated authorization action first.`,
1727
+ );
1728
+ }
1729
+
1672
1730
  const result = await withAssistantPluginResourceLeases(
1673
1731
  controllerHome,
1674
1732
  scope,
@@ -1820,8 +1878,23 @@ async function submitPluginAction(
1820
1878
  ? error.effectOutcome
1821
1879
  : 'failed';
1822
1880
  const outcomeUnknown = effectOutcome === 'outcome_unknown';
1881
+ const failure: NonNullable<PluginActionReceipt['error']> = {
1882
+ code,
1883
+ message: redactSensitiveText(message).text,
1884
+ ...(isAssistantPluginError(error) ? { retryable: error.retryable } : {}),
1885
+ };
1886
+ // Error evidence stays compact; oversized provider diagnostics must not
1887
+ // turn failure settlement into an unbounded persistence write.
1888
+ if (isAssistantPluginError(error) && error.details) {
1889
+ try {
1890
+ const details = redactSensitiveValue(error.details).value;
1891
+ if (Buffer.byteLength(JSON.stringify(details), 'utf8') <= 8_192) failure.details = details;
1892
+ } catch {
1893
+ // Non-JSON provider diagnostics cannot override the settled failure.
1894
+ }
1895
+ }
1823
1896
  const outcomeResult = outcomeUnknown
1824
- ? { outcome: 'outcome_unknown', error: { code, message } }
1897
+ ? { outcome: 'outcome_unknown', error: failure }
1825
1898
  : undefined;
1826
1899
  if (boundRemoteWork) {
1827
1900
  appendWorkEvidence({ controllerHome, repoId: workAttributionRepoId(scope, request) }, boundRemoteWork.workId, {
@@ -1851,7 +1924,7 @@ async function submitPluginAction(
1851
1924
  origin: request.origin,
1852
1925
  authorization,
1853
1926
  ...(outcomeResult ? { result: outcomeResult } : {}),
1854
- error: { code, message },
1927
+ error: failure,
1855
1928
  };
1856
1929
  writeJsonAtomic(pluginActionReceiptPath(controllerHome, scope.scopeKey, receiptId), receipt);
1857
1930
  writeJsonAtomic(requestPath, {
@@ -50,6 +50,8 @@ export interface AssistantPluginActionDescriptor {
50
50
  readOnly: boolean;
51
51
  risk: AssistantPluginActionRisk;
52
52
  confirmation: AssistantPluginActionConfirmation;
53
+ /** Requires an existing explicit reusable grant; this action cannot implicitly create its own grant. */
54
+ requiresExplicitCapabilityGrant?: boolean;
53
55
  requiredConfirmationText?: string;
54
56
  defaultTimeoutMs: number;
55
57
  cancellable: boolean;
@@ -194,6 +196,8 @@ export interface AssistantPluginAdapter {
194
196
  /** Internal adapters remain exactly addressable by trusted composition but are omitted from normal user-facing plugin listings. */
195
197
  exposure?: AssistantPluginExposure;
196
198
  buildManifest(previousRevision?: number, previousUpdatedAt?: string, repoRoot?: string, context?: AssistantPluginBuildContext): AssistantPluginManifest;
199
+ /** Pure plugin-owned schema conversion/validation; the config store alone commits migrated bytes. */
200
+ normalizeLegacyConfig?(raw: unknown): object;
197
201
  /** Resolve an exact stable resource identity for reusable authorization-class actions. */
198
202
  resolveAuthorizationContext?(input: AssistantPluginActionExecutionInput): Promise<AssistantPluginAuthorizationContext | undefined>;
199
203
  executeAction(input: AssistantPluginActionExecutionInput): Promise<Record<string, unknown>>;
@@ -1,7 +1,7 @@
1
1
  import { join } from 'path';
2
2
  import { repositoryControllerRoot } from '../../cli/repositories/controller-home';
3
3
  import { selectRepositoryCheckout } from '../../cli/repositories/registry';
4
- import type { RepositoryGitStatusSnapshot } from '../../cli/repositories/structured-git';
4
+ import { splitRepositoryGitStatus, type RepositoryGitStatusSnapshot } from '../../cli/repositories/structured-git';
5
5
  import type { RepositoryRecord } from '../../cli/repositories/types';
6
6
  import { runBoundedGit } from '../execution/thin-harness/async-process';
7
7
  import { readJsonFile, sanitizeFileComponent, writeJsonAtomic } from '../shared/json-files';
@@ -23,23 +23,6 @@ function samplePath(controllerHome: string, repoId: string, checkoutId: string):
23
23
  const GIT_STATUS_SAMPLE_TIMEOUT_MS = 5_000;
24
24
  const GIT_STATUS_SAMPLE_MAX_BYTES = 512 * 1024;
25
25
 
26
- function splitStatus(porcelain: string): Pick<RepositoryGitStatusSnapshot, 'staged' | 'unstaged' | 'untracked'> {
27
- const staged: string[] = [];
28
- const unstaged: string[] = [];
29
- const untracked: string[] = [];
30
- for (const line of porcelain.split(/\r?\n/)) {
31
- if (!line || line.startsWith('## ')) continue;
32
- const code = line.slice(0, 2);
33
- const path = line.slice(3).trim();
34
- if (code === '??') untracked.push(path);
35
- else {
36
- if (code[0] !== ' ' && code[0] !== '?') staged.push(path);
37
- if (code[1] !== ' ' && code[1] !== '?') unstaged.push(path);
38
- }
39
- }
40
- return { staged, unstaged, untracked };
41
- }
42
-
43
26
  async function gitSample(repository: RepositoryRecord, args: readonly string[], maxOutputBytes = GIT_STATUS_SAMPLE_MAX_BYTES) {
44
27
  return runBoundedGit(repository.canonicalRoot, args, {
45
28
  timeoutMs: GIT_STATUS_SAMPLE_TIMEOUT_MS,
@@ -63,7 +46,7 @@ export async function writeRepositoryGitStatusSample(
63
46
  throw new Error(`GIT_STATUS_SAMPLE_FAILED: ${porcelainResult.stderr || `exit ${porcelainResult.exitCode}`}`);
64
47
  }
65
48
  const porcelain = porcelainResult.stdout;
66
- const split = splitStatus(porcelain);
49
+ const split = splitRepositoryGitStatus(porcelain);
67
50
  const sample: RepositoryGitStatusSample = {
68
51
  repoId: repository.repoId,
69
52
  checkoutId: repository.activeCheckoutId,
@@ -44,7 +44,7 @@ export const RECOVERY_ACTIONS = {
44
44
  localJobsReconcile: {
45
45
  id: 'recovery.local_jobs_reconcile',
46
46
  title: 'Reconcile Local Jobs',
47
- description: 'Terminalize stale active Local Jobs and rebuild the Local Bridge active index without using repository_command_execute.',
47
+ description: 'Terminalize stale active Local Jobs and rebuild the Local Bridge active index without invoking execute_command.',
48
48
  class: 'local_jobs_reconciliation_required',
49
49
  risk: 'low',
50
50
  confirmation: 'authorization',
@@ -169,7 +169,7 @@ export const RUNTIME_LIFECYCLE_INVENTORY: readonly RuntimeLifecycleClassDefiniti
169
169
  retentionCapacity: 'Global maximum, lease expiry and absolute lifetime bound stream-only sessions.',
170
170
  cleanupAuthority: 'MCP transport registry lifecycle.',
171
171
  recoverySemantics: 'Closing transport state never cancels or completes a durably accepted Job.',
172
- closureStatus: 'existing_bounded', evidencePaths: ['docs/architecture/modules/controller-runtime/transport-lifecycle.md','src/cli/mcp/transports'],
172
+ closureStatus: 'existing_bounded', evidencePaths: ['docs/architecture/modules/controller-runtime/transport-lifecycle.md','adapters/mcp/transports'],
173
173
  }),
174
174
  lifecycle({
175
175
  id: 'browser_session_profile', owner: 'Browser session/provider authority', scope: 'provider', dataClass: 'provider_artifact',
@@ -241,16 +241,6 @@ export const RUNTIME_LIFECYCLE_INVENTORY: readonly RuntimeLifecycleClassDefiniti
241
241
  recoverySemantics: 'Quarantine remains diagnostic evidence, never a shadow active authority; legacy symlinks are relocated without traversal, and repeated maintenance is idempotent and fail-closed on unreadable evidence.',
242
242
  closureStatus: 'existing_bounded', evidencePaths: ['src/runtime/recovery/quarantine-retention.ts','src/runtime/recovery/maintenance-executor.ts','src/runtime/recovery/local-jobs-repair.ts','tests/runtime/capability-recovery.test.ts'],
243
243
  }),
244
- lifecycle({
245
- id: 'codegraph_cache', owner: 'Context / CodeGraph cache lifecycle', scope: 'repository', dataClass: 'cache',
246
- storage: 'Controller Home tool-cache/codegraph or explicit external cache root',
247
- terminalCondition: 'Index is stale, superseded, repository-retired or evicted as rebuildable cache.',
248
- activeProtection: 'Only a live process-scoped Forge locator fences Controller Home cache deletion; unreadable, truncated, or ambiguous locator evidence fails closed. Raw source remains authority when cache is missing/stale.',
249
- retentionCapacity: 'Forge-owned Controller Home indexes use a 7-day default idle grace, 512 MiB per-repository cap, 2 GiB Controller aggregate cap, and bounded oldest-first scan/removal budgets. Inactive caches may be rebuilt and evicted under age or capacity pressure; explicit external cache roots remain outside Forge cleanup authority.',
250
- cleanupAuthority: 'Central Runtime cleanup owns rebuildable Controller Home CodeGraph eviction; no semantic authority is deleted and non-owned/symlink entries fail closed.',
251
- recoverySemantics: 'Missing/stale cache degrades explicitly to lexical/raw-source evidence and rebuilds on demand; removal is idempotent and bounded by the shared cleanup budget.',
252
- closureStatus: 'existing_bounded', evidencePaths: ['src/runtime/context/codegraph-cache-boundary.ts','src/runtime/control-plane/codegraph-cache-retention.ts','src/runtime/control-plane/runtime-cleanup.ts','tests/runtime/codegraph-cache-retention.test.ts'],
253
- }),
254
244
  lifecycle({
255
245
  id: 'operational_memory', owner: 'Kernel Memory shadow reducer', scope: 'controller', dataClass: 'derived',
256
246
  storage: 'Controller Home derived operational Memory records backed by retained Process evidence',
@@ -205,7 +205,7 @@ export function buildCapabilityRecoverySnapshot(input: CapabilityRecoveryInput):
205
205
  recentClass === 'platform_blocked'
206
206
  ? 'Command execute appears blocked before reaching forge. Do not restart-loop local services.'
207
207
  : ['runtime_storage_not_ready', 'local_jobs_legacy_active', 'local_jobs_unreadable', 'local_jobs_reconciliation_required'].includes(recentClass)
208
- ? 'Command execute is blocked by forge runtime storage; use the maintenance executor instead of repository_command_execute.'
208
+ ? 'Command execute is blocked by forge runtime storage; use the maintenance executor instead of execute_command.'
209
209
  : 'Command execute is blocked, denied, or unavailable.',
210
210
  commandExecuteActions(recentClass),
211
211
  )
@@ -300,7 +300,7 @@ export function buildCapabilityRecoverySnapshot(input: CapabilityRecoveryInput):
300
300
  ? ['Runtime lifecycle action is required outside the running Runtime. Use the existing single forge-runtime service only; no rollout, second Runtime, component restart, or source mutation.']
301
301
  : platformBlocked
302
302
  ? ['One or more calls appear blocked before reaching forge. Use patch handoff or narrower typed tools; do not infer a local Runtime lifecycle failure.']
303
- : (input.runtimeStorageReady === false ? ['Runtime storage is not ready. Use runtime_maintenance_status/runtime_maintenance_apply; do not try to repair repository_command_execute with repository_command_execute.'] : []),
303
+ : (input.runtimeStorageReady === false ? ['Runtime storage is not ready. Use runtime_maintenance_status/runtime_maintenance_apply; do not try to repair execute_command with execute_command.'] : []),
304
304
  runtimeHealth: sharedHealth,
305
305
  runtimeOperationalView: input.runtimeOperationalView,
306
306
  };
@@ -145,7 +145,12 @@ export function decideConfiguredRuntimeReleaseReconciliation(
145
145
  const sessions = completeRuntimeReleaseSessions(controllerHome);
146
146
  const active = sessions.filter((session) => !releaseSessionIsTerminal(session));
147
147
  const session = coordinatorSession(active);
148
- if (session) {
148
+ // A single soaking predecessor remains rollback authority, but inability to
149
+ // certify it must not forever prevent a newer immutable source revision from
150
+ // preparing its own Candidate B. Two active sessions are allowed only by the
151
+ // existing exact soaking-predecessor/successor placement contract.
152
+ const onlySoakingPredecessor = active.length === 1 && session?.phase === 'soaking';
153
+ if (session && !onlySoakingPredecessor) {
149
154
  return { required: true, reason: 'active_session', action: runtimeReleaseActionForSession(session), session };
150
155
  }
151
156
 
@@ -153,12 +158,16 @@ export function decideConfiguredRuntimeReleaseReconciliation(
153
158
  // ReleaseSession can progress entirely from its durable authority without
154
159
  // periodic repository/process churn in the Recovery daemon.
155
160
  const source = observeSource();
156
- if (!source.configured) return { required: false, reason: 'source_not_configured' };
161
+ if (!source.configured) return session
162
+ ? { required: true, reason: 'active_session', action: runtimeReleaseActionForSession(session), session }
163
+ : { required: false, reason: 'source_not_configured' };
157
164
  const sourceRevision = source.sourceRevision?.trim();
158
165
  const activeSourceCommit = source.activeSourceCommit?.trim();
159
166
  if (!sourceRevision) throw new Error('RELEASE_AUTOMATION_SOURCE_REVISION_UNKNOWN');
160
167
  if (!activeSourceCommit) throw new Error('RELEASE_AUTOMATION_ACTIVE_SOURCE_COMMIT_UNKNOWN');
161
- if (sourceRevision === activeSourceCommit) return { required: false, reason: 'source_current' };
168
+ if (sourceRevision === activeSourceCommit) return session
169
+ ? { required: true, reason: 'active_session', action: runtimeReleaseActionForSession(session), session }
170
+ : { required: false, reason: 'source_current' };
162
171
 
163
172
  // An autonomous release is one attempt per immutable source revision.
164
173
  // A terminal ReleaseSession is durable evidence that this exact commit was
@@ -187,8 +196,15 @@ export async function advanceConfiguredRuntimeReleaseStep<C extends RuntimeRelea
187
196
  config: C,
188
197
  provider: RuntimeReleaseProvider<C>,
189
198
  requestId?: string,
199
+ reconciliation?: RuntimeReleaseReconciliationDecision,
190
200
  ): Promise<RuntimeReleaseProgressResult> {
191
- const decision = decideConfiguredRuntimeReleaseAction(config.controllerHome);
201
+ // The Coordinator's source comparison may select a new Candidate while the
202
+ // one allowable soaking predecessor stays open. Provider.prepare itself
203
+ // revalidates source/Stable A under the existing Recovery mutation lock.
204
+ const decision: RuntimeReleaseCoordinatorDecision = reconciliation?.required
205
+ && reconciliation.reason === 'source_mismatch' && reconciliation.action === 'prepare'
206
+ ? { action: 'prepare' }
207
+ : decideConfiguredRuntimeReleaseAction(config.controllerHome);
192
208
  const request = requestId?.trim();
193
209
  switch (decision.action) {
194
210
  case 'prepare':
@@ -1,6 +1,8 @@
1
1
  import {
2
2
  beginControllerRoundRelayAfterRelease,
3
+ bindControllerSessionToCurrentRuntime,
3
4
  controllerRoundBlockerClass,
5
+ controllerSessionPrincipalId,
4
6
  getControllerRoundRelay,
5
7
  getControllerWorkBinding,
6
8
  getRetainedControllerSession,
@@ -8,6 +10,7 @@ import {
8
10
  resumeControllerRoundOccurrence,
9
11
  type ControllerBinding,
10
12
  type ControllerHost,
13
+ type ControllerSession,
11
14
  } from '../../../packages/kernel/controller/api/index';
12
15
  import { getWorkContract, semanticWorkState } from '../../../packages/kernel/work/api/index';
13
16
  import {
@@ -16,8 +19,10 @@ import {
16
19
  } from './scheduled-controller-composition';
17
20
  import {
18
21
  ensureWorkflowSupervisorEnrollmentForWork,
22
+ wakeWorkflowSupervisorScheduledTaskForWork,
19
23
  type WorkflowSupervisorEnrollmentStatus,
20
24
  } from './workflow-supervisor-composition';
25
+ import { getRuntimeWriteClaim } from './write-fence';
21
26
 
22
27
  export interface ControllerProgressionInput {
23
28
  workId: string;
@@ -26,6 +31,8 @@ export interface ControllerProgressionInput {
26
31
  continuationHint?: string;
27
32
  allowSemanticWaitRecovery?: boolean;
28
33
  scheduleName?: string;
34
+ scheduleId?: string;
35
+ scheduleOccurrenceId?: string;
29
36
  bindingArgs?: Record<string, unknown>;
30
37
  }
31
38
 
@@ -35,6 +42,7 @@ export interface ControllerProgressionDependencies {
35
42
  prepareOccurrence?: typeof prepareControllerRoundOccurrence;
36
43
  resumeOccurrence?: typeof resumeControllerRoundOccurrence;
37
44
  ensureSupervisorEnrollment?: typeof ensureWorkflowSupervisorEnrollmentForWork;
45
+ wakeScheduledSupervisorTask?: typeof wakeWorkflowSupervisorScheduledTaskForWork;
38
46
  }
39
47
 
40
48
  export type ControllerProgressionResult =
@@ -56,6 +64,12 @@ export type ControllerProgressionResult =
56
64
  reason?: string;
57
65
  };
58
66
  }
67
+ | {
68
+ status: 'scheduled_supervisor_woken';
69
+ binding: ControllerBinding;
70
+ taskId: string;
71
+ effectId: string;
72
+ }
59
73
  | {
60
74
  status: 'chatgpt_not_enrolled';
61
75
  binding: ControllerBinding;
@@ -85,6 +99,27 @@ export type ControllerProgressionResult =
85
99
  * exclusively reserves and delivers Work-bound ChatGPT effects. Non-ChatGPT
86
100
  * ControllerHost remains the direct provider boundary.
87
101
  */
102
+ function rebindRetainedSessionToCurrentRuntime(
103
+ options: { controllerHome: string; repoId: string },
104
+ session: ControllerSession,
105
+ ): ControllerSession {
106
+ const currentRuntimeInstanceId = getRuntimeWriteClaim()?.runtimeInstanceId?.trim();
107
+ if (!currentRuntimeInstanceId || currentRuntimeInstanceId === session.controllerInstanceId?.trim()) return session;
108
+ return bindControllerSessionToCurrentRuntime({
109
+ controllerHome: options.controllerHome,
110
+ repoId: options.repoId,
111
+ }, {
112
+ workId: session.workId,
113
+ controllerId: session.controllerId,
114
+ controllerType: session.controllerType,
115
+ sessionId: session.sessionId,
116
+ principalId: controllerSessionPrincipalId(session),
117
+ controllerInstanceId: currentRuntimeInstanceId,
118
+ currentRuntimeInstanceId,
119
+ allowClaimIfMissing: true,
120
+ });
121
+ }
122
+
88
123
  export async function reconcileControllerProgression(
89
124
  options: { controllerHome: string; repoId: string; repoRoot: string },
90
125
  input: ControllerProgressionInput,
@@ -100,18 +135,45 @@ export async function reconcileControllerProgression(
100
135
  if (!retainedSession) return { status: 'retained_session_missing', reason: 'CONTROLLER_SESSION_NOT_RETAINED' };
101
136
  if (retainedSession.controllerType === 'human') return { status: 'human_controller', reason: 'HUMAN_CONTROLLER' };
102
137
 
138
+ let existingRound = retainedSession.controllerType === 'chatgpt'
139
+ ? getControllerRoundRelay(store, input.workId)
140
+ : undefined;
141
+ if (existingRound?.status === 'pending_release') {
142
+ // The release fence belongs to the previous Controller epoch. Consume it
143
+ // before migrating the retained session to the new Runtime epoch.
144
+ existingRound = beginControllerRoundRelayAfterRelease(store, {
145
+ workId: input.workId,
146
+ releasedSession: retainedSession,
147
+ }) ?? getControllerRoundRelay(store, input.workId);
148
+ }
149
+ const session = rebindRetainedSessionToCurrentRuntime(store, retainedSession);
150
+
103
151
  const ensureBinding = dependencies.ensureBinding ?? ensureScheduledControllerBindingForWork;
104
152
  const existingBinding = getControllerWorkBinding(store, input.workId)?.binding;
105
153
  const binding = existingBinding ?? ensureBinding(store, {
106
154
  workId: input.workId,
107
- session: retainedSession,
155
+ session,
108
156
  scheduleName: input.scheduleName,
109
157
  args: input.bindingArgs ?? {},
110
158
  });
111
159
 
112
- if (retainedSession.controllerType === 'chatgpt') {
160
+ if (session.controllerType === 'chatgpt' && input.scheduleId && input.scheduleOccurrenceId) {
161
+ const wakeScheduledSupervisorTask = dependencies.wakeScheduledSupervisorTask ?? wakeWorkflowSupervisorScheduledTaskForWork;
162
+ const wake = await wakeScheduledSupervisorTask(
163
+ store, input.workId, input.scheduleId, input.scheduleOccurrenceId,
164
+ );
165
+ if (wake.status === 'woken' && wake.taskId && wake.effectId) {
166
+ return {
167
+ status: 'scheduled_supervisor_woken',
168
+ binding,
169
+ taskId: wake.taskId,
170
+ effectId: wake.effectId,
171
+ };
172
+ }
173
+ }
174
+
175
+ if (session.controllerType === 'chatgpt') {
113
176
  const ensureSupervisorEnrollment = dependencies.ensureSupervisorEnrollment ?? ensureWorkflowSupervisorEnrollmentForWork;
114
- let existingRound = getControllerRoundRelay(store, input.workId);
115
177
  let controllerRoundOccurrenceId = input.occurrenceId;
116
178
  if (existingRound?.status === 'pending_release') {
117
179
  // The semantic turn already chose continue_immediately. Once its exact
@@ -134,7 +196,7 @@ export async function reconcileControllerProgression(
134
196
  }
135
197
  if (existingRound?.status === 'blocked'
136
198
  && controllerRoundBlockerClass(existingRound) === 'provider_dispatch_outcome_unknown') {
137
- const supervisorEnrollment = await ensureSupervisorEnrollment(store, input.workId);
199
+ const supervisorEnrollment = await ensureSupervisorEnrollment(store, input.workId, input.scheduleId, input.scheduleOccurrenceId);
138
200
  return {
139
201
  status: supervisorEnrollment.status === 'enrolled' ? 'chatgpt_enrolled' : 'chatgpt_not_enrolled',
140
202
  binding,
@@ -157,7 +219,7 @@ export async function reconcileControllerProgression(
157
219
  if (prepared.outcome === 'wait_for_user') return { status: 'wait_for_user', reason: prepared.reason, binding };
158
220
  if (prepared.outcome === 'rejected') return { status: 'rejected', reason: prepared.reason, binding };
159
221
 
160
- const supervisorEnrollment = await ensureSupervisorEnrollment(store, input.workId);
222
+ const supervisorEnrollment = await ensureSupervisorEnrollment(store, input.workId, input.scheduleId, input.scheduleOccurrenceId);
161
223
  return {
162
224
  status: supervisorEnrollment.status === 'enrolled' ? 'chatgpt_enrolled' : 'chatgpt_not_enrolled',
163
225
  binding,