@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.
- package/AGENTS.md +5 -2
- package/CHANGELOG.md +20 -0
- package/README.en.md +24 -3
- package/README.md +10 -6
- package/README.zh-CN.md +5 -3
- package/THIRD_PARTY_NOTICES.md +1 -2
- package/adapters/chatgpt/browser-delivery-runtime.ts +5 -3
- package/adapters/chatgpt/provider-delivery.ts +1 -1
- package/adapters/chatgpt/work-conversation-binding-store.ts +20 -1
- package/adapters/computer/chatgpt-extension-target.ts +295 -50
- package/adapters/computer/chrome-extension/background.js +227 -71
- package/adapters/computer/chrome-extension/content.js +181 -157
- package/adapters/computer/chrome-extension/manifest.json +3 -1
- package/adapters/computer/desktop-operator-provider.ts +18 -6
- package/adapters/computer/interaction-target-authority.ts +39 -9
- package/adapters/mcp/access-mode.ts +18 -39
- package/adapters/mcp/auth.ts +2 -6
- package/adapters/mcp/instructions.ts +2 -2
- package/adapters/mcp/multi-repository.ts +7 -28
- package/adapters/mcp/runtime-gateway/context-adapter.ts +79 -40
- package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +7 -15
- package/adapters/mcp/runtime-gateway/execution-tools.ts +7 -13
- package/adapters/mcp/runtime-gateway/plugin-adapter.ts +16 -3
- package/adapters/mcp/runtime-gateway/process-tools.ts +3 -21
- package/adapters/mcp/runtime-gateway/router.ts +3 -5
- package/adapters/mcp/runtime-gateway/routing-policy.ts +17 -10
- package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1 -1
- package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +14 -9
- package/adapters/mcp/runtime-gateway/runtime-tools.ts +1 -5
- package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +36 -1
- package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +58 -4
- package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +7 -3
- package/adapters/mcp/runtime-gateway/work-adapter.ts +16 -16
- package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +0 -17
- package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +69 -12
- package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +26 -2
- package/adapters/mcp/server.ts +24 -3
- package/adapters/mcp/setup.ts +4 -10
- package/adapters/mcp/tool-mapping/access-tools.ts +0 -6
- package/adapters/mcp/tool-mapping/legacy-tool-service.ts +672 -2935
- package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +1 -1
- package/adapters/mcp/tool-mapping/repository-tools.ts +52 -86
- package/adapters/mcp/toolset-names.ts +11 -42
- package/adapters/mcp/toolset.ts +16 -90
- package/{src/cli → adapters}/mcp/transports/AGENTS.md +5 -5
- package/{src/cli → adapters}/mcp/transports/CLAUDE.md +5 -5
- package/adapters/mcp/transports/http-observation.ts +18 -6
- package/adapters/mcp/transports/http-post-lifecycle.ts +19 -0
- package/adapters/mcp/transports/http.ts +54 -14
- package/adapters/mcp/types.ts +0 -6
- package/assets/hooks/lib/session-state.sh +0 -38
- package/assets/hooks/post-bash.sh +1 -1
- package/assets/hooks/post-tool-observer.sh +0 -5
- package/assets/hooks/prompt-guard.sh +0 -55
- package/assets/partials/04-project-structure.partial.md +1 -1
- package/assets/partials/07-footer.partial.md +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +1 -1
- package/assets/partials-agents/08-deep-docs.partial.md +1 -1
- package/assets/reference-configs/agentic-development-flow.md +1 -1
- package/assets/reference-configs/external-tooling.md +0 -76
- package/assets/reference-configs/release-deploy.md +1 -1
- package/assets/templates/helpers/architecture-queue.sh +1 -1
- package/assets/templates/helpers/check-agent-tooling.sh +1 -327
- package/assets/templates/helpers/check-task-workflow.sh +1 -1
- package/assets/templates/helpers/ensure-task-workflow.sh +0 -14
- package/assets/templates/helpers/migrate-project-template.sh +2 -2
- package/assets/workflow-contract.v1.json +0 -2
- package/docs/README.md +5 -2
- package/docs/architecture/CURRENT.md +114 -30
- package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +42 -0
- package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +15 -2
- package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +4 -2
- package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +109 -4
- package/docs/architecture/decisions/20261009-exact-code-context-discovery.md +19 -0
- package/docs/architecture/decisions/20261009-generated-xcode-cache-retention.md +37 -0
- package/docs/architecture/decisions/20261011-autonomous-continuation-convergence.md +597 -0
- package/docs/architecture/decisions/20261017-edit-session-finalization-reconciliation.md +17 -0
- package/docs/architecture/decisions/20261019-retire-host-skills.md +16 -0
- package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +1 -1
- package/docs/architecture/domains/public-surface.md +3 -5
- package/docs/architecture/domains/verification.md +5 -5
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +2 -2
- package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +4 -4
- package/docs/architecture/reviews/20261015-code-search-runtime-acceptance.md +75 -0
- package/docs/forge-plugin-management.md +14 -2
- package/docs/operations/controller-browser-plugin.md +23 -0
- package/docs/operations/features.md +6 -7
- package/docs/operations/features.zh-CN.md +1 -1
- package/docs/operations/github-repository.md +4 -4
- package/docs/operations/github-semantic-release-projection.md +20 -0
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/mcp-tool-exposure.md +13 -54
- package/docs/operations/runtime-performance-diagnostics.md +8 -0
- package/docs/operations/standalone-disaster-recovery.md +1 -1
- package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
- package/docs/tutorials/03-first-repository-task.md +2 -0
- package/docs/wiki/Computer-and-Plugins.md +78 -0
- package/docs/wiki/Computer-and-Plugins.zh-CN.md +88 -0
- package/docs/wiki/Home.md +16 -2
- package/docs/wiki/Implementation.md +24 -12
- package/docs/wiki/Installation.md +28 -32
- package/docs/wiki/Integrations.md +4 -0
- package/docs/wiki/Quick-Start.md +35 -22
- package/docs/wiki/Releases-and-Upgrades.md +15 -25
- package/docs/wiki/_Sidebar.md +5 -1
- package/package.json +1 -12
- package/packages/kernel/controller/infrastructure/controller-session-store.ts +63 -38
- package/packages/kernel/identity/infrastructure/activity-projection-store.ts +10 -0
- package/packages/kernel/scheduler/application/eligibility.ts +4 -1
- package/packages/kernel/scheduler/infrastructure/schedule-store.ts +13 -10
- package/packages/kernel/work/application/work-service.ts +1 -0
- package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
- package/packages/kernel/work/domain/state-machine.ts +9 -2
- package/packages/kernel/work/infrastructure/work-contract-store.ts +28 -1
- package/packages/plugin-runtime/browser/provider-registry.ts +12 -36
- package/packages/plugin-runtime/computer/conversation-target.ts +17 -7
- package/scripts/assemble-template.ts +11 -72
- package/scripts/check-mcp-compatibility.ts +55 -74
- package/scripts/check-package-identity.mjs +0 -5
- package/scripts/check-runtime-architecture.mjs +305 -28
- package/scripts/init-project.sh +0 -46
- package/scripts/install-ios-hid-toolchain.sh +60 -0
- package/scripts/install-standalone-recovery.ts +1 -1
- package/scripts/lib/project-init-lib.sh +3 -3
- package/scripts/migrate-windows-wsl-controller-home.ts +2 -0
- package/scripts/open-source-audit-allowlist.txt +0 -1
- package/scripts/run-governed-gate.ts +2 -0
- package/scripts/smoke-ios-physical-device.ts +38 -10
- package/scripts/smoke-mcp-http-runtime.ts +7 -8
- package/scripts/smoke-mcp-tool-surface.ts +27 -41
- package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +6 -3
- package/scripts/sync-generated-authority.mjs +0 -1
- package/src/cli/agent-jobs/executor-health.ts +1 -1
- package/src/cli/agent-jobs/integration.ts +2 -2
- package/src/cli/chatgpt-browser/file-policy.ts +2 -2
- package/src/cli/commands/doctor.ts +1 -143
- package/src/cli/commands/global-runtime.ts +3 -196
- package/src/cli/commands/init-hook.ts +6 -23
- package/src/cli/commands/init.ts +3 -252
- package/src/cli/commands/mcp.ts +0 -5
- package/src/cli/commands/prompt-guard-decision.ts +0 -2
- package/src/cli/commands/recovery.ts +2 -14
- package/src/cli/commands/repository.ts +17 -1
- package/src/cli/commands/setup-execution.ts +41 -0
- package/src/cli/controller/completion-orchestrator.ts +1 -1
- package/src/cli/controller/composite-operations.ts +1 -1
- package/src/cli/controller/context/instruction-context.ts +1 -1
- package/src/cli/controller/context/known-paths.ts +2 -2
- package/src/cli/controller/context/query-planning.ts +11 -4
- package/src/cli/controller/context/rule-resolution.ts +2 -2
- package/src/cli/controller/context/source-materializer.ts +2 -2
- package/src/cli/controller/context/structural-provider.ts +84 -0
- package/src/cli/controller/context/structural.ts +6 -6
- package/src/cli/controller/context/types.ts +8 -9
- package/src/cli/controller/context-pack.ts +97 -76
- package/src/cli/controller/operational-plan.ts +1 -1
- package/src/cli/controller/systemd-user.ts +4 -10
- package/src/cli/editing/edit-session.ts +17 -5
- package/src/cli/github/github.ts +4 -4
- package/src/cli/github/semantic-projection.ts +174 -0
- package/src/cli/hook/prompt-intents.ts +0 -9
- package/src/cli/index.ts +1 -43
- package/src/cli/local-bridge/connector-freshness.ts +1 -12
- package/src/cli/local-bridge/console-automations.ts +1 -1
- package/src/cli/local-bridge/console-view-models.ts +1 -1
- package/src/cli/local-bridge/facade-api.ts +0 -2
- package/src/cli/repositories/command-classifier.ts +0 -9
- package/src/cli/repositories/command-scope.ts +2 -2
- package/src/cli/repositories/locks.ts +20 -0
- package/src/cli/repositories/registry.ts +2 -1
- package/src/cli/repositories/safe-patch.ts +2 -12
- package/src/cli/repositories/structured-git.ts +39 -3
- package/src/cli/repository/inspector.ts +185 -343
- package/src/effects/process-runner.ts +17 -1
- package/src/runtime/context/generic-lsp-provider.ts +1 -1
- package/src/runtime/context/semantic-navigation-contract.ts +7 -0
- package/src/runtime/context/semantic-navigation.ts +1 -1
- package/src/runtime/control-plane/cleanup-artifact-retention.ts +4 -4
- package/src/runtime/control-plane/execution/work-delivery-service.ts +150 -31
- package/src/runtime/control-plane/execution/work-handle-authority.ts +12 -4
- package/src/runtime/control-plane/execution/work-handle-store.ts +53 -1
- package/src/runtime/control-plane/execution/work-operation-service.ts +20 -10
- package/src/runtime/control-plane/execution/work-preparation-service.ts +108 -4
- package/src/runtime/control-plane/execution/work-process-evidence.ts +2 -2
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +28 -13
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +5 -1
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +1 -1
- package/src/runtime/control-plane/facade/capability-registry.ts +2 -0
- package/src/runtime/control-plane/facade/work-continuation.ts +1 -1
- package/src/runtime/control-plane/generated-cache-retention.ts +114 -63
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +20 -8
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +1 -11
- package/src/runtime/control-plane/governance/external-effects.ts +1 -1
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +1 -0
- package/src/runtime/control-plane/runtime-cleanup.ts +12 -93
- package/src/runtime/evidence/sensitive-output.ts +22 -2
- package/src/runtime/execution/jobs/child-reference.ts +11 -0
- package/src/runtime/execution/jobs/receipt-recovery.ts +1 -11
- package/src/runtime/execution/process-runtime/command-facade.ts +3 -3
- package/src/runtime/execution/process-runtime/runtime.ts +20 -16
- package/src/runtime/execution/thin-harness/execution-router.ts +16 -16
- package/src/runtime/execution/thin-harness/fast-executor.ts +2 -2
- package/src/runtime/execution/thin-harness/search-worker.ts +2 -2
- package/src/runtime/execution/workers/executor.ts +1 -2
- package/src/runtime/maintenance/cleanup.ts +41 -5
- package/src/runtime/plugins/action-application.ts +12 -1
- package/src/runtime/plugins/app-store-connect-adapter.ts +252 -4
- package/src/runtime/plugins/browser-adapter.ts +381 -313
- package/src/runtime/plugins/browser-macos-bridge.ts +114 -112
- package/src/runtime/plugins/browser-manifest-surface.ts +2 -1
- package/src/runtime/plugins/browser-registration.ts +2 -0
- package/src/runtime/plugins/browser-runtime.ts +26 -35
- package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +11 -2
- package/src/runtime/plugins/computer-registration.ts +76 -9
- package/src/runtime/plugins/github-adapter.ts +30 -3
- package/src/runtime/plugins/gmail-adapter.ts +166 -1
- package/src/runtime/plugins/google-shared.ts +10 -5
- package/src/runtime/plugins/ios/remote-xpc-hid.ts +70 -30
- package/src/runtime/plugins/ios-adapter.ts +45 -40
- package/src/runtime/plugins/ios-agent-device.ts +54 -4
- package/src/runtime/plugins/ios-device-ownership.ts +103 -0
- package/src/runtime/plugins/ios-physical-device.ts +497 -79
- package/src/runtime/plugins/local-system-adapter.ts +1 -99
- package/src/runtime/plugins/store.ts +80 -7
- package/src/runtime/plugins/types.ts +4 -0
- package/src/runtime/projections/git-status-sampler.ts +2 -19
- package/src/runtime/recovery/actions.ts +1 -1
- package/src/runtime/recovery/lifecycle-inventory.ts +1 -11
- package/src/runtime/recovery/probe.ts +2 -2
- package/src/runtime/release/release-coordinator.ts +20 -4
- package/src/runtime/root/controller-progression-composition.ts +67 -5
- package/src/runtime/root/package-connector-service.ts +123 -11
- package/src/runtime/root/package-runtime-release.ts +6 -18
- package/src/runtime/root/package-runtime-service.ts +7 -13
- package/src/runtime/root/release-execution-canary.ts +18 -5
- package/src/runtime/root/release-manifest.ts +0 -34
- package/src/runtime/root/release-materialize.ts +0 -78
- package/src/runtime/root/runtime.ts +1 -2
- package/src/runtime/root/scheduled-controller-composition.ts +2 -1
- package/src/runtime/root/service.ts +90 -11
- package/src/runtime/root/types.ts +0 -6
- package/src/runtime/root/workflow-supervisor-composition.ts +207 -19
- package/src/runtime/root/workflow-supervisor-runtime.ts +365 -10
- package/src/runtime/safe-tooling/google-credential-store.ts +1 -11
- package/src/runtime/safe-tooling/ios-development.ts +22 -8
- package/src/runtime/safe-tooling/job-summary.ts +1 -1
- package/src/runtime/safe-tooling/workspace-auth.ts +4 -1
- package/src/runtime/shared/atomic-text-file.ts +21 -0
- package/src/runtime/shared/json-files.ts +8 -3
- package/src/runtime/standalone-recovery/core.ts +189 -174
- package/src/runtime/standalone-recovery/entry.ts +62 -30
- package/src/runtime/standalone-recovery/operation-lock.ts +11 -1
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +1 -0
- package/src/runtime/workflow/schedules/engine.ts +36 -14
- package/src/runtime/workflow/schedules/pending-activation.ts +11 -4
- package/src/runtime/workflow/schedules/work-continuation.ts +51 -6
- package/supervisor/client.ts +31 -14
- package/supervisor/control-plane.ts +210 -115
- package/supervisor/forge-validators.ts +88 -21
- package/supervisor/native-browser-adapter.ts +393 -28
- package/supervisor/native-messaging/host.ts +21 -2
- package/supervisor/protocol.ts +74 -26
- package/supervisor/server.ts +16 -8
- package/supervisor/store.ts +476 -57
- package/supervisor/types.ts +19 -13
- package/SKILL.md +0 -427
- package/adapters/computer/chatgpt-target-router.ts +0 -103
- package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +0 -87
- package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +0 -1
- package/adapters/mcp/toolset-selection.ts +0 -41
- package/agents/openai.yaml +0 -4
- package/assets/skill-hooks.json +0 -41
- package/assets/skill-version.json +0 -189
- package/assets/skills/claude-review/SKILL.md +0 -231
- package/assets/skills/codex-review/SKILL.md +0 -103
- package/assets/templates/helpers/check-skill-version.ts +0 -193
- package/scripts/check-skill-version.ts +0 -5
- package/scripts/ensure-codegraph.sh +0 -16
- package/scripts/run-skill-hook.ts +0 -244
- package/scripts/sync-codex-installed-copies.sh +0 -182
- package/src/cli/commands/tools.ts +0 -167
- package/src/cli/mcp/access-mode.ts +0 -2
- package/src/cli/mcp/access-tools.ts +0 -2
- package/src/cli/mcp/audit.ts +0 -2
- package/src/cli/mcp/auth.ts +0 -2
- package/src/cli/mcp/instructions.ts +0 -2
- package/src/cli/mcp/legacy-context.ts +0 -2
- package/src/cli/mcp/legacy-tool-service.ts +0 -2
- package/src/cli/mcp/multi-repository.ts +0 -2
- package/src/cli/mcp/oauth.ts +0 -2
- package/src/cli/mcp/openai-secure-tunnel.ts +0 -2
- package/src/cli/mcp/paths.ts +0 -2
- package/src/cli/mcp/policy.ts +0 -2
- package/src/cli/mcp/post-finalize-work-attribution.ts +0 -2
- package/src/cli/mcp/proxy-env.ts +0 -2
- package/src/cli/mcp/readiness-projection.ts +0 -2
- package/src/cli/mcp/redaction.ts +0 -2
- package/src/cli/mcp/repo.ts +0 -2
- package/src/cli/mcp/repository-tools.ts +0 -2
- package/src/cli/mcp/server.ts +0 -2
- package/src/cli/mcp/setup.ts +0 -2
- package/src/cli/mcp/tool-capability-cache.ts +0 -2
- package/src/cli/mcp/tool-contract.ts +0 -2
- package/src/cli/mcp/tools.ts +0 -2
- package/src/cli/mcp/toolset-names.ts +0 -2
- package/src/cli/mcp/toolset-selection.ts +0 -2
- package/src/cli/mcp/toolset.ts +0 -2
- package/src/cli/mcp/transports/http.ts +0 -2
- package/src/cli/mcp/transports/session-registry.ts +0 -2
- package/src/cli/mcp/transports/stdio.ts +0 -2
- package/src/cli/mcp/types.ts +0 -2
- package/src/cli/mcp/umbrella-tools.ts +0 -2
- package/src/cli/tools/codegraph.ts +0 -695
- package/src/core/adoption/helper-wrapper-plan.ts +0 -159
- package/src/runtime/context/codegraph-cache-boundary.ts +0 -255
- package/src/runtime/context/codegraph-read-provider.ts +0 -611
- package/src/runtime/context/codegraph-sidecar.cjs +0 -248
- package/src/runtime/control-plane/codegraph-cache-retention.ts +0 -164
- package/src/runtime/gateway/mcp/execution-tools.ts +0 -2
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +0 -2
- package/src/runtime/gateway/mcp/persisted-check-process.ts +0 -2
- package/src/runtime/gateway/mcp/process-tools.ts +0 -2
- package/src/runtime/gateway/mcp/router.ts +0 -2
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +0 -2
- package/src/runtime/gateway/mcp/runtime-tools.ts +0 -2
- package/src/runtime/gateway/mcp/work-validation-reconciler.ts +0 -2
- package/src/runtime/plugins/browser-provider-registry.ts +0 -2
- 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 {
|
|
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 =
|
|
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 =
|
|
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
|
|
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(() =>
|
|
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;
|
|
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.
|
|
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,
|
|
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 =
|
|
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
|
|
513
|
-
const
|
|
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 =
|
|
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 '
|
|
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:
|
|
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
|
-
|
|
23
|
-
|
|
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 {
|