@moretea-labs/forge 1.8.1 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (328) hide show
  1. package/AGENTS.md +5 -2
  2. package/CHANGELOG.md +20 -0
  3. package/README.en.md +24 -3
  4. package/README.md +10 -6
  5. package/README.zh-CN.md +5 -3
  6. package/THIRD_PARTY_NOTICES.md +1 -2
  7. package/adapters/chatgpt/browser-delivery-runtime.ts +5 -3
  8. package/adapters/chatgpt/provider-delivery.ts +1 -1
  9. package/adapters/chatgpt/work-conversation-binding-store.ts +20 -1
  10. package/adapters/computer/chatgpt-extension-target.ts +295 -50
  11. package/adapters/computer/chrome-extension/background.js +227 -71
  12. package/adapters/computer/chrome-extension/content.js +181 -157
  13. package/adapters/computer/chrome-extension/manifest.json +3 -1
  14. package/adapters/computer/desktop-operator-provider.ts +18 -6
  15. package/adapters/computer/interaction-target-authority.ts +39 -9
  16. package/adapters/mcp/access-mode.ts +18 -39
  17. package/adapters/mcp/auth.ts +2 -6
  18. package/adapters/mcp/instructions.ts +2 -2
  19. package/adapters/mcp/multi-repository.ts +7 -28
  20. package/adapters/mcp/runtime-gateway/context-adapter.ts +79 -40
  21. package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +7 -15
  22. package/adapters/mcp/runtime-gateway/execution-tools.ts +7 -13
  23. package/adapters/mcp/runtime-gateway/plugin-adapter.ts +16 -3
  24. package/adapters/mcp/runtime-gateway/process-tools.ts +3 -21
  25. package/adapters/mcp/runtime-gateway/router.ts +3 -5
  26. package/adapters/mcp/runtime-gateway/routing-policy.ts +17 -10
  27. package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1 -1
  28. package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +14 -9
  29. package/adapters/mcp/runtime-gateway/runtime-tools.ts +1 -5
  30. package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +36 -1
  31. package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +58 -4
  32. package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +7 -3
  33. package/adapters/mcp/runtime-gateway/work-adapter.ts +16 -16
  34. package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +0 -17
  35. package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +69 -12
  36. package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +26 -2
  37. package/adapters/mcp/server.ts +24 -3
  38. package/adapters/mcp/setup.ts +4 -10
  39. package/adapters/mcp/tool-mapping/access-tools.ts +0 -6
  40. package/adapters/mcp/tool-mapping/legacy-tool-service.ts +672 -2935
  41. package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +1 -1
  42. package/adapters/mcp/tool-mapping/repository-tools.ts +52 -86
  43. package/adapters/mcp/toolset-names.ts +11 -42
  44. package/adapters/mcp/toolset.ts +16 -90
  45. package/{src/cli → adapters}/mcp/transports/AGENTS.md +5 -5
  46. package/{src/cli → adapters}/mcp/transports/CLAUDE.md +5 -5
  47. package/adapters/mcp/transports/http-observation.ts +18 -6
  48. package/adapters/mcp/transports/http-post-lifecycle.ts +19 -0
  49. package/adapters/mcp/transports/http.ts +54 -14
  50. package/adapters/mcp/types.ts +0 -6
  51. package/assets/hooks/lib/session-state.sh +0 -38
  52. package/assets/hooks/post-bash.sh +1 -1
  53. package/assets/hooks/post-tool-observer.sh +0 -5
  54. package/assets/hooks/prompt-guard.sh +0 -55
  55. package/assets/partials/04-project-structure.partial.md +1 -1
  56. package/assets/partials/07-footer.partial.md +1 -1
  57. package/assets/partials-agents/02-operating-mode.partial.md +1 -1
  58. package/assets/partials-agents/08-deep-docs.partial.md +1 -1
  59. package/assets/reference-configs/agentic-development-flow.md +1 -1
  60. package/assets/reference-configs/external-tooling.md +0 -76
  61. package/assets/reference-configs/release-deploy.md +1 -1
  62. package/assets/templates/helpers/architecture-queue.sh +1 -1
  63. package/assets/templates/helpers/check-agent-tooling.sh +1 -327
  64. package/assets/templates/helpers/check-task-workflow.sh +1 -1
  65. package/assets/templates/helpers/ensure-task-workflow.sh +0 -14
  66. package/assets/templates/helpers/migrate-project-template.sh +2 -2
  67. package/assets/workflow-contract.v1.json +0 -2
  68. package/docs/README.md +5 -2
  69. package/docs/architecture/CURRENT.md +114 -30
  70. package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +42 -0
  71. package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +15 -2
  72. package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +4 -2
  73. package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +109 -4
  74. package/docs/architecture/decisions/20261009-exact-code-context-discovery.md +19 -0
  75. package/docs/architecture/decisions/20261009-generated-xcode-cache-retention.md +37 -0
  76. package/docs/architecture/decisions/20261011-autonomous-continuation-convergence.md +597 -0
  77. package/docs/architecture/decisions/20261017-edit-session-finalization-reconciliation.md +17 -0
  78. package/docs/architecture/decisions/20261019-retire-host-skills.md +16 -0
  79. package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +1 -1
  80. package/docs/architecture/domains/public-surface.md +3 -5
  81. package/docs/architecture/domains/verification.md +5 -5
  82. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +2 -2
  83. package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +4 -4
  84. package/docs/architecture/reviews/20261015-code-search-runtime-acceptance.md +75 -0
  85. package/docs/forge-plugin-management.md +14 -2
  86. package/docs/operations/controller-browser-plugin.md +23 -0
  87. package/docs/operations/features.md +6 -7
  88. package/docs/operations/features.zh-CN.md +1 -1
  89. package/docs/operations/github-repository.md +4 -4
  90. package/docs/operations/github-semantic-release-projection.md +20 -0
  91. package/docs/operations/homebrew.md +2 -2
  92. package/docs/operations/mcp-tool-exposure.md +13 -54
  93. package/docs/operations/runtime-performance-diagnostics.md +8 -0
  94. package/docs/operations/standalone-disaster-recovery.md +1 -1
  95. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  96. package/docs/tutorials/03-first-repository-task.md +2 -0
  97. package/docs/wiki/Computer-and-Plugins.md +78 -0
  98. package/docs/wiki/Computer-and-Plugins.zh-CN.md +88 -0
  99. package/docs/wiki/Home.md +16 -2
  100. package/docs/wiki/Implementation.md +24 -12
  101. package/docs/wiki/Installation.md +28 -32
  102. package/docs/wiki/Integrations.md +4 -0
  103. package/docs/wiki/Quick-Start.md +35 -22
  104. package/docs/wiki/Releases-and-Upgrades.md +15 -25
  105. package/docs/wiki/_Sidebar.md +5 -1
  106. package/package.json +1 -12
  107. package/packages/kernel/controller/infrastructure/controller-session-store.ts +63 -38
  108. package/packages/kernel/identity/infrastructure/activity-projection-store.ts +10 -0
  109. package/packages/kernel/scheduler/application/eligibility.ts +4 -1
  110. package/packages/kernel/scheduler/infrastructure/schedule-store.ts +13 -10
  111. package/packages/kernel/work/application/work-service.ts +1 -0
  112. package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
  113. package/packages/kernel/work/domain/state-machine.ts +9 -2
  114. package/packages/kernel/work/infrastructure/work-contract-store.ts +28 -1
  115. package/packages/plugin-runtime/browser/provider-registry.ts +12 -36
  116. package/packages/plugin-runtime/computer/conversation-target.ts +17 -7
  117. package/scripts/assemble-template.ts +11 -72
  118. package/scripts/check-mcp-compatibility.ts +55 -74
  119. package/scripts/check-package-identity.mjs +0 -5
  120. package/scripts/check-runtime-architecture.mjs +305 -28
  121. package/scripts/init-project.sh +0 -46
  122. package/scripts/install-ios-hid-toolchain.sh +60 -0
  123. package/scripts/install-standalone-recovery.ts +1 -1
  124. package/scripts/lib/project-init-lib.sh +3 -3
  125. package/scripts/migrate-windows-wsl-controller-home.ts +2 -0
  126. package/scripts/open-source-audit-allowlist.txt +0 -1
  127. package/scripts/run-governed-gate.ts +2 -0
  128. package/scripts/smoke-ios-physical-device.ts +38 -10
  129. package/scripts/smoke-mcp-http-runtime.ts +7 -8
  130. package/scripts/smoke-mcp-tool-surface.ts +27 -41
  131. package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +6 -3
  132. package/scripts/sync-generated-authority.mjs +0 -1
  133. package/src/cli/agent-jobs/executor-health.ts +1 -1
  134. package/src/cli/agent-jobs/integration.ts +2 -2
  135. package/src/cli/chatgpt-browser/file-policy.ts +2 -2
  136. package/src/cli/commands/doctor.ts +1 -143
  137. package/src/cli/commands/global-runtime.ts +3 -196
  138. package/src/cli/commands/init-hook.ts +6 -23
  139. package/src/cli/commands/init.ts +3 -252
  140. package/src/cli/commands/mcp.ts +0 -5
  141. package/src/cli/commands/prompt-guard-decision.ts +0 -2
  142. package/src/cli/commands/recovery.ts +2 -14
  143. package/src/cli/commands/repository.ts +17 -1
  144. package/src/cli/commands/setup-execution.ts +41 -0
  145. package/src/cli/controller/completion-orchestrator.ts +1 -1
  146. package/src/cli/controller/composite-operations.ts +1 -1
  147. package/src/cli/controller/context/instruction-context.ts +1 -1
  148. package/src/cli/controller/context/known-paths.ts +2 -2
  149. package/src/cli/controller/context/query-planning.ts +11 -4
  150. package/src/cli/controller/context/rule-resolution.ts +2 -2
  151. package/src/cli/controller/context/source-materializer.ts +2 -2
  152. package/src/cli/controller/context/structural-provider.ts +84 -0
  153. package/src/cli/controller/context/structural.ts +6 -6
  154. package/src/cli/controller/context/types.ts +8 -9
  155. package/src/cli/controller/context-pack.ts +97 -76
  156. package/src/cli/controller/operational-plan.ts +1 -1
  157. package/src/cli/controller/systemd-user.ts +4 -10
  158. package/src/cli/editing/edit-session.ts +17 -5
  159. package/src/cli/github/github.ts +4 -4
  160. package/src/cli/github/semantic-projection.ts +174 -0
  161. package/src/cli/hook/prompt-intents.ts +0 -9
  162. package/src/cli/index.ts +1 -43
  163. package/src/cli/local-bridge/connector-freshness.ts +1 -12
  164. package/src/cli/local-bridge/console-automations.ts +1 -1
  165. package/src/cli/local-bridge/console-view-models.ts +1 -1
  166. package/src/cli/local-bridge/facade-api.ts +0 -2
  167. package/src/cli/repositories/command-classifier.ts +0 -9
  168. package/src/cli/repositories/command-scope.ts +2 -2
  169. package/src/cli/repositories/locks.ts +20 -0
  170. package/src/cli/repositories/registry.ts +2 -1
  171. package/src/cli/repositories/safe-patch.ts +2 -12
  172. package/src/cli/repositories/structured-git.ts +39 -3
  173. package/src/cli/repository/inspector.ts +185 -343
  174. package/src/effects/process-runner.ts +17 -1
  175. package/src/runtime/context/generic-lsp-provider.ts +1 -1
  176. package/src/runtime/context/semantic-navigation-contract.ts +7 -0
  177. package/src/runtime/context/semantic-navigation.ts +1 -1
  178. package/src/runtime/control-plane/cleanup-artifact-retention.ts +4 -4
  179. package/src/runtime/control-plane/execution/work-delivery-service.ts +150 -31
  180. package/src/runtime/control-plane/execution/work-handle-authority.ts +12 -4
  181. package/src/runtime/control-plane/execution/work-handle-store.ts +53 -1
  182. package/src/runtime/control-plane/execution/work-operation-service.ts +20 -10
  183. package/src/runtime/control-plane/execution/work-preparation-service.ts +108 -4
  184. package/src/runtime/control-plane/execution/work-process-evidence.ts +2 -2
  185. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +28 -13
  186. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +5 -1
  187. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +1 -1
  188. package/src/runtime/control-plane/facade/capability-registry.ts +2 -0
  189. package/src/runtime/control-plane/facade/work-continuation.ts +1 -1
  190. package/src/runtime/control-plane/generated-cache-retention.ts +114 -63
  191. package/src/runtime/control-plane/global-scheduler/maintenance.ts +20 -8
  192. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +1 -11
  193. package/src/runtime/control-plane/governance/external-effects.ts +1 -1
  194. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +1 -0
  195. package/src/runtime/control-plane/runtime-cleanup.ts +12 -93
  196. package/src/runtime/evidence/sensitive-output.ts +22 -2
  197. package/src/runtime/execution/jobs/child-reference.ts +11 -0
  198. package/src/runtime/execution/jobs/receipt-recovery.ts +1 -11
  199. package/src/runtime/execution/process-runtime/command-facade.ts +3 -3
  200. package/src/runtime/execution/process-runtime/runtime.ts +20 -16
  201. package/src/runtime/execution/thin-harness/execution-router.ts +16 -16
  202. package/src/runtime/execution/thin-harness/fast-executor.ts +2 -2
  203. package/src/runtime/execution/thin-harness/search-worker.ts +2 -2
  204. package/src/runtime/execution/workers/executor.ts +1 -2
  205. package/src/runtime/maintenance/cleanup.ts +41 -5
  206. package/src/runtime/plugins/action-application.ts +12 -1
  207. package/src/runtime/plugins/app-store-connect-adapter.ts +252 -4
  208. package/src/runtime/plugins/browser-adapter.ts +381 -313
  209. package/src/runtime/plugins/browser-macos-bridge.ts +114 -112
  210. package/src/runtime/plugins/browser-manifest-surface.ts +2 -1
  211. package/src/runtime/plugins/browser-registration.ts +2 -0
  212. package/src/runtime/plugins/browser-runtime.ts +26 -35
  213. package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +11 -2
  214. package/src/runtime/plugins/computer-registration.ts +76 -9
  215. package/src/runtime/plugins/github-adapter.ts +30 -3
  216. package/src/runtime/plugins/gmail-adapter.ts +166 -1
  217. package/src/runtime/plugins/google-shared.ts +10 -5
  218. package/src/runtime/plugins/ios/remote-xpc-hid.ts +70 -30
  219. package/src/runtime/plugins/ios-adapter.ts +45 -40
  220. package/src/runtime/plugins/ios-agent-device.ts +54 -4
  221. package/src/runtime/plugins/ios-device-ownership.ts +103 -0
  222. package/src/runtime/plugins/ios-physical-device.ts +497 -79
  223. package/src/runtime/plugins/local-system-adapter.ts +1 -99
  224. package/src/runtime/plugins/store.ts +80 -7
  225. package/src/runtime/plugins/types.ts +4 -0
  226. package/src/runtime/projections/git-status-sampler.ts +2 -19
  227. package/src/runtime/recovery/actions.ts +1 -1
  228. package/src/runtime/recovery/lifecycle-inventory.ts +1 -11
  229. package/src/runtime/recovery/probe.ts +2 -2
  230. package/src/runtime/release/release-coordinator.ts +20 -4
  231. package/src/runtime/root/controller-progression-composition.ts +67 -5
  232. package/src/runtime/root/package-connector-service.ts +123 -11
  233. package/src/runtime/root/package-runtime-release.ts +6 -18
  234. package/src/runtime/root/package-runtime-service.ts +7 -13
  235. package/src/runtime/root/release-execution-canary.ts +18 -5
  236. package/src/runtime/root/release-manifest.ts +0 -34
  237. package/src/runtime/root/release-materialize.ts +0 -78
  238. package/src/runtime/root/runtime.ts +1 -2
  239. package/src/runtime/root/scheduled-controller-composition.ts +2 -1
  240. package/src/runtime/root/service.ts +90 -11
  241. package/src/runtime/root/types.ts +0 -6
  242. package/src/runtime/root/workflow-supervisor-composition.ts +207 -19
  243. package/src/runtime/root/workflow-supervisor-runtime.ts +365 -10
  244. package/src/runtime/safe-tooling/google-credential-store.ts +1 -11
  245. package/src/runtime/safe-tooling/ios-development.ts +22 -8
  246. package/src/runtime/safe-tooling/job-summary.ts +1 -1
  247. package/src/runtime/safe-tooling/workspace-auth.ts +4 -1
  248. package/src/runtime/shared/atomic-text-file.ts +21 -0
  249. package/src/runtime/shared/json-files.ts +8 -3
  250. package/src/runtime/standalone-recovery/core.ts +189 -174
  251. package/src/runtime/standalone-recovery/entry.ts +62 -30
  252. package/src/runtime/standalone-recovery/operation-lock.ts +11 -1
  253. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +1 -0
  254. package/src/runtime/workflow/schedules/engine.ts +36 -14
  255. package/src/runtime/workflow/schedules/pending-activation.ts +11 -4
  256. package/src/runtime/workflow/schedules/work-continuation.ts +51 -6
  257. package/supervisor/client.ts +31 -14
  258. package/supervisor/control-plane.ts +210 -115
  259. package/supervisor/forge-validators.ts +88 -21
  260. package/supervisor/native-browser-adapter.ts +393 -28
  261. package/supervisor/native-messaging/host.ts +21 -2
  262. package/supervisor/protocol.ts +74 -26
  263. package/supervisor/server.ts +16 -8
  264. package/supervisor/store.ts +476 -57
  265. package/supervisor/types.ts +19 -13
  266. package/SKILL.md +0 -427
  267. package/adapters/computer/chatgpt-target-router.ts +0 -103
  268. package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +0 -87
  269. package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +0 -1
  270. package/adapters/mcp/toolset-selection.ts +0 -41
  271. package/agents/openai.yaml +0 -4
  272. package/assets/skill-hooks.json +0 -41
  273. package/assets/skill-version.json +0 -189
  274. package/assets/skills/claude-review/SKILL.md +0 -231
  275. package/assets/skills/codex-review/SKILL.md +0 -103
  276. package/assets/templates/helpers/check-skill-version.ts +0 -193
  277. package/scripts/check-skill-version.ts +0 -5
  278. package/scripts/ensure-codegraph.sh +0 -16
  279. package/scripts/run-skill-hook.ts +0 -244
  280. package/scripts/sync-codex-installed-copies.sh +0 -182
  281. package/src/cli/commands/tools.ts +0 -167
  282. package/src/cli/mcp/access-mode.ts +0 -2
  283. package/src/cli/mcp/access-tools.ts +0 -2
  284. package/src/cli/mcp/audit.ts +0 -2
  285. package/src/cli/mcp/auth.ts +0 -2
  286. package/src/cli/mcp/instructions.ts +0 -2
  287. package/src/cli/mcp/legacy-context.ts +0 -2
  288. package/src/cli/mcp/legacy-tool-service.ts +0 -2
  289. package/src/cli/mcp/multi-repository.ts +0 -2
  290. package/src/cli/mcp/oauth.ts +0 -2
  291. package/src/cli/mcp/openai-secure-tunnel.ts +0 -2
  292. package/src/cli/mcp/paths.ts +0 -2
  293. package/src/cli/mcp/policy.ts +0 -2
  294. package/src/cli/mcp/post-finalize-work-attribution.ts +0 -2
  295. package/src/cli/mcp/proxy-env.ts +0 -2
  296. package/src/cli/mcp/readiness-projection.ts +0 -2
  297. package/src/cli/mcp/redaction.ts +0 -2
  298. package/src/cli/mcp/repo.ts +0 -2
  299. package/src/cli/mcp/repository-tools.ts +0 -2
  300. package/src/cli/mcp/server.ts +0 -2
  301. package/src/cli/mcp/setup.ts +0 -2
  302. package/src/cli/mcp/tool-capability-cache.ts +0 -2
  303. package/src/cli/mcp/tool-contract.ts +0 -2
  304. package/src/cli/mcp/tools.ts +0 -2
  305. package/src/cli/mcp/toolset-names.ts +0 -2
  306. package/src/cli/mcp/toolset-selection.ts +0 -2
  307. package/src/cli/mcp/toolset.ts +0 -2
  308. package/src/cli/mcp/transports/http.ts +0 -2
  309. package/src/cli/mcp/transports/session-registry.ts +0 -2
  310. package/src/cli/mcp/transports/stdio.ts +0 -2
  311. package/src/cli/mcp/types.ts +0 -2
  312. package/src/cli/mcp/umbrella-tools.ts +0 -2
  313. package/src/cli/tools/codegraph.ts +0 -695
  314. package/src/core/adoption/helper-wrapper-plan.ts +0 -159
  315. package/src/runtime/context/codegraph-cache-boundary.ts +0 -255
  316. package/src/runtime/context/codegraph-read-provider.ts +0 -611
  317. package/src/runtime/context/codegraph-sidecar.cjs +0 -248
  318. package/src/runtime/control-plane/codegraph-cache-retention.ts +0 -164
  319. package/src/runtime/gateway/mcp/execution-tools.ts +0 -2
  320. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +0 -2
  321. package/src/runtime/gateway/mcp/persisted-check-process.ts +0 -2
  322. package/src/runtime/gateway/mcp/process-tools.ts +0 -2
  323. package/src/runtime/gateway/mcp/router.ts +0 -2
  324. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +0 -2
  325. package/src/runtime/gateway/mcp/runtime-tools.ts +0 -2
  326. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +0 -2
  327. package/src/runtime/plugins/browser-provider-registry.ts +0 -2
  328. package/src/runtime/plugins/computer-chatgpt-macos-target.ts +0 -891
@@ -1,7 +1,8 @@
1
1
  import { once } from 'node:events';
2
+ import { createHash } from 'node:crypto';
2
3
  import { WorkflowSupervisorControlPlane } from '../../../supervisor/control-plane';
3
4
  import { forgeWorkflowSupervisorValidators } from '../../../supervisor/forge-validators';
4
- import { forgeWorkflowSupervisorLifecycleHooks } from './workflow-supervisor-composition';
5
+ import { forgeWorkflowSupervisorLifecycleHooks, workflowSupervisorContractText } from './workflow-supervisor-composition';
5
6
  import { resolveWorkflowSupervisorForgeHome, workflowSupervisorSocketPath } from '../../../supervisor/paths';
6
7
  import {
7
8
  createWorkflowSupervisorServer,
@@ -12,12 +13,46 @@ import { startWorkflowSupervisorNativeBrowserAdapter, type WorkflowSupervisorNat
12
13
  import type { WorkflowSupervisorConsumerStatus } from '../../../supervisor/types';
13
14
  import { WorkflowSupervisorStore } from '../../../supervisor/store';
14
15
  import { createComputerInteractionTargetAuthority } from '../../../adapters/computer/interaction-target-authority';
15
- import { MacOsChatgptConversationTargetPort } from '../plugins/computer-chatgpt-macos-target';
16
16
  import { ChromeExtensionChatgptConversationTargetPort } from '../../../adapters/computer/chatgpt-extension-target';
17
- import { PreferredChatgptConversationTargetPort } from '../../../adapters/computer/chatgpt-target-router';
18
17
  import { createRuntimeComputerTargetPersistence } from './computer-target-persistence';
19
- import { listDirectActivities, listUserRequests, recordDirectActivity, recordUserRequest, resolveUserRequest } from '../../../packages/kernel/identity/api/index';
18
+ import { getDirectActivity, listUserRequests, recordDirectActivity, recordUserRequest, resolveUserRequest } from '../../../packages/kernel/identity/api/index';
20
19
  import { getRuntimeWriteClaim } from './write-fence';
20
+ import { getRepository } from '../../cli/repositories/registry';
21
+ import { getWorkContract } from '../../../packages/kernel/work/api/index';
22
+ import { getChatgptWorkConversationBinding, parseChatgptConversationIdentity } from '../../../adapters/chatgpt/work-conversation-binding-store';
23
+ import { isTerminalSemanticWorkState } from '../../../packages/kernel/work/api/index';
24
+ import { findActivePluginCapabilityAuthorization, pluginCapabilityAuthorizationOwnerScope } from '../plugins/capability-authorization-grants';
25
+ import { readPluginActionReceiptForRequest, submitAssistantPluginAction, type PluginActionReceipt } from '../plugins/store';
26
+ import { loadGmailPluginConfig } from '../plugins/google-shared';
27
+ import { GMAIL_OWNER_NOTIFICATION_CAPABILITY_ID, GMAIL_PLUGIN_ID } from '../plugins/gmail-adapter';
28
+
29
+ /** An uncertain external send remains owned by its original immutable plugin receipt. */
30
+ export function recordUnknownSupervisorNotificationReceipt(
31
+ controllerHome: string,
32
+ taskId: string,
33
+ requestId: string,
34
+ receipt: Pick<PluginActionReceipt, 'receiptId' | 'requestId' | 'pluginId' | 'actionId' | 'status' | 'effectOutcome'>,
35
+ ): boolean {
36
+ if (receipt.requestId !== requestId || receipt.pluginId !== GMAIL_PLUGIN_ID
37
+ || receipt.actionId !== 'send_owner_notification'
38
+ || receipt.status !== 'failed' || receipt.effectOutcome !== 'outcome_unknown') return false;
39
+ const activityId = `workflow-supervisor-notification-unknown-${requestId}`;
40
+ const existing = getDirectActivity(controllerHome, activityId);
41
+ if (existing?.receiptRef && existing.receiptRef !== receipt.receiptId) {
42
+ throw new Error(`WORKFLOW_SUPERVISOR_NOTIFICATION_RECEIPT_CONFLICT: ${requestId}`);
43
+ }
44
+ if (!existing) recordDirectActivity(controllerHome, {
45
+ activityId,
46
+ capabilityId: 'workflow-supervisor.notification',
47
+ kind: 'direct_execution',
48
+ targetScope: `workflow-supervisor-task:${taskId}`,
49
+ principalId: 'forge-runtime',
50
+ status: 'failed',
51
+ receiptRef: receipt.receiptId,
52
+ summary: 'Gmail notification delivery outcome is unknown. Preserve the exact provider receipt and request ID; do not resend or rekey without authoritative reconciliation.',
53
+ });
54
+ return true;
55
+ }
21
56
 
22
57
  export interface RuntimeWorkflowSupervisorHandle {
23
58
  readonly done: Promise<void>;
@@ -61,7 +96,7 @@ export async function startWorkflowSupervisorRuntime(
61
96
  const transportActivityId = (taskId?: string): string => `workflow-supervisor-transport-${(taskId ?? 'instance').replace(/[^a-zA-Z0-9._-]/g, '_')}`;
62
97
  const reportTransportState = (projection: WorkflowSupervisorTransportProjection): void => {
63
98
  const activityId = transportActivityId(projection.taskId);
64
- const existing = listDirectActivities(controllerHome, 200).find((item) => item.activityId === activityId);
99
+ const existing = getDirectActivity(controllerHome, activityId);
65
100
  const recovered = projection.state === 'recovered';
66
101
  recordDirectActivity(controllerHome, {
67
102
  activityId,
@@ -86,7 +121,7 @@ export async function startWorkflowSupervisorRuntime(
86
121
  const stallActivityId = (taskId: string): string => `workflow-supervisor-stall-${taskId.replace(/[^a-zA-Z0-9._-]/g, '_')}`;
87
122
  const reportTaskStall = (projection: WorkflowSupervisorStallProjection): void => {
88
123
  const activityId = stallActivityId(projection.taskId);
89
- const existing = listDirectActivities(controllerHome, 200).find((item) => item.activityId === activityId);
124
+ const existing = getDirectActivity(controllerHome, activityId);
90
125
  const exhausted = projection.state === 'exhausted';
91
126
  if (!exhausted && !existing) return;
92
127
  const budget = projection.generations !== undefined && projection.maxGenerations !== undefined
@@ -99,7 +134,7 @@ export async function startWorkflowSupervisorRuntime(
99
134
  targetScope: `workflow-supervisor-task:${projection.taskId}`,
100
135
  principalId: 'forge-runtime',
101
136
  status: exhausted ? 'running' : 'completed',
102
- startedAt: existing?.startedAt ?? projection.observedAt,
137
+ startedAt: exhausted && existing?.status !== 'running' ? projection.observedAt : existing?.startedAt ?? projection.observedAt,
103
138
  ...(!exhausted ? { completedAt: projection.observedAt } : {}),
104
139
  summary: exhausted
105
140
  ? projection.stallKind === 'provider_resume_exhausted'
@@ -125,10 +160,330 @@ export async function startWorkflowSupervisorRuntime(
125
160
  },
126
161
  });
127
162
  };
163
+ const notificationRequestId = (...parts: string[]): string => `wfsn_${createHash('sha256').update(parts.join('\u0000')).digest('hex').slice(0, 32)}`;
164
+ const notificationOrigin = { surface: 'system' as const };
165
+ const notificationOwnerScope = pluginCapabilityAuthorizationOwnerScope({ surface: 'system', actor: 'plugin_action_execute' });
166
+ const findNotificationGrant = (repoId: string, target: { kind: 'gmail-mailbox'; id: string; identityFingerprint: string } | undefined) => target && findActivePluginCapabilityAuthorization(controllerHome, {
167
+ ownerScope: notificationOwnerScope,
168
+ repoId,
169
+ pluginId: GMAIL_PLUGIN_ID,
170
+ capabilityId: GMAIL_OWNER_NOTIFICATION_CAPABILITY_ID,
171
+ target,
172
+ scopes: ['gmail.send'],
173
+ risk: 'remote_write',
174
+ });
175
+ const sendSupervisorNotification = async (input: {
176
+ taskId: string;
177
+ requestId: string;
178
+ kind: 'user_action_required' | 'continuation_exhausted';
179
+ subject: string;
180
+ reason: string;
181
+ effectId?: string;
182
+ userRequestId?: string;
183
+ repoId?: string;
184
+ workId?: string;
185
+ conversationUrl?: string;
186
+ objective?: string;
187
+ occurredAt?: string;
188
+ stillActive?: () => boolean;
189
+ }): Promise<void> => {
190
+ if (input.stillActive && !input.stillActive()) return;
191
+ const task = store.getTask(input.taskId);
192
+ const repoId = (task ? workflowSupervisorContractText(task, 'repo_id') : undefined) ?? input.repoId;
193
+ const candidateConversationUrl = task?.conversationUrl ?? input.conversationUrl;
194
+ if (!repoId || !candidateConversationUrl) return;
195
+ let conversationUrl: string;
196
+ try { conversationUrl = parseChatgptConversationIdentity(candidateConversationUrl).conversationUrl; }
197
+ catch { return; } // Never fabricate a ChatGPT session link.
198
+ const repository = getRepository(repoId, controllerHome);
199
+ if (!repository) return;
200
+ // Read the exact prior receipt before grant checks or changing args. An
201
+ // outcome-unknown send must never be resubmitted under any envelope.
202
+ const prior = readPluginActionReceiptForRequest(controllerHome, input.requestId);
203
+ if (prior && recordUnknownSupervisorNotificationReceipt(controllerHome, input.taskId, input.requestId, prior)) return;
204
+ const mailbox = loadGmailPluginConfig(repository.canonicalRoot, { controllerHome, repoId }).accountEmail?.trim().toLowerCase();
205
+ const target = mailbox ? {
206
+ kind: 'gmail-mailbox' as const,
207
+ id: mailbox,
208
+ identityFingerprint: `gmail-profile:${mailbox}`,
209
+ } : undefined;
210
+ const grant = findNotificationGrant(repoId, target);
211
+ if (!grant) {
212
+ recordUserRequest(controllerHome, {
213
+ kind: 'user_action_request',
214
+ rootCauseKey: `workflow-supervisor.notification-auth:${repoId}:${mailbox ?? 'unconfigured'}`,
215
+ title: 'Authorize owner-only Gmail alerts',
216
+ summary: `Forge cannot email owner authorization notices until Gmail action authorize_owner_notifications is approved for the configured and Google-verified owner mailbox${mailbox ? ` (${mailbox})` : ''}. The send-only grant lasts up to 90 days, cannot archive email or contact other recipients, and creates no email when authorized. Unsent events remain in UserRequest.`,
217
+ actionRequired: 'grant_permission',
218
+ targetScope: task ? { scopeKind: 'workflow_supervisor_task', scopeId: input.taskId, repoId, ...(input.workId ? { workId: input.workId } : {}) }
219
+ : { scopeKind: 'work', scopeId: input.workId ?? input.taskId, repoId, ...(input.workId ? { workId: input.workId } : {}) },
220
+ presentation: {
221
+ severity: 'blocked',
222
+ creationReason: 'scheduled_gmail_notification_authorization_required',
223
+ reason: 'EXTERNAL_EFFECT_AUTHORIZATION_REQUIRED',
224
+ currentState: {
225
+ taskId: input.taskId,
226
+ repoId,
227
+ ...(mailbox ? { mailbox } : {}),
228
+ pluginId: GMAIL_PLUGIN_ID,
229
+ capabilityId: GMAIL_OWNER_NOTIFICATION_CAPABILITY_ID,
230
+ authorizationAction: 'authorize_owner_notifications',
231
+ scopes: ['gmail.send'],
232
+ grantLifetimeDays: 90,
233
+ },
234
+ },
235
+ });
236
+ return;
237
+ }
238
+ // Opt-in is prospective. Do not replay a backlog of old blockers or
239
+ // historical terminal completions when the owner first grants email access.
240
+ if (input.occurredAt && Date.parse(input.occurredAt) <= Date.parse(grant.createdAt)) return;
241
+ if (input.stillActive && !input.stillActive()) return;
242
+ const workId = (task ? workflowSupervisorContractText(task, 'work_id') : undefined) ?? input.workId;
243
+ const body = [
244
+ 'Forge Workflow Supervisor 检测到需要人工处理的事件。',
245
+ `事件:${input.kind}`,
246
+ `原因:${input.reason.slice(0, 1800)}`,
247
+ `任务:${task?.taskId ?? input.taskId}`,
248
+ `Work:${workId ?? '未绑定 Work'}`,
249
+ ...(task?.objective || input.objective ? [`目标:${(task?.objective ?? input.objective ?? '').slice(0, 800)}`] : []),
250
+ `ChatGPT 会话链接:${conversationUrl}`,
251
+ ...(repoId && workId ? [`Forge Work 查询:rh_work operation=get repo_id=${repoId} work_id=${workId}`] : []),
252
+ ...(input.effectId ? [`Effect:${input.effectId}`] : []),
253
+ ...(input.userRequestId ? [`UserRequest:${input.userRequestId}`] : []),
254
+ '处理或恢复后,Forge 会继续沿用原有任务与 effect;不要手动重复发送。',
255
+ ].join('\n').slice(0, 11000);
256
+ try {
257
+ const submitted = await submitAssistantPluginAction(controllerHome, repository, {
258
+ pluginId: GMAIL_PLUGIN_ID,
259
+ actionId: 'send_owner_notification',
260
+ requestId: input.requestId,
261
+ args: { subject: input.subject, body_text: body },
262
+ origin: {
263
+ surface: 'system',
264
+ correlationId: input.requestId,
265
+ ...(input.effectId ? { causationId: input.effectId } : {}),
266
+ },
267
+ authorizationGrantRefs: [grant.grantId],
268
+ });
269
+ if (recordUnknownSupervisorNotificationReceipt(controllerHome, input.taskId, input.requestId, submitted.receipt)) {
270
+ process.stderr.write(`[workflow-supervisor-notification] ${input.requestId}: outcome_unknown; receipt ${submitted.receipt.receiptId} retained without resend\n`);
271
+ }
272
+ } catch (error) {
273
+ const failureMessage = error instanceof Error ? error.message : String(error);
274
+ const failureCode = /^([A-Z][A-Z0-9_]+)(?::|$)/.exec(failureMessage)?.[1];
275
+ const loginRequired = failureCode === 'PLUGIN_AUTH_REQUIRED' || failureCode === 'PLUGIN_AUTH_FAILED';
276
+ const authorizationRequired = loginRequired || [
277
+ 'PLUGIN_CAPABILITY_GRANT_REQUIRED',
278
+ 'GMAIL_AUTOMATION_DISABLED',
279
+ 'GMAIL_AUTOMATION_MAILBOX_NOT_CONFIGURED',
280
+ 'GMAIL_AUTOMATION_MAILBOX_MISMATCH',
281
+ 'EXTERNAL_EFFECT_AUTHORIZATION_REQUIRED',
282
+ ].includes(failureCode ?? '');
283
+ if (authorizationRequired && failureCode) {
284
+ const workId = (task ? workflowSupervisorContractText(task, 'work_id') : undefined) ?? input.workId;
285
+ recordUserRequest(controllerHome, {
286
+ kind: 'user_action_request',
287
+ rootCauseKey: `workflow-supervisor.notification-send-auth:${input.taskId}:${input.requestId}`,
288
+ title: loginRequired ? 'Gmail notification reauthorization required' : 'Gmail notification authorization required',
289
+ summary: `Forge could not send the notification to the configured Gmail profile${mailbox ? ` (${mailbox})` : ''} because the exact Gmail provider action failed with ${failureCode}: ${failureMessage}. No alternate provider or automatic resend was used. Restore the Gmail authorization or exact mailbox grant, then continue from the original task.`,
290
+ actionRequired: loginRequired ? 'login' : 'grant_permission',
291
+ targetScope: {
292
+ scopeKind: task ? 'workflow_supervisor_task' : 'work',
293
+ scopeId: task ? input.taskId : (workId ?? input.taskId),
294
+ repoId,
295
+ ...(workId ? { workId } : {}),
296
+ },
297
+ presentation: {
298
+ severity: 'blocked',
299
+ creationReason: 'scheduled_gmail_notification_authorization_failed',
300
+ reason: failureCode,
301
+ currentState: {
302
+ taskId: input.taskId,
303
+ repoId,
304
+ ...(workId ? { workId } : {}),
305
+ ...(mailbox ? { mailbox } : {}),
306
+ pluginId: GMAIL_PLUGIN_ID,
307
+ capabilityId: GMAIL_OWNER_NOTIFICATION_CAPABILITY_ID,
308
+ actionId: 'send_owner_notification',
309
+ requestId: input.requestId,
310
+ ...(input.effectId ? { effectId: input.effectId } : {}),
311
+ errorCode: failureCode,
312
+ },
313
+ },
314
+ });
315
+ }
316
+ process.stderr.write(`[workflow-supervisor-notification] ${input.requestId}: ${failureMessage}\\n`);
317
+ }
318
+ };
319
+ let notificationReconciliationInFlight: Promise<void> | undefined;
320
+ const reconcileSupervisorNotifications = (): void => {
321
+ if (closing || notificationReconciliationInFlight) return;
322
+ notificationReconciliationInFlight = (async () => {
323
+ const allRequests = listUserRequests(controllerHome);
324
+ for (const request of allRequests.filter((item) => item.status === 'pending' && item.rootCauseKey.startsWith('workflow-supervisor.notification-auth:'))) {
325
+ const taskId = request.targetScope?.scopeId;
326
+ if (!taskId) continue;
327
+ const task = store.getTask(taskId);
328
+ if (!task) continue;
329
+ const repoId = workflowSupervisorContractText(task, 'repo_id');
330
+ const workId = workflowSupervisorContractText(task, 'work_id');
331
+ if (!repoId) continue;
332
+ if (workId) {
333
+ const work = getWorkContract({ controllerHome, repoId }, workId);
334
+ if (!work || isTerminalSemanticWorkState(work.semanticState)) {
335
+ resolveUserRequest(controllerHome, { requestId: request.requestId, decision: 'underlying_work_terminal', resolvedBy: 'forge-runtime' });
336
+ continue;
337
+ }
338
+ }
339
+ const repository = getRepository(repoId, controllerHome);
340
+ if (!repository) continue;
341
+ const mailbox = loadGmailPluginConfig(repository.canonicalRoot, { controllerHome, repoId }).accountEmail?.trim().toLowerCase();
342
+ const target = mailbox ? {
343
+ kind: 'gmail-mailbox' as const,
344
+ id: mailbox,
345
+ identityFingerprint: `gmail-profile:${mailbox}`,
346
+ } : undefined;
347
+ if (findNotificationGrant(repoId, target)) {
348
+ resolveUserRequest(controllerHome, { requestId: request.requestId, decision: 'gmail_automation_authorized', resolvedBy: 'forge-runtime' });
349
+ }
350
+ }
351
+
352
+ // One canonical pending UserRequest -> one Gmail notice. Never email
353
+ // our own notification-grant blockers. A request opts in only when it
354
+ // was created after the owner-notification grant, so enabling this
355
+ // capability cannot fan out the backlog that predated authorization.
356
+ for (const request of allRequests.filter((item) => item.status === 'pending'
357
+ && ['grant_permission', 'login', 'confirm_destructive'].includes(item.actionRequired)
358
+ && !item.rootCauseKey.startsWith('workflow-supervisor.notification-'))) {
359
+ const scope = request.targetScope;
360
+ const task = (scope?.scopeKind === 'workflow_supervisor_task' ? store.getTask(scope.scopeId) : undefined)
361
+ ?? (scope?.workId || scope?.scopeKind === 'work'
362
+ ? store.listTasks().find((candidate) => workflowSupervisorContractText(candidate, 'work_id') === (scope?.workId ?? scope?.scopeId))
363
+ : undefined);
364
+ const workId = scope?.workId ?? (scope?.scopeKind === 'work' ? scope.scopeId : undefined);
365
+ const repoId = scope?.repoId ?? (task ? workflowSupervisorContractText(task, 'repo_id') : undefined);
366
+ const bound = !task && workId && repoId
367
+ ? getChatgptWorkConversationBinding({ controllerHome, repoId }, workId) : undefined;
368
+ const taskId = task?.taskId ?? (bound ? `work:${workId}` : undefined);
369
+ if (!taskId || (!task && !bound)) continue;
370
+ const work = repoId && workId ? getWorkContract({ controllerHome, repoId }, workId) : undefined;
371
+ if (repoId && workId) {
372
+ if (!work || isTerminalSemanticWorkState(work.semanticState)) {
373
+ resolveUserRequest(controllerHome, { requestId: request.requestId, decision: 'underlying_work_terminal', resolvedBy: 'forge-runtime' });
374
+ continue;
375
+ }
376
+ }
377
+ const repoIdForMail = repoId;
378
+ const repositoryForMail = repoIdForMail ? getRepository(repoIdForMail, controllerHome) : undefined;
379
+ const configuredMailbox = repositoryForMail && repoIdForMail
380
+ ? loadGmailPluginConfig(repositoryForMail.canonicalRoot, { controllerHome, repoId: repoIdForMail }).accountEmail?.trim().toLowerCase()
381
+ : undefined;
382
+ const grant = repoIdForMail && configuredMailbox ? findNotificationGrant(repoIdForMail, {
383
+ kind: 'gmail-mailbox', id: configuredMailbox, identityFingerprint: `gmail-profile:${configuredMailbox}`,
384
+ }) : undefined;
385
+ if (!grant || Date.parse(request.createdAt) <= Date.parse(grant.createdAt)) continue;
386
+ const effectId = typeof request.presentation?.currentState?.effectId === 'string' ? request.presentation.currentState.effectId : undefined;
387
+ const actionLabel = request.actionRequired === 'login' ? '登录授权' : request.actionRequired === 'confirm_destructive' ? '操作确认' : '执行授权';
388
+ await sendSupervisorNotification({
389
+ taskId,
390
+ repoId: repoIdForMail,
391
+ workId,
392
+ ...(bound ? { conversationUrl: bound.conversationUrl } : {}),
393
+ ...(work?.objective ? { objective: work.objective } : {}),
394
+ requestId: notificationRequestId('user-action', request.requestId),
395
+ occurredAt: request.createdAt,
396
+ kind: 'user_action_required',
397
+ subject: `[Forge] 需要处理:${actionLabel}`,
398
+ reason: request.summary,
399
+ effectId,
400
+ userRequestId: request.requestId,
401
+ stillActive: () => listUserRequests(controllerHome, 'pending').some((item) => item.requestId === request.requestId),
402
+ });
403
+ }
404
+
405
+ for (const task of store.listTasks()) {
406
+ const taskId = task.taskId;
407
+ const activity = getDirectActivity(controllerHome, stallActivityId(taskId));
408
+ if (activity?.capabilityId !== 'workflow-supervisor.continuation'
409
+ || activity.status !== 'running'
410
+ || activity.targetScope !== `workflow-supervisor-task:${taskId}`) continue;
411
+ const stall = controlPlane.taskStall(taskId);
412
+ if (!stall || !(stall.state === 'provider_resume_exhausted'
413
+ || (stall.state === 'retryable' && stall.generations >= stall.maxGenerations))) continue;
414
+ const reason = stall.state === 'provider_resume_exhausted'
415
+ ? 'Supervisor 的 provider resume 尝试已确认耗尽;原 effect 仍被保留,不会盲目重发。'
416
+ : `Supervisor 自动派发尝试已确认耗尽(${stall.generations}/${stall.maxGenerations});原 effect 仍被保留,不会盲目重发。`;
417
+ await sendSupervisorNotification({
418
+ taskId,
419
+ requestId: notificationRequestId('stall', taskId, stall.state, stall.effectId),
420
+ occurredAt: activity.startedAt,
421
+ kind: 'continuation_exhausted',
422
+ subject: `[Forge] 自动续跑已耗尽:${taskId}`,
423
+ reason,
424
+ effectId: stall.effectId,
425
+ stillActive: () => {
426
+ const currentActivity = getDirectActivity(controllerHome, stallActivityId(taskId));
427
+ const currentStall = controlPlane.taskStall(taskId);
428
+ if (!currentStall) return false;
429
+ const currentStallExhausted = currentStall.state === 'provider_resume_exhausted'
430
+ || (currentStall.state === 'retryable' && currentStall.generations >= currentStall.maxGenerations);
431
+ return currentActivity?.status === 'running'
432
+ && currentActivity.targetScope === `workflow-supervisor-task:${taskId}`
433
+ && currentStallExhausted
434
+ && currentStall.state === stall.state
435
+ && currentStall.effectId === stall.effectId;
436
+ },
437
+ });
438
+ }
439
+
440
+ for (const task of store.listTasks()) {
441
+ if (store.terminalAction(task.taskId) !== 'NEEDS_USER') continue;
442
+ const completion = store.getLatestCompletion(task.taskId);
443
+ if (!completion || completion.action !== 'NEEDS_USER') continue;
444
+ const repoId = workflowSupervisorContractText(task, 'repo_id');
445
+ const workId = workflowSupervisorContractText(task, 'work_id');
446
+ if (repoId && workId) {
447
+ const work = getWorkContract({ controllerHome, repoId }, workId);
448
+ if (!work || isTerminalSemanticWorkState(work.semanticState)) continue;
449
+ }
450
+ const relatedRequests = allRequests.filter((request) => request.targetScope?.scopeId === task.taskId
451
+ && request.rootCauseKey.startsWith(`workflow-supervisor.transport-human:${task.taskId}:`));
452
+ if (relatedRequests.some((request) => request.status === 'pending')) continue;
453
+ const completionCommittedAt = Date.parse(completion.committedAt);
454
+ if (relatedRequests.some((request) => {
455
+ const resolvedAt = Date.parse(request.resolution?.resolvedAt ?? '');
456
+ return Number.isFinite(resolvedAt) && Number.isFinite(completionCommittedAt) && resolvedAt >= completionCommittedAt;
457
+ })) continue;
458
+ await sendSupervisorNotification({
459
+ taskId: task.taskId,
460
+ requestId: notificationRequestId('terminal-needs-user', task.taskId, completion.completionFingerprint),
461
+ occurredAt: completion.committedAt,
462
+ kind: 'user_action_required',
463
+ subject: `[Forge] 需要人工处理:${task.taskId}`,
464
+ reason: completion.proposal.reason,
465
+ effectId: completion.sourceEffectId,
466
+ stillActive: () => {
467
+ const currentTask = store.getTask(task.taskId);
468
+ const currentCompletion = store.getLatestCompletion(task.taskId);
469
+ if (store.terminalAction(task.taskId) !== 'NEEDS_USER' || currentCompletion?.completionFingerprint !== completion.completionFingerprint) return false;
470
+ const currentRepoId = currentTask ? workflowSupervisorContractText(currentTask, 'repo_id') : undefined;
471
+ const currentWorkId = currentTask ? workflowSupervisorContractText(currentTask, 'work_id') : undefined;
472
+ if (currentRepoId && currentWorkId) {
473
+ const currentWork = getWorkContract({ controllerHome, repoId: currentRepoId }, currentWorkId);
474
+ if (!currentWork || isTerminalSemanticWorkState(currentWork.semanticState)) return false;
475
+ }
476
+ return true;
477
+ },
478
+ });
479
+ }
480
+ })()
481
+ .catch((error) => { process.stderr.write(`[workflow-supervisor-notification-reconcile] ${error instanceof Error ? error.message : String(error)}\\n`); })
482
+ .finally(() => { notificationReconciliationInFlight = undefined; });
483
+ };
128
484
  const targetAuthority = createComputerInteractionTargetAuthority(createRuntimeComputerTargetPersistence());
129
485
  const extensionTargetPort = new ChromeExtensionChatgptConversationTargetPort(controllerHome, targetAuthority);
130
- const appleEventsTargetPort = new MacOsChatgptConversationTargetPort(controllerHome, targetAuthority);
131
- const targetPort = new PreferredChatgptConversationTargetPort(extensionTargetPort, appleEventsTargetPort);
486
+ const targetPort = extensionTargetPort;
132
487
  const browserConsumerStatus = (): WorkflowSupervisorConsumerStatus => nativeBrowser?.status() ?? {
133
488
  enabled: browserAdapterEnabled,
134
489
  running: false,
@@ -153,7 +508,7 @@ export async function startWorkflowSupervisorRuntime(
153
508
  const reconcileCommittedContinuations = (): void => {
154
509
  if (closing || reconciliationInFlight) return;
155
510
  reconciliationInFlight = controlPlane.reconcileCommittedContinuations()
156
- .then(() => undefined)
511
+ .then(() => { reconcileSupervisorNotifications(); })
157
512
  .catch((error) => { process.stderr.write(`[workflow-supervisor-reconcile] ${error instanceof Error ? error.message : String(error)}\\n`); })
158
513
  .finally(() => { reconciliationInFlight = undefined; });
159
514
  };
@@ -14,16 +14,11 @@ const KEYCHAIN_ACCOUNT: Record<StoredGoogleCredentialKind, string> = {
14
14
  'refresh-token': 'refresh-token',
15
15
  'client-secret': 'client-secret',
16
16
  };
17
- const memoryCache = new Map<string, string>();
18
17
 
19
18
  function keychainService(service: StoredGoogleService): string {
20
19
  return `${KEYCHAIN_SERVICE_PREFIX}.${service}`;
21
20
  }
22
21
 
23
- function cacheKey(service: StoredGoogleService, kind: StoredGoogleCredentialKind): string {
24
- return `${service}:${kind}`;
25
- }
26
-
27
22
  const macKeychainAdapter: GoogleCredentialStoreAdapter = {
28
23
  available: () => process.platform === 'darwin',
29
24
  read(service, kind = 'refresh-token') {
@@ -66,7 +61,6 @@ let adapter: GoogleCredentialStoreAdapter = macKeychainAdapter;
66
61
 
67
62
  export function setGoogleCredentialStoreAdapterForTest(next?: GoogleCredentialStoreAdapter): void {
68
63
  adapter = next ?? macKeychainAdapter;
69
- memoryCache.clear();
70
64
  }
71
65
 
72
66
  export function googleCredentialStoreStatus(): Record<string, unknown> {
@@ -87,13 +81,10 @@ function readStoredGoogleCredential(
87
81
  ? ['google-workspace']
88
82
  : [service, 'google-workspace'];
89
83
  for (const candidate of candidates) {
90
- const key = cacheKey(candidate, kind);
91
- const cached = memoryCache.get(key);
92
- if (cached) return { value: cached, source: `keychain:${candidate}:${kind}` };
93
84
  if (!adapter.available()) continue;
85
+ // Keychain is the authority: another Forge process may have rotated this credential.
94
86
  const value = adapter.read(candidate, kind)?.trim();
95
87
  if (!value) continue;
96
- memoryCache.set(key, value);
97
88
  return { value, source: `keychain:${candidate}:${kind}` };
98
89
  }
99
90
  return undefined;
@@ -104,7 +95,6 @@ function writeStoredGoogleCredential(service: StoredGoogleService, kind: StoredG
104
95
  if (!normalized) throw new Error(kind === 'refresh-token' ? 'GOOGLE_REFRESH_TOKEN_REQUIRED' : 'GOOGLE_CLIENT_SECRET_REQUIRED');
105
96
  if (!adapter.available()) throw new Error('GOOGLE_CREDENTIAL_STORE_UNAVAILABLE');
106
97
  adapter.write(service, normalized, kind);
107
- memoryCache.set(cacheKey(service, kind), normalized);
108
98
  }
109
99
 
110
100
  export function readStoredGoogleRefreshToken(service: StoredGoogleService): { token: string; source: string } | undefined {
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'crypto';
2
- import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, writeFileSync } from 'fs';
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, utimesSync, writeFileSync } from 'fs';
3
3
  import { basename, dirname, join, relative, resolve, sep } from 'path';
4
4
  import { spawnSync } from 'child_process';
5
5
  import type { RepositoryRecord } from '../../cli/repositories/types';
@@ -155,13 +155,26 @@ function sanitize(value: string): string {
155
155
 
156
156
  function stableBuildDerivedDataPath(
157
157
  repository: RepositoryRecord,
158
- input: { workspace?: string; project?: string; scheme: string; configuration: string; destination: string },
158
+ input: { workspace?: string; project?: string; scheme: string; configuration: string; destinationPlatform: 'ios_simulator' | 'ios' },
159
159
  ): string {
160
- const identity = JSON.stringify([input.workspace ?? '', input.project ?? '', input.scheme, input.configuration, input.destination]);
160
+ const identity = JSON.stringify([input.workspace ?? '', input.project ?? '', input.scheme, input.configuration, input.destinationPlatform]);
161
161
  const digest = createHash('sha256').update(identity).digest('hex').slice(0, 16);
162
162
  return join(safeArtifactDir(repository, 'DerivedData'), `build-${sanitize(input.scheme)}-${digest}`);
163
163
  }
164
164
 
165
+ // Xcode owns invalidation within this checkout-local, platform/configuration bucket.
166
+ // Device identity and test selectors affect execution, not the reusable build cache.
167
+ function runCachedXcodeCommand(repository: RepositoryRecord, derivedDataPath: string, args: string[], timeoutMs: number): IosCommandResult {
168
+ const touch = (): void => { const usedAt = now(); utimesSync(derivedDataPath, usedAt, usedAt); };
169
+ touch();
170
+ try {
171
+ return runCommand('xcodebuild', args, { cwd: repository.canonicalRoot, timeoutMs });
172
+ } finally {
173
+ // Reuse may only modify descendants; keep last-use grace on the bucket itself.
174
+ touch();
175
+ }
176
+ }
177
+
165
178
  function boundedText(value: string, maxBytes = 20_000): { content: string; truncated: boolean } {
166
179
  const buffer = Buffer.from(value, 'utf-8');
167
180
  if (buffer.byteLength <= maxBytes) return { content: value, truncated: false };
@@ -427,7 +440,7 @@ export function iosAppBuild(repository: RepositoryRecord, input: {
427
440
  : udid
428
441
  ? `platform=iOS Simulator,id=${udid}`
429
442
  : `platform=iOS Simulator,name=${input.simulatorName ?? 'iPhone 16 Pro'}`;
430
- const derivedDataPath = stableBuildDerivedDataPath(repository, { workspace, project, scheme, configuration, destination });
443
+ const derivedDataPath = stableBuildDerivedDataPath(repository, { workspace, project, scheme, configuration, destinationPlatform });
431
444
  const derivedDataReused = existsSync(derivedDataPath);
432
445
  mkdirSync(derivedDataPath, { recursive: true });
433
446
  const priorBuiltApps = derivedDataReused ? findBuiltApps(repository, derivedDataPath) : [];
@@ -445,7 +458,7 @@ export function iosAppBuild(repository: RepositoryRecord, input: {
445
458
  '-authenticationKeyIssuerID', authentication.issuerId,
446
459
  );
447
460
  }
448
- const result = runCommand('xcodebuild', args, { cwd: repository.canonicalRoot, timeoutMs: input.timeoutMs ?? 10 * 60_000 });
461
+ const result = runCachedXcodeCommand(repository, derivedDataPath, args, input.timeoutMs ?? 10 * 60_000);
449
462
  const sensitiveValues = authentication
450
463
  ? [authentication.privateKeyPath, authentication.keyId, authentication.issuerId]
451
464
  : [];
@@ -509,8 +522,9 @@ export function iosXcodeTest(repository: RepositoryRecord, input: {
509
522
  if (!scheme) throw new Error('IOS_SCHEME_REQUIRED: provide scheme or share at least one Xcode scheme');
510
523
  const workspace = safeRepoRelativePath(repository.canonicalRoot, input.workspace ?? listedReady.workspace);
511
524
  const project = safeRepoRelativePath(repository.canonicalRoot, input.project ?? listedReady.project);
512
- const derivedDataPath = join(safeArtifactDir(repository, 'DerivedData'), `tests-${sanitize(scheme)}`);
513
- const resultBundlePath = join(safeArtifactDir(repository, 'test-results'), `${timestamp()}-${sanitize(scheme)}.xcresult`);
525
+ const configuration = input.configuration ?? 'Debug';
526
+ const derivedDataPath = stableBuildDerivedDataPath(repository, { workspace, project, scheme, configuration, destinationPlatform: 'ios_simulator' });
527
+ const resultBundlePath = join(safeArtifactDir(repository, 'test-results'), `${artifactStamp()}-${sanitize(scheme)}.xcresult`);
514
528
  mkdirSync(derivedDataPath, { recursive: true });
515
529
  const args = ['test'];
516
530
  if (workspace) args.push('-workspace', workspace);
@@ -531,7 +545,7 @@ export function iosXcodeTest(repository: RepositoryRecord, input: {
531
545
  if (normalized) args.push(`-only-testing:${normalized}`);
532
546
  }
533
547
  args.push('-derivedDataPath', derivedDataPath, '-resultBundlePath', resultBundlePath);
534
- const result = runCommand('xcodebuild', args, { cwd: repository.canonicalRoot, timeoutMs: input.timeoutMs ?? 15 * 60_000 });
548
+ const result = runCachedXcodeCommand(repository, derivedDataPath, args, input.timeoutMs ?? 15 * 60_000);
535
549
  return {
536
550
  ready: result.ok,
537
551
  ok: result.ok,
@@ -1,4 +1,4 @@
1
- import { redactMcpText } from '../../cli/mcp/redaction';
1
+ import { redactMcpText } from '../../../adapters/mcp/redaction';
2
2
  import type { ExecutionJob } from '../execution/jobs/types';
3
3
  import { buildJobOperationDigest } from '../control-plane/facade/operation-digest';
4
4
  import {
@@ -56,6 +56,7 @@ function pluginAuthSummary(manifest: AssistantPluginManifest): Record<string, un
56
56
  lifecycleReason: manifest.lifecycle.reason,
57
57
  healthState: manifest.health.state,
58
58
  ready: manifest.health.ready,
59
+ readinessMode: manifest.health.details?.readinessMode,
59
60
  authRequired: manifest.health.errors.some((error) => /access token|auth|authorization|oauth/i.test(error)),
60
61
  errors: manifest.health.errors.slice(0, 5),
61
62
  warnings: manifest.health.warnings.slice(0, 5),
@@ -74,7 +75,9 @@ export function buildWorkspaceAuthStatus(manifests: AssistantPluginManifest[]):
74
75
  actionRequired: summaries.filter((entry) => entry.authRequired === true || entry.ready !== true).map((entry) => ({
75
76
  pluginId: entry.pluginId,
76
77
  reason: entry.lifecycleReason,
77
- next: 'Call workspace_auth_login_prepare and open the returned local OAuth URL. The callback stores the refresh token in macOS Keychain.',
78
+ next: entry.readinessMode === 'live_token_unverified'
79
+ ? 'Run a read-only provider action. If Google requires client_secret, restore the matching OAuth client secret in macOS Keychain; if it cannot be recovered, configure a dedicated OAuth client ID and matching secret, then reauthorize.'
80
+ : 'Call workspace_auth_login_prepare and open the returned local OAuth URL. The callback stores the refresh token in macOS Keychain.',
78
81
  })),
79
82
  safety: {
80
83
  credentialMaterialReturned: false,
@@ -0,0 +1,21 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { mkdirSync, renameSync, rmSync, writeFileSync } from 'fs';
3
+ import { dirname } from 'path';
4
+
5
+ /**
6
+ * The single atomic text writer for Forge-managed service and release files.
7
+ * Create private parent directories, use a collision-resistant temporary file,
8
+ * preserve the caller-selected target mode, and remove abandoned temp files
9
+ * after a failed write or rename. An atomic rename is not an fsync guarantee.
10
+ */
11
+ export function writeAtomicTextFile(path: string, content: string, mode = 0o600): void {
12
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
13
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
14
+ try {
15
+ writeFileSync(temporary, content, { encoding: 'utf8', mode, flag: 'wx' });
16
+ renameSync(temporary, path);
17
+ } catch (error) {
18
+ try { rmSync(temporary, { force: true }); } catch { /* preserve original write failure */ }
19
+ throw error;
20
+ }
21
+ }
@@ -16,11 +16,16 @@ export function readJsonFile<T>(path: string, fallback?: T): T {
16
16
  }
17
17
  }
18
18
 
19
- export function writeJsonAtomic(path: string, value: unknown): void {
19
+ export function writeJsonAtomic(path: string, value: unknown, options?: { mode?: number }): void {
20
20
  ensureParent(path);
21
21
  const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
22
- writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
23
- renameSync(temporary, path);
22
+ try {
23
+ writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', ...(options?.mode === undefined ? {} : { mode: options.mode }) });
24
+ renameSync(temporary, path);
25
+ } catch (error) {
26
+ rmSync(temporary, { force: true });
27
+ throw error;
28
+ }
24
29
  }
25
30
 
26
31
  export function appendJsonLine(path: string, value: unknown): void {