@moretea-labs/forge 1.6.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -0
- package/CHANGELOG.md +39 -0
- package/CLAUDE.md +4 -0
- package/README.md +2 -2
- package/README.zh-CN.md +1 -1
- package/SKILL.md +2 -2
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/agents/openai.yaml +1 -1
- package/assets/hooks/hook-input.sh +11 -8
- package/assets/hooks/lib/session-state.sh +10 -7
- package/assets/hooks/lib/workflow-state.sh +13 -177
- package/assets/hooks/post-tool-observer.sh +8 -2
- package/assets/hooks/prompt-guard.sh +5 -81
- package/assets/hooks/session-start-context.sh +56 -1
- package/assets/partials/05-workflow.partial.md +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +2 -0
- package/assets/partials-agents/03-orchestration.partial.md +1 -1
- package/assets/plugin-registry.v1.json +3 -3
- package/assets/reference-configs/document-generation.md +1 -1
- package/assets/reference-configs/harness-overview.md +1 -1
- package/assets/reference-configs/sprint-contracts.md +3 -3
- package/assets/skill-version.json +2 -2
- package/assets/templates/contract.template.md +2 -7
- package/assets/templates/helpers/capture-plan.sh +2 -2
- package/assets/templates/helpers/check-architecture-sync.sh +17 -0
- package/assets/templates/helpers/check-task-workflow.sh +4 -4
- package/assets/templates/helpers/contract-worktree.sh +1 -21
- package/assets/templates/helpers/ensure-task-workflow.sh +11 -36
- package/assets/templates/helpers/inspect-project-state.ts +8 -6
- package/assets/templates/helpers/migrate-project-template.sh +22 -45
- package/assets/templates/helpers/migrate-workflow-docs.ts +9 -7
- package/assets/templates/helpers/new-plan.sh +1 -1
- package/assets/templates/helpers/plan-to-todo.sh +12 -37
- package/assets/templates/helpers/ship-worktrees.sh +2 -29
- package/assets/templates/helpers/verify-contract.sh +1 -111
- package/assets/templates/helpers/verify-sprint.sh +3 -180
- package/assets/templates/helpers/workflow-contract.ts +5 -1
- package/assets/templates/plan.template.md +1 -1
- package/assets/templates/review.template.md +9 -34
- package/assets/workflow-contract.v1.json +10 -19
- package/docs/README.md +10 -10
- package/docs/architecture/CURRENT.md +178 -0
- package/docs/architecture/EVOLUTION.md +42 -0
- package/docs/architecture/current/README.md +4 -156
- package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +2 -2
- package/docs/architecture/decisions/20260823-immediate-controller-relay.md +53 -0
- package/docs/architecture/decisions/20260826-browser-runtime-v2.md +73 -0
- package/docs/architecture/history/README.md +7 -0
- package/docs/architecture/{chatgpt-handoff-facade.md → history/chatgpt-handoff-facade.md} +4 -0
- package/docs/architecture/{global-hook-runtime.md → history/global-hook-runtime.md} +7 -3
- package/docs/architecture/{ios-semantic-automation-provider-v2.md → history/ios-semantic-automation-provider-v2.md} +6 -2
- package/docs/architecture/history.md +3 -34
- package/docs/architecture/index.md +15 -98
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
- package/docs/architecture/reviews/2026-08-22-semantic-harness-convergence-acceptance.md +39 -0
- package/docs/architecture/versions/1.6.md +38 -0
- package/docs/architecture/versions/1.7.md +41 -0
- package/docs/forge-chatgpt-browser-engine.md +2 -2
- package/docs/forge-chatgpt-mcp-setup.md +4 -28
- package/docs/forge-chatgpt-supervised-automation.md +3 -3
- package/docs/forge-github-issue-launcher.md +1 -1
- package/docs/forge-plugin-management.md +1 -1
- package/docs/forge-runtime-self-healing-loop.md +1 -1
- package/docs/operations/controller-browser-plugin.md +26 -6
- package/docs/operations/controller-performance-and-502.md +143 -0
- package/docs/operations/controller-reliability-runbook.md +1 -1
- package/docs/operations/features.md +2 -2
- package/docs/operations/github-repository.md +1 -1
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/mcp-tool-exposure.md +5 -3
- package/docs/wiki/Architecture.md +6 -6
- package/docs/wiki/Implementation.md +2 -2
- package/docs/wiki/Integrations.md +2 -2
- package/docs/wiki/Work-Lifecycle.md +1 -1
- package/package.json +33 -11
- package/scripts/activate-source-baseline.ts +17 -13
- package/scripts/architecture-event.ts +2 -820
- package/scripts/architecture-queue.sh +2 -645
- package/scripts/archive-architecture-request.sh +2 -242
- package/scripts/archive-workflow.sh +1 -291
- package/scripts/benchmark-background-check-overlap.ts +228 -0
- package/scripts/benchmark-mcp-host.sh +117 -0
- package/scripts/benchmark-mcp-transport.ts +170 -0
- package/scripts/benchmark-quality-harness.ts +146 -0
- package/scripts/benchmark-route-session-concurrency.ts +207 -6
- package/scripts/benchmark-thin-harness-mcp-e2e.ts +7 -1
- package/scripts/benchmark-typescript-navigation.ts +186 -0
- package/scripts/benchmark-work-efficiency.ts +334 -0
- package/scripts/capability-config.ts +2 -392
- package/scripts/capability-resolver.ts +2 -503
- package/scripts/capture-plan.sh +1 -332
- package/scripts/check-agent-tooling.sh +2 -1558
- package/scripts/check-architecture-sync.sh +2 -371
- package/scripts/check-brain-manifest.sh +2 -253
- package/scripts/check-context-files.sh +2 -47
- package/scripts/check-deploy-sql-order.sh +2 -115
- package/scripts/check-mcp-compatibility.ts +14 -0
- package/scripts/check-runtime-architecture.mjs +349 -79
- package/scripts/check-skill-version.ts +3 -191
- package/scripts/check-source-duplication.mjs +61 -0
- package/scripts/check-stable-baseline.ts +92 -0
- package/scripts/check-task-sync.sh +2 -63
- package/scripts/check-task-workflow.sh +2 -1000
- package/scripts/codex-handoff-resume.sh +2 -267
- package/scripts/context-contract-sync.sh +2 -514
- package/scripts/contract-run.ts +2 -463
- package/scripts/contract-worktree.sh +1 -722
- package/scripts/ensure-task-workflow.sh +1 -1225
- package/scripts/host-temporary-cloud-mcp-e2e.sh +265 -0
- package/scripts/inspect-project-state.ts +4 -279
- package/scripts/install-standalone-recovery.ts +24 -0
- package/scripts/lib/project-init-lib.sh +22 -46
- package/scripts/lib/run-forge-helper.sh +19 -0
- package/scripts/lib/run-forge-helper.ts +26 -0
- package/scripts/migrate-project-template.sh +1 -1189
- package/scripts/migrate-workflow-docs.ts +4 -412
- package/scripts/new-plan.sh +1 -180
- package/scripts/new-spec.sh +1 -47
- package/scripts/new-sprint.sh +1 -46
- package/scripts/plan-to-todo.sh +1 -796
- package/scripts/prepare-codex-handoff.sh +2 -201
- package/scripts/prepare-handoff.sh +1 -78
- package/scripts/refresh-current-status.sh +2 -467
- package/scripts/run-governed-gate.ts +5 -1
- package/scripts/select-agent-context-blocks.sh +2 -64
- package/scripts/ship-worktrees.sh +1 -558
- package/scripts/smoke-ios-physical-device.ts +4 -2
- package/scripts/smoke-runtime-recovery.ts +1 -35
- package/scripts/smoke-schedule-engine.ts +1 -20
- package/scripts/sprint-backlog.sh +1 -834
- package/scripts/stage-runtime-release.ts +2 -1
- package/scripts/summarize-failures.sh +2 -123
- package/scripts/switch-plan.sh +1 -150
- package/scripts/sync-brain-docs.sh +2 -379
- package/scripts/sync-generated-authority.mjs +175 -0
- package/scripts/verify-contract.sh +2 -703
- package/scripts/verify-forge-runtime.sh +0 -1
- package/scripts/verify-sprint.sh +1 -677
- package/scripts/workflow-contract.ts +4 -205
- package/scripts/workstream-sync.sh +2 -261
- package/src/cli/agent-jobs/executor-health.ts +1 -1
- package/src/cli/commands/init-hook.ts +3 -3
- package/src/cli/commands/mcp.ts +9 -5
- package/src/cli/commands/plugin.ts +12 -3
- package/src/cli/commands/recovery.ts +25 -3
- package/src/cli/commands/runtime.ts +55 -3
- package/src/cli/commands/security.ts +12 -1
- package/src/cli/commands/setup-profile.ts +31 -21
- package/src/cli/controller/check-runner.ts +113 -5
- package/src/cli/controller/composite-result.ts +1 -1
- package/src/cli/controller/context/focus.ts +51 -0
- package/src/cli/controller/context/instruction-context.ts +114 -0
- package/src/cli/controller/context/known-paths.ts +76 -0
- package/src/cli/controller/context/query-planning.ts +110 -0
- package/src/cli/controller/context/rule-resolution.ts +532 -0
- package/src/cli/controller/context/source-materializer.ts +278 -0
- package/src/cli/controller/context/structural.ts +16 -0
- package/src/cli/controller/context/types.ts +286 -0
- package/src/cli/controller/context-pack.ts +828 -487
- package/src/cli/controller/launch-agents.ts +77 -2
- package/src/cli/controller/operational-plan.ts +2 -2
- package/src/cli/controller/work-mode.ts +6 -4
- package/src/cli/editing/edit-session.ts +60 -7
- package/src/cli/github/contracts.ts +16 -0
- package/src/cli/github/github.ts +135 -16
- package/src/cli/github/session.ts +2 -1
- package/src/cli/installer/managed-entries.ts +15 -1
- package/src/cli/local-bridge/console-automations.ts +54 -78
- package/src/cli/local-bridge/console-view-models.ts +1 -35
- package/src/cli/local-bridge/facade-api.ts +37 -264
- package/src/cli/local-bridge/frontend/src/App.tsx +3 -3
- package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +23 -13
- package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +37 -4
- package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +6 -4
- package/src/cli/local-bridge/frontend/src/lib/api.ts +1 -6
- package/src/cli/local-bridge/frontend/src/lib/routes.ts +1 -1
- package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -2
- package/src/cli/local-bridge/frontend/src/styles.css +1 -1
- package/src/cli/local-bridge/frontend/src/types.ts +5 -8
- package/src/cli/local-bridge/job-store.ts +1 -490
- package/src/cli/local-bridge/server.ts +2 -515
- package/src/cli/local-bridge/ui-dist/app.css +1 -1
- package/src/cli/local-bridge/ui-dist/app.js +3703 -3757
- package/src/cli/mcp/auth.ts +5 -5
- package/src/cli/mcp/instructions.ts +3 -2
- package/src/cli/mcp/legacy-tool-service.ts +10 -222
- package/src/cli/mcp/multi-repository.ts +16 -5
- package/src/cli/mcp/oauth.ts +175 -26
- package/src/cli/mcp/post-finalize-work-attribution.ts +150 -0
- package/src/cli/mcp/repository-tools.ts +282 -146
- package/src/cli/mcp/server.ts +586 -116
- package/src/cli/mcp/tool-capability-cache.ts +1 -1
- package/src/cli/mcp/tool-contract.ts +14 -0
- package/src/cli/mcp/toolset-selection.ts +1 -1
- package/src/cli/mcp/toolset.ts +44 -7
- package/src/cli/mcp/transports/http.ts +124 -14
- package/src/cli/mcp/transports/session-registry.ts +43 -5
- package/src/cli/mcp/types.ts +3 -3
- package/src/cli/repositories/command-classifier.ts +103 -21
- package/src/cli/repositories/command-executor.ts +78 -526
- package/src/cli/repositories/command-process.ts +108 -0
- package/src/cli/repositories/command-scope.ts +99 -41
- package/src/cli/repositories/controller-home.ts +7 -8
- package/src/cli/repositories/git-command-executor.ts +2 -0
- package/src/cli/repositories/locks.ts +53 -16
- package/src/cli/repositories/repository-snapshot.ts +124 -0
- package/src/cli/repositories/selected-path-actions.ts +17 -9
- package/src/cli/repositories/structured-git.ts +118 -6
- package/src/cli/repository/inspector.ts +563 -65
- package/src/cli/repository/session-cache.ts +58 -17
- package/src/cli/v81-entry.ts +1 -4
- package/src/effects/fs-transaction.ts +85 -17
- package/src/runtime/context/codegraph-read-provider.ts +233 -2
- package/src/runtime/context/codegraph-sidecar.cjs +74 -40
- package/src/runtime/context/generic-lsp-provider.ts +355 -0
- package/src/runtime/context/lsp-client.ts +283 -0
- package/src/runtime/context/semantic-navigation-contract.ts +61 -0
- package/src/runtime/context/semantic-navigation.ts +223 -0
- package/src/runtime/context/swift-navigation.ts +565 -0
- package/src/runtime/context/typescript-navigation.ts +226 -0
- package/src/runtime/control-plane/dispatch-priority.ts +28 -0
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +48 -16
- package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +86 -24
- package/src/runtime/control-plane/execution/repository-work-attribution.ts +106 -0
- package/src/runtime/control-plane/execution/retained-work-resume.ts +203 -0
- package/src/runtime/control-plane/execution/session-store.ts +18 -0
- package/src/runtime/control-plane/execution/verification-evidence.ts +58 -3
- package/src/runtime/control-plane/execution/work-completion-authority.ts +58 -0
- package/src/runtime/control-plane/execution/work-evidence-policy.ts +200 -0
- package/src/runtime/control-plane/execution/work-execution-support.ts +386 -0
- package/src/runtime/control-plane/execution/work-finalization-service.ts +1913 -0
- package/src/runtime/control-plane/execution/work-handle-authority.ts +96 -0
- package/src/runtime/control-plane/execution/work-handle-store.ts +27 -2
- package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +258 -0
- package/src/runtime/control-plane/execution/work-operation-service.ts +468 -0
- package/src/runtime/control-plane/execution/work-path-scope.ts +37 -0
- package/src/runtime/control-plane/execution/work-preparation-service.ts +479 -0
- package/src/runtime/control-plane/execution/work-process-evidence.ts +129 -0
- package/src/runtime/control-plane/execution/work-remote-delivery.ts +215 -0
- package/src/runtime/control-plane/execution/work-task-receipt.ts +22 -18
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +156 -11
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +22 -14
- package/src/runtime/control-plane/execution/work-verification-context.ts +56 -0
- package/src/runtime/control-plane/execution/work-verification-service.ts +388 -0
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +90 -9
- package/src/runtime/control-plane/facade/capability-registry.ts +129 -25
- package/src/runtime/control-plane/facade/controller-round-relay.ts +1118 -0
- package/src/runtime/control-plane/facade/controller-session-store.ts +302 -12
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +17 -1
- package/src/runtime/control-plane/facade/goal-workloop.ts +928 -190
- package/src/runtime/control-plane/facade/index.ts +3 -0
- package/src/runtime/control-plane/facade/plan-contract-store.ts +275 -42
- package/src/runtime/control-plane/facade/repository-work-admission.ts +147 -0
- package/src/runtime/control-plane/facade/requirement-authority.ts +83 -0
- package/src/runtime/control-plane/facade/semantic-admission.ts +205 -0
- package/src/runtime/control-plane/facade/suggested-actions.ts +5 -1
- package/src/runtime/control-plane/facade/types.ts +90 -10
- package/src/runtime/control-plane/facade/work-contract-store.ts +157 -261
- package/src/runtime/control-plane/facade/work-state-machine.ts +250 -0
- package/src/runtime/control-plane/generated-cache-retention.ts +271 -0
- package/src/runtime/control-plane/global-scheduler/config.ts +44 -0
- package/src/runtime/control-plane/global-scheduler/dispatch-capacity.ts +80 -0
- package/src/runtime/control-plane/global-scheduler/durable-admission.ts +46 -0
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +152 -0
- package/src/runtime/control-plane/global-scheduler/projection-refresh.ts +84 -0
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -3
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +278 -543
- package/src/runtime/control-plane/global-scheduler/source-scan.ts +59 -0
- package/src/runtime/control-plane/global-scheduler/state.ts +88 -0
- package/src/runtime/control-plane/global-scheduler/worker-attachment.ts +57 -0
- package/src/runtime/control-plane/global-scheduler/worker-exit-decision.ts +38 -0
- package/src/runtime/control-plane/global-scheduler/worker-exit-reconciler.ts +175 -0
- package/src/runtime/control-plane/global-scheduler/worker-launch.ts +111 -0
- package/src/runtime/control-plane/global-scheduler/worker-lifecycle-store.ts +64 -0
- package/src/runtime/control-plane/global-scheduler/worker-lifecycle.ts +150 -0
- package/src/runtime/control-plane/global-scheduler/worker-process.ts +119 -0
- package/src/runtime/control-plane/global-scheduler/worker-stderr.ts +60 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +63 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +365 -117
- package/src/runtime/control-plane/launcher/launch-reservation-store.ts +89 -0
- package/src/runtime/control-plane/launcher/thin-launcher.ts +244 -11
- package/src/runtime/control-plane/persistence/controller-home-migration.ts +487 -0
- package/src/runtime/control-plane/persistence/requirement-store.ts +0 -12
- package/src/runtime/control-plane/persistence/sqlite-store.ts +33 -2
- package/src/runtime/control-plane/release-retention.ts +62 -2
- package/src/runtime/control-plane/repo-actor/registry.ts +4 -2
- package/src/runtime/control-plane/routing/route-policy.ts +24 -21
- package/src/runtime/control-plane/routing/workspace-admission.ts +70 -0
- package/src/runtime/control-plane/runtime-status-client.ts +0 -2
- package/src/runtime/diagnostics/mcp-timing.ts +13 -0
- package/src/runtime/diagnostics/mcp-transport-benchmark.ts +129 -0
- package/src/runtime/diagnostics/performance.ts +24 -1
- package/src/runtime/evidence/event-ledger.ts +1 -1
- package/src/runtime/evidence/process-check-execution.ts +18 -0
- package/src/runtime/execution/jobs/child-reference.ts +6 -2
- package/src/runtime/execution/jobs/store.ts +58 -4
- package/src/runtime/execution/jobs/types.ts +1 -1
- package/src/runtime/execution/managed-workspace.ts +203 -1
- package/src/runtime/execution/process-runtime/check-facade.ts +51 -4
- package/src/runtime/execution/process-runtime/check-result.ts +88 -0
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +7 -1
- package/src/runtime/execution/process-runtime/check-scheduling.ts +114 -0
- package/src/runtime/execution/process-runtime/command-facade.ts +367 -40
- package/src/runtime/execution/process-runtime/gc.ts +38 -6
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +1201 -0
- package/src/runtime/execution/process-runtime/persisted-check.ts +51 -5
- package/src/runtime/execution/process-runtime/resource-claims.ts +10 -6
- package/src/runtime/execution/process-runtime/runtime.ts +53 -3
- package/src/runtime/execution/process-runtime/store.ts +1 -0
- package/src/runtime/execution/process-runtime/types.ts +4 -12
- package/src/runtime/execution/thin-harness/async-process.ts +12 -2
- package/src/runtime/execution/thin-harness/search-worker.ts +54 -5
- package/src/runtime/execution/work-activity.ts +21 -0
- package/src/runtime/execution/workers/executor.ts +18 -61
- package/src/runtime/gateway/mcp/execution-tools.ts +35 -1852
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +61 -0
- package/src/runtime/gateway/mcp/process-tools.ts +83 -37
- package/src/runtime/gateway/mcp/router.ts +186 -84
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +565 -0
- package/src/runtime/gateway/mcp/runtime-tools.ts +2378 -1947
- package/src/runtime/plugins/app-store-connect-adapter.ts +216 -47
- package/src/runtime/plugins/browser-adapter.ts +2662 -829
- package/src/runtime/plugins/browser-automation-service.ts +10 -0
- package/src/runtime/plugins/browser-handoff-host.ts +2 -25
- package/src/runtime/plugins/browser-handoff.ts +110 -5
- package/src/runtime/plugins/browser-macos-bridge.ts +701 -63
- package/src/runtime/plugins/browser-manifest-surface.ts +603 -0
- package/src/runtime/plugins/browser-native-session-service.ts +222 -0
- package/src/runtime/plugins/browser-node-bridge.ts +6 -34
- package/src/runtime/plugins/browser-provider-registry.ts +109 -0
- package/src/runtime/plugins/browser-runtime-contract.ts +154 -0
- package/src/runtime/plugins/browser-runtime.ts +193 -0
- package/src/runtime/plugins/browser-session-authority.ts +332 -0
- package/src/runtime/plugins/browser-session-store.ts +121 -0
- package/src/runtime/plugins/browser-session-types.ts +98 -0
- package/src/runtime/plugins/capability-authorization-grants.ts +354 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +10 -4
- package/src/runtime/plugins/external-adapter.ts +358 -3
- package/src/runtime/plugins/external-unix-socket.ts +7 -1
- package/src/runtime/plugins/first-party-registry.ts +2 -0
- package/src/runtime/plugins/github-adapter.ts +125 -0
- package/src/runtime/plugins/ios/agent-device-provider.ts +1 -1
- package/src/runtime/plugins/ios/agent-device-typed-provider.ts +52 -9
- package/src/runtime/plugins/ios/remote-xpc-hid.ts +1171 -0
- package/src/runtime/plugins/ios-adapter.ts +106 -16
- package/src/runtime/plugins/ios-agent-device.ts +234 -64
- package/src/runtime/plugins/ios-physical-device.ts +1143 -392
- package/src/runtime/plugins/lightweight-action.ts +153 -0
- package/src/runtime/plugins/local-system-adapter.ts +123 -6
- package/src/runtime/plugins/macos-capability-broker.ts +47 -9
- package/src/runtime/plugins/plugin-action-sidecar.ts +59 -0
- package/src/runtime/plugins/plugin-management-adapter.ts +49 -8
- package/src/runtime/plugins/resend-adapter.ts +1 -1
- package/src/runtime/plugins/store.ts +330 -33
- package/src/runtime/plugins/types.ts +38 -3
- package/src/runtime/plugins/xiaohongshu-publish.ts +515 -0
- package/src/runtime/projections/git-status-sampler.ts +16 -16
- package/src/runtime/projections/materialized-view.ts +28 -10
- package/src/runtime/recovery/maintenance-executor.ts +426 -25
- package/src/runtime/root/mcp-transport.ts +180 -10
- package/src/runtime/root/package-connector-service.ts +230 -10
- package/src/runtime/root/package-runtime-release.ts +143 -17
- package/src/runtime/root/package-runtime-service.ts +461 -22
- package/src/runtime/root/release-manifest.ts +144 -18
- package/src/runtime/root/release-materialize.ts +125 -32
- package/src/runtime/root/release-store.ts +48 -11
- package/src/runtime/root/runtime.ts +23 -5
- package/src/runtime/root/service.ts +100 -29
- package/src/runtime/root/types.ts +12 -0
- package/src/runtime/safe-tooling/ios-development.ts +321 -63
- package/src/runtime/shared/trusted-executable.ts +62 -0
- package/src/runtime/shared/trusted-node-executable.ts +48 -0
- package/src/runtime/standalone-recovery/core.ts +1079 -240
- package/src/runtime/standalone-recovery/entry.ts +100 -24
- package/src/runtime/standalone-recovery/installer.ts +20 -4
- package/src/runtime/standalone-recovery/service-labels.ts +2 -0
- package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +160 -0
- package/src/runtime/workflow/schedules/browser-probe.ts +81 -3
- package/src/runtime/workflow/schedules/engine.ts +364 -202
- package/src/runtime/workflow/schedules/github-issue-watch.ts +158 -0
- package/src/runtime/workflow/schedules/settlement.ts +64 -17
- package/src/runtime/workflow/schedules/store.ts +209 -19
- package/src/runtime/workflow/schedules/types.ts +2 -4
- package/src/runtime/workflow/schedules/work-continuation.ts +26 -13
- package/src/testing/test-governance.ts +8 -40
- package/assets/reference-configs/heartbeat-triage.md +0 -57
- package/assets/templates/helpers/harness-trace-grade.sh +0 -150
- package/assets/templates/helpers/heartbeat-triage.sh +0 -361
- package/assets/templates/helpers/maintenance-triage.sh +0 -167
- package/docs/architecture/current/approved-target-architecture.zh-CN.md +0 -1501
- package/docs/architecture/current/architecture-invariants.md +0 -331
- package/docs/architecture/current/automation-and-schedule-engine.md +0 -459
- package/docs/architecture/current/codegraph-context-provider.md +0 -238
- package/docs/architecture/current/control-plane-authority-inventory.md +0 -94
- package/docs/architecture/current/controller-repository-decoupling.md +0 -148
- package/docs/architecture/current/dispatch-and-agent-strategy.md +0 -373
- package/docs/architecture/current/entity-model.md +0 -612
- package/docs/architecture/current/external-plugin-governance.md +0 -396
- package/docs/architecture/current/failure-recovery.md +0 -447
- package/docs/architecture/current/governance.md +0 -213
- package/docs/architecture/current/human-interaction-plane.md +0 -87
- package/docs/architecture/current/implementation-status.md +0 -128
- package/docs/architecture/current/job-and-run-lifecycle.md +0 -413
- package/docs/architecture/current/legacy-governance.md +0 -58
- package/docs/architecture/current/local-workspace-target-grants.md +0 -70
- package/docs/architecture/current/migration-roadmap.md +0 -105
- package/docs/architecture/current/multi-repository-execution.md +0 -371
- package/docs/architecture/current/operations-runbook.md +0 -45
- package/docs/architecture/current/peer-supercontroller-runtime.md +0 -32
- package/docs/architecture/current/personal-assistant-plugin-baseline.md +0 -206
- package/docs/architecture/current/repository-access-modes.md +0 -102
- package/docs/architecture/current/route-policy.md +0 -15
- package/docs/architecture/current/route-session-concurrency.md +0 -85
- package/docs/architecture/current/runtime-architecture-simplification.md +0 -214
- package/docs/architecture/current/runtime-directory-map.md +0 -49
- package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +0 -85
- package/docs/architecture/current/scheduler-and-resource-claims.md +0 -422
- package/docs/architecture/current/session-aware-execution-and-authorization.md +0 -77
- package/docs/architecture/current/system-overview.md +0 -169
- package/docs/architecture/current/target-requirements-traceability.md +0 -202
- package/docs/architecture/current/thin-harness-governance-policy.md +0 -32
- package/docs/architecture/current/thin-harness-v1.md +0 -321
- package/docs/architecture/current/verification-and-release-gates.md +0 -391
- package/docs/forge-autonomous-goal-loop.md +0 -9
- package/docs/forge-provider-configuration.md +0 -7
- package/docs/operations/chatgpt-personal-assistant-runtime.md +0 -93
- package/docs/operations/gmail-assistant-routines.md +0 -186
- package/docs/operations/personal-assistant-operator-guide.md +0 -61
- package/docs/operations/provider-configuration.md +0 -31
- package/scripts/harness-trace-grade.sh +0 -150
- package/scripts/heartbeat-triage.sh +0 -361
- package/scripts/maintenance-triage.sh +0 -167
- package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +0 -2
- package/src/cli/repositories/goal-registry.ts +0 -239
- package/src/runtime/assistant/action-proposals.ts +0 -279
- package/src/runtime/assistant/intent.ts +0 -395
- package/src/runtime/assistant/model-provider.ts +0 -460
- package/src/runtime/assistant/openapi.ts +0 -223
- package/src/runtime/assistant/policy.ts +0 -89
- package/src/runtime/assistant/readiness.ts +0 -258
- package/src/runtime/assistant/routine-runtime.ts +0 -477
- package/src/runtime/assistant/schedule-binding.ts +0 -171
- package/src/runtime/assistant/standing-grants.ts +0 -375
- package/src/runtime/assistant/store.ts +0 -243
- package/src/runtime/assistant/types.ts +0 -135
- package/src/runtime/control-plane/goal-loop/config-facade.ts +0 -700
- package/src/runtime/control-plane/goal-loop/config-store.ts +0 -570
- package/src/runtime/control-plane/goal-loop/config-types.ts +0 -213
- package/src/runtime/control-plane/goal-loop/executor-router.ts +0 -109
- package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +0 -149
- package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +0 -304
- package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +0 -1001
- package/src/runtime/control-plane/goal-loop/handoff-packet.ts +0 -160
- package/src/runtime/control-plane/goal-loop/index.ts +0 -13
- package/src/runtime/control-plane/goal-loop/local-tools.ts +0 -290
- package/src/runtime/control-plane/goal-loop/policy-gates.ts +0 -203
- package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +0 -93
- package/src/runtime/control-plane/goal-loop/provider-registry.ts +0 -545
- package/src/runtime/control-plane/goal-loop/types.ts +0 -378
- package/src/runtime/control-plane/startup-recovery.ts +0 -198
- package/src/runtime/gateway/mcp/resource-policy.ts +0 -186
- package/src/runtime/personal-assistant/gmail-triage-manager.ts +0 -231
- package/src/runtime/personal-assistant/reporting-runtime.ts +0 -344
- package/src/runtime/personal-assistant/triage-runtime.ts +0 -748
- package/src/runtime/workflow/findings/store.ts +0 -204
- package/src/runtime/workflow/findings/types.ts +0 -58
- package/src/runtime/workflow/portfolio/engine.ts +0 -20
- package/src/runtime/workflow/portfolio/store.ts +0 -185
- package/src/runtime/workflow/portfolio/types.ts +0 -29
|
@@ -3,17 +3,22 @@ import { execFile } from 'child_process';
|
|
|
3
3
|
import { promisify } from 'util';
|
|
4
4
|
import { getRepository } from '../../../cli/repositories/registry';
|
|
5
5
|
import { findExecutionJob, listActiveExecutionJobs, listExecutionJobs } from '../../execution/jobs/store';
|
|
6
|
+
import { executionJobBelongsToWork, workHasActiveExecution } from '../../execution/work-activity';
|
|
6
7
|
import { readRepositoryProjection } from '../../projections/materialized-view';
|
|
7
8
|
import {
|
|
8
9
|
applyRuntimeMaintenance,
|
|
9
10
|
previewAutomaticRuntimeMaintenance,
|
|
10
11
|
type RuntimeMaintenanceActionId,
|
|
11
12
|
} from '../../recovery/maintenance-executor';
|
|
12
|
-
import {
|
|
13
|
-
import { getControllerSession, getWorkContract, isTerminalWorkContractStatus, listHandoffItems } from '../../control-plane/facade';
|
|
13
|
+
import { appendWorkEvidence, controllerSessionBlocksRecovery, getControllerSession, getWorkContract, isTerminalWorkContractStatus, listHandoffItems } from '../../control-plane/facade';
|
|
14
14
|
import { launchSuperController } from '../../control-plane/launcher/thin-launcher';
|
|
15
15
|
import { getExternalControllerLaunchReservation } from '../../control-plane/launcher/launch-reservation-store';
|
|
16
|
-
import {
|
|
16
|
+
import { runStandaloneChatgptPrompt, runWorkChatgptContinuation } from '../../control-plane/launcher/chatgpt-work-continuation';
|
|
17
|
+
import {
|
|
18
|
+
beginInitialControllerRoundDispatch,
|
|
19
|
+
buildControllerRoundRelayPrompt,
|
|
20
|
+
finishControllerRoundRelayDispatch,
|
|
21
|
+
} from '../../control-plane/facade/controller-round-relay';
|
|
17
22
|
import {
|
|
18
23
|
getOccurrence,
|
|
19
24
|
getSchedule,
|
|
@@ -22,11 +27,12 @@ import {
|
|
|
22
27
|
recordScheduleOccurrenceHandoff,
|
|
23
28
|
listSchedules,
|
|
24
29
|
saveOccurrence,
|
|
25
|
-
saveSchedule,
|
|
26
30
|
saveScheduleDecision,
|
|
31
|
+
updateSchedule,
|
|
27
32
|
} from './store';
|
|
28
|
-
import { applyScheduleFailure } from './settlement';
|
|
33
|
+
import { applyScheduleFailure, applyScheduleRetryableFailure } from './settlement';
|
|
29
34
|
import { classifyScheduledBrowserObservation, executeScheduledBrowserProbe } from './browser-probe';
|
|
35
|
+
import { executeScheduledGithubIssueWatch } from './github-issue-watch';
|
|
30
36
|
import type {
|
|
31
37
|
RepositorySchedule,
|
|
32
38
|
ScheduleDecisionType,
|
|
@@ -37,16 +43,26 @@ import type {
|
|
|
37
43
|
const execFileAsync = promisify(execFile);
|
|
38
44
|
const LIVE_MAINTENANCE_OPERATION = 'runtime_maintenance_apply';
|
|
39
45
|
const EXTERNAL_CONTROLLER_WAKE_OPERATION = 'external_controller_wake';
|
|
40
|
-
const CHATGPT_BROWSER_PROMPT_OPERATION = 'chatgpt_browser_prompt';
|
|
41
46
|
const BROWSER_PROBE_OPERATION = 'browser_probe';
|
|
47
|
+
const GITHUB_ISSUE_WATCH_OPERATION = 'github_issue_watch';
|
|
48
|
+
const DEFAULT_EXTERNAL_CONTROLLER_WAKE_TIMEOUT_MS = 60_000;
|
|
49
|
+
const MAX_EXTERNAL_CONTROLLER_WAKE_TIMEOUT_MS = 120_000;
|
|
42
50
|
|
|
43
51
|
function isDeterministicSchedule(schedule: RepositorySchedule): boolean {
|
|
44
|
-
// Kernel
|
|
45
|
-
// ChatGPT owns semantic work; the Scheduler only dispatches the configured prompt.
|
|
52
|
+
// Kernel executes only typed, bounded dispatch operations.
|
|
46
53
|
return schedule.action.operation === LIVE_MAINTENANCE_OPERATION
|
|
47
54
|
|| schedule.action.operation === EXTERNAL_CONTROLLER_WAKE_OPERATION
|
|
48
|
-
|| schedule.action.operation ===
|
|
49
|
-
|| schedule.action.operation ===
|
|
55
|
+
|| schedule.action.operation === BROWSER_PROBE_OPERATION
|
|
56
|
+
|| schedule.action.operation === GITHUB_ISSUE_WATCH_OPERATION;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function retiredScheduleReason(schedule: RepositorySchedule): string | undefined {
|
|
60
|
+
if (schedule.action.operation === 'chatgpt_browser_prompt') return 'RETIRED_WORK_FREE_PROMPT_SCHEDULE';
|
|
61
|
+
if (schedule.action.operation === 'assistant_routine_execute') return 'RETIRED_PERSONAL_ASSISTANT_SCHEDULE';
|
|
62
|
+
if ((schedule.trigger.condition as { kind?: string } | undefined)?.kind === 'candidate_observation_threshold') {
|
|
63
|
+
return 'RETIRED_CANDIDATE_FINDING_SCHEDULE';
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
50
66
|
}
|
|
51
67
|
|
|
52
68
|
function normalizedWindow(minutes: number, at = Date.now()): string {
|
|
@@ -107,6 +123,12 @@ function cronWindowKey(schedule: RepositorySchedule, at = Date.now()): string {
|
|
|
107
123
|
}
|
|
108
124
|
|
|
109
125
|
function triggerWindowKey(schedule: RepositorySchedule, context: ScheduleTriggerContext | undefined, at = Date.now()): string {
|
|
126
|
+
// Explicit control-plane triggers use a separate identity namespace from timer/cron windows.
|
|
127
|
+
// A caller-supplied event/request id is the stable retry key; otherwise each manual trigger is distinct.
|
|
128
|
+
if (context?.source === 'manual' && schedule.trigger.type !== 'manual') {
|
|
129
|
+
const stableManualKey = context.eventId?.trim();
|
|
130
|
+
return `manual:${stableManualKey || randomUUID()}`;
|
|
131
|
+
}
|
|
110
132
|
switch (schedule.trigger.type) {
|
|
111
133
|
case 'cron':
|
|
112
134
|
return cronWindowKey(schedule, at);
|
|
@@ -229,11 +251,6 @@ async function triggerDue(
|
|
|
229
251
|
const due = condition.kind === 'job_succeeded' ? job?.status === 'succeeded' : terminal;
|
|
230
252
|
return { due, reason: due ? undefined : `Condition ${condition.kind} is not met.`, evidence: { jobId: condition.jobId, status: job?.status ?? 'missing' } };
|
|
231
253
|
}
|
|
232
|
-
const threshold = Math.max(1, condition.observationThreshold ?? 2);
|
|
233
|
-
const finding = listCandidateFindings(controllerHome, schedule.repoId, { includeTerminal: true, limit: 1000 })
|
|
234
|
-
.find((entry) => entry.semanticKey === condition.semanticKey);
|
|
235
|
-
const due = Boolean(finding && finding.observationCount >= threshold);
|
|
236
|
-
return { due, reason: due ? undefined : 'Candidate observation threshold is not met.', evidence: { semanticKey: condition.semanticKey, observations: finding?.observationCount ?? 0, threshold } };
|
|
237
254
|
}
|
|
238
255
|
default:
|
|
239
256
|
return { due: false, reason: 'Unsupported Schedule trigger.' };
|
|
@@ -247,13 +264,17 @@ function workBoundScheduleWorkId(schedule: RepositorySchedule): string | undefin
|
|
|
247
264
|
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
248
265
|
}
|
|
249
266
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
267
|
+
const HUMAN_ONLY_HANDOFF_REASONS = new Set([
|
|
268
|
+
'policy_approval_required',
|
|
269
|
+
'missing_authorization',
|
|
270
|
+
'invalid_objective',
|
|
271
|
+
'destructive_action_requires_confirmation',
|
|
272
|
+
]);
|
|
273
|
+
|
|
274
|
+
function handoffRequiresHumanReview(item: ReturnType<typeof listHandoffItems>[number]): boolean {
|
|
275
|
+
if (item.approvalAction) return true;
|
|
276
|
+
if (!item.creationReason) return true;
|
|
277
|
+
return HUMAN_ONLY_HANDOFF_REASONS.has(item.creationReason);
|
|
257
278
|
}
|
|
258
279
|
|
|
259
280
|
async function stopReason(controllerHome: string, schedule: RepositorySchedule): Promise<string | undefined> {
|
|
@@ -263,8 +284,8 @@ async function stopReason(controllerHome: string, schedule: RepositorySchedule):
|
|
|
263
284
|
if (schedule.stopConditions.includes('human_review_required')) {
|
|
264
285
|
if (workId) {
|
|
265
286
|
const activeHandoff = listHandoffItems({ controllerHome, repoId: schedule.repoId, status: 'active', limit: 100 })
|
|
266
|
-
.find((item) => item.workId === workId);
|
|
267
|
-
if (activeHandoff) return `Work ${workId} has active Handoff ${activeHandoff.id} requiring review.`;
|
|
287
|
+
.find((item) => item.workId === workId && handoffRequiresHumanReview(item));
|
|
288
|
+
if (activeHandoff) return `Work ${workId} has active Handoff ${activeHandoff.id} requiring human review.`;
|
|
268
289
|
} else if (projection.currentAttention.length > 0) {
|
|
269
290
|
return 'Repository has jobs requiring human attention.';
|
|
270
291
|
}
|
|
@@ -351,6 +372,64 @@ function decideOccurrence(
|
|
|
351
372
|
return saveOccurrence(controllerHome, { ...occurrence, decision, decisionId, status, reason });
|
|
352
373
|
}
|
|
353
374
|
|
|
375
|
+
export type ChatgptWakeFailureClass = 'retryable_readiness' | 'semantic_wait' | 'user_action_required' | 'ordinary_failure';
|
|
376
|
+
|
|
377
|
+
export function externalControllerWakeTimeoutMs(value: unknown): number {
|
|
378
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return DEFAULT_EXTERNAL_CONTROLLER_WAKE_TIMEOUT_MS;
|
|
379
|
+
return Math.max(5_000, Math.min(Math.trunc(value), MAX_EXTERNAL_CONTROLLER_WAKE_TIMEOUT_MS));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export async function awaitExternalControllerWake<T>(operation: Promise<T>, timeoutMs: number): Promise<T> {
|
|
383
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
384
|
+
try {
|
|
385
|
+
return await Promise.race([
|
|
386
|
+
operation,
|
|
387
|
+
new Promise<T>((_, reject) => {
|
|
388
|
+
timer = setTimeout(() => reject(new Error(`CONTROLLER_WAKE_TOTAL_TIMEOUT: external Controller dispatch exceeded ${timeoutMs}ms.`)), timeoutMs);
|
|
389
|
+
}),
|
|
390
|
+
]);
|
|
391
|
+
} finally {
|
|
392
|
+
if (timer) clearTimeout(timer);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function classifyChatgptWakeFailure(reason: string): ChatgptWakeFailureClass {
|
|
397
|
+
const normalized = reason.toUpperCase();
|
|
398
|
+
if (
|
|
399
|
+
normalized.includes('CHATGPT_AUTOMATION_LOGIN_REQUIRED')
|
|
400
|
+
|| normalized.includes('PLUGIN_BROWSER_JAVASCRIPT_PERMISSION_REQUIRED')
|
|
401
|
+
|| normalized.includes('PLUGIN_MACOS_CAPABILITY_BROKER_CAPABILITY_UNSUPPORTED')
|
|
402
|
+
|| normalized.includes('AUTHENTICATION_REQUIRED')
|
|
403
|
+
|| normalized.includes('CONSENT_REQUIRED')
|
|
404
|
+
) return 'user_action_required';
|
|
405
|
+
if (normalized.includes('REPEATED_STATE:')) return 'semantic_wait';
|
|
406
|
+
const retryableMarkers = [
|
|
407
|
+
'PLUGIN_BROWSER_ATTACH_UNAVAILABLE',
|
|
408
|
+
'PLUGIN_BROWSER_DEPENDENCY_UNAVAILABLE',
|
|
409
|
+
'PLUGIN_BROWSER_NODE_HOST_UNAVAILABLE',
|
|
410
|
+
'PLUGIN_BROWSER_NODE_UNAVAILABLE',
|
|
411
|
+
'PLUGIN_BROWSER_ACTIVE_TAB_UNAVAILABLE',
|
|
412
|
+
'PLUGIN_BROWSER_SESSION_STATE_LOST',
|
|
413
|
+
'PLUGIN_SESSION_NOT_FOUND',
|
|
414
|
+
'PLUGIN_BROWSER_NATIVE_TAB_IDENTITY_UNPROVEN',
|
|
415
|
+
'PLUGIN_MACOS_CAPABILITY_BROKER_UNAVAILABLE',
|
|
416
|
+
'PLUGIN_BROWSER_NATIVE_FOREGROUND_ACTIVATOR_UNAVAILABLE',
|
|
417
|
+
'CHATGPT_CONTROLLER_BROWSER_ROOT_UNAVAILABLE',
|
|
418
|
+
'CHATGPT_CONTROLLER_BROWSER_FAILED',
|
|
419
|
+
'CHATGPT_AUTOMATION_COMPOSER_UNAVAILABLE',
|
|
420
|
+
'CHATGPT_AUTOMATION_INTELLIGENCE_CONTROL_UNAVAILABLE',
|
|
421
|
+
'CHATGPT_AUTOMATION_REPLACEMENT_SESSION_NOT_CONFIRMED',
|
|
422
|
+
'CONTROLLER_WAKE_TOTAL_TIMEOUT',
|
|
423
|
+
'HTTP 502',
|
|
424
|
+
'ECONNREFUSED',
|
|
425
|
+
'ECONNRESET',
|
|
426
|
+
'ETIMEDOUT',
|
|
427
|
+
'UND_ERR_CONNECT_TIMEOUT',
|
|
428
|
+
];
|
|
429
|
+
if (retryableMarkers.some((marker) => normalized.includes(marker))) return 'retryable_readiness';
|
|
430
|
+
return 'ordinary_failure';
|
|
431
|
+
}
|
|
432
|
+
|
|
354
433
|
function externalControllerLaunchArgs(args: Record<string, unknown>, controllerType: string): string[] {
|
|
355
434
|
const launchArgs = Array.isArray(args.launch_args) ? args.launch_args.map(String) : [];
|
|
356
435
|
if (controllerType !== 'chatgpt') return launchArgs;
|
|
@@ -365,90 +444,6 @@ function externalControllerLaunchArgs(args: Record<string, unknown>, controllerT
|
|
|
365
444
|
return launchArgs;
|
|
366
445
|
}
|
|
367
446
|
|
|
368
|
-
async function executeChatgptBrowserPrompt(
|
|
369
|
-
controllerHome: string,
|
|
370
|
-
schedule: RepositorySchedule,
|
|
371
|
-
occurrence: ScheduleOccurrence,
|
|
372
|
-
timestamp: string,
|
|
373
|
-
args: Record<string, unknown>,
|
|
374
|
-
): Promise<ScheduleOccurrence> {
|
|
375
|
-
const prompt = typeof args.prompt === 'string' ? args.prompt.trim() : '';
|
|
376
|
-
if (!prompt) {
|
|
377
|
-
return decideOccurrence(controllerHome, schedule, occurrence, 'operation_blocked', 'skipped', 'CHATGPT_BROWSER_PROMPT_REQUIRED');
|
|
378
|
-
}
|
|
379
|
-
const model = typeof args.model === 'string' ? args.model : 'gpt-5.6';
|
|
380
|
-
const reasoning = args.reasoning === 'medium' || args.reasoning === 'xhigh' ? args.reasoning : 'high';
|
|
381
|
-
const tabPolicy = args.tab_policy === 'reuse' || args.tab_policy === 'new' ? args.tab_policy : 'auto';
|
|
382
|
-
const dispatching = decideOccurrence(
|
|
383
|
-
controllerHome,
|
|
384
|
-
schedule,
|
|
385
|
-
occurrence,
|
|
386
|
-
'execute',
|
|
387
|
-
'running',
|
|
388
|
-
'Forge Scheduler is dispatching the configured prompt through the controller-owned ChatGPT browser session.',
|
|
389
|
-
occurrenceDecisionEvidence({ operation: CHATGPT_BROWSER_PROMPT_OPERATION, model, reasoning, tabPolicy }),
|
|
390
|
-
);
|
|
391
|
-
const result = await runScheduledChatgptPrompt({
|
|
392
|
-
controllerHome,
|
|
393
|
-
repoId: schedule.repoId,
|
|
394
|
-
automationId: schedule.scheduleId,
|
|
395
|
-
prompt,
|
|
396
|
-
title: schedule.name,
|
|
397
|
-
browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : undefined,
|
|
398
|
-
conversationUrl: typeof args.conversation_url === 'string' ? args.conversation_url : undefined,
|
|
399
|
-
model,
|
|
400
|
-
reasoning,
|
|
401
|
-
tabPolicy,
|
|
402
|
-
timeoutMs: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
|
|
403
|
-
});
|
|
404
|
-
if (result.status === 'failed') {
|
|
405
|
-
const reason = result.error?.message ?? 'ChatGPT browser prompt dispatch failed.';
|
|
406
|
-
const failed = applyScheduleFailure(controllerHome, schedule.scheduleId, schedule.repoId, occurrence.occurrenceId, {
|
|
407
|
-
outcome: 'failed',
|
|
408
|
-
decision: 'execute',
|
|
409
|
-
reason,
|
|
410
|
-
handoff: {
|
|
411
|
-
title: `ChatGPT scheduled prompt ${occurrence.occurrenceId} failed`,
|
|
412
|
-
summary: 'Forge recorded the trigger but could not dispatch the configured ChatGPT prompt.',
|
|
413
|
-
reason,
|
|
414
|
-
creationReason: 'repeated_infrastructure_failure',
|
|
415
|
-
blockingDecision: 'Repair ChatGPT/browser readiness before unattended prompt dispatch resumes.',
|
|
416
|
-
recommendedDecision: 'Inspect browser attachment and ChatGPT execution preference verification, then retrigger this automation.',
|
|
417
|
-
recommendedPrompt: `Inspect failed ChatGPT prompt occurrence ${occurrence.occurrenceId} for schedule ${schedule.scheduleId}.`,
|
|
418
|
-
statusSummary: 'Scheduled ChatGPT prompt dispatch failed.',
|
|
419
|
-
blockedBy: ['chatgpt_browser_prompt_failed'],
|
|
420
|
-
attemptedActions: [`schedule:${schedule.scheduleId}`, `browser-session:${result.browserSessionId}`],
|
|
421
|
-
},
|
|
422
|
-
});
|
|
423
|
-
const latest = getSchedule(controllerHome, schedule.repoId, schedule.scheduleId);
|
|
424
|
-
saveSchedule(controllerHome, { ...latest, lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId });
|
|
425
|
-
return failed.occurrence ?? saveOccurrence(controllerHome, { ...dispatching, status: 'failed', reason });
|
|
426
|
-
}
|
|
427
|
-
const latest = getSchedule(controllerHome, schedule.repoId, schedule.scheduleId);
|
|
428
|
-
const durableConversationUrl = result.conversationUrl && /\/c\/[^/?#]+/.test(result.conversationUrl)
|
|
429
|
-
? result.conversationUrl
|
|
430
|
-
: undefined;
|
|
431
|
-
saveSchedule(controllerHome, {
|
|
432
|
-
...latest,
|
|
433
|
-
action: durableConversationUrl && tabPolicy !== 'new'
|
|
434
|
-
? {
|
|
435
|
-
...latest.action,
|
|
436
|
-
arguments: { ...latest.action.arguments, conversation_url: durableConversationUrl },
|
|
437
|
-
}
|
|
438
|
-
: latest.action,
|
|
439
|
-
lastTriggeredAt: timestamp,
|
|
440
|
-
lastOccurrenceId: occurrence.occurrenceId,
|
|
441
|
-
consecutiveFailures: 0,
|
|
442
|
-
nextEligibleAt: undefined,
|
|
443
|
-
pausedReason: undefined,
|
|
444
|
-
});
|
|
445
|
-
return saveOccurrence(controllerHome, {
|
|
446
|
-
...dispatching,
|
|
447
|
-
status: 'succeeded',
|
|
448
|
-
reason: `ChatGPT prompt dispatched via ${result.browserSessionId}${result.reusedSession ? ' (reused)' : ' (new session)'}; execution preference ${result.executionPreferenceVerified ? 'verified' : 'not verified'}.`,
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
|
|
452
447
|
async function executeExternalControllerWake(
|
|
453
448
|
controllerHome: string,
|
|
454
449
|
schedule: RepositorySchedule,
|
|
@@ -456,6 +451,7 @@ async function executeExternalControllerWake(
|
|
|
456
451
|
timestamp: string,
|
|
457
452
|
args: Record<string, unknown>,
|
|
458
453
|
extraEvidence: Record<string, unknown> = {},
|
|
454
|
+
options: { transientWakeFailure?: boolean } = {},
|
|
459
455
|
): Promise<ScheduleOccurrence> {
|
|
460
456
|
const workId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
|
|
461
457
|
const controllerType = typeof args.controller_type === 'string' ? args.controller_type.trim() : 'chatgpt';
|
|
@@ -468,21 +464,27 @@ async function executeExternalControllerWake(
|
|
|
468
464
|
const workStore = { controllerHome, repoId: schedule.repoId };
|
|
469
465
|
const work = getWorkContract(workStore, workId);
|
|
470
466
|
if (!work) {
|
|
471
|
-
|
|
467
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({ enabled: false, pausedReason: `Work ${workId} no longer exists.`, lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId }));
|
|
472
468
|
return decideOccurrence(controllerHome, schedule, occurrence, 'operation_blocked', 'skipped', `EXTERNAL_CONTROLLER_WAKE_WORK_NOT_FOUND:${workId}`);
|
|
473
469
|
}
|
|
474
470
|
if (isTerminalWorkContractStatus(work.status)) {
|
|
475
|
-
|
|
471
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({ enabled: false, pausedReason: `Work ${workId} is terminal (${work.status}).`, lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId }));
|
|
476
472
|
return decideOccurrence(controllerHome, schedule, occurrence, 'nothing_to_do', 'skipped', `Work ${workId} is terminal (${work.status}); automatic continuation stopped.`);
|
|
477
473
|
}
|
|
478
474
|
const existingOwner = getControllerSession(workStore, workId);
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
475
|
+
const occurrenceNowMs = Date.parse(timestamp);
|
|
476
|
+
const nowMs = Number.isFinite(occurrenceNowMs) ? occurrenceNowMs : Date.now();
|
|
477
|
+
if (workHasActiveExecution(controllerHome, schedule.repoId, workId)) {
|
|
478
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({ lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId }));
|
|
479
|
+
return decideOccurrence(controllerHome, schedule, occurrence, 'nothing_to_do', 'skipped', `Work ${workId} has active execution; duplicate Controller wake suppressed.`);
|
|
480
|
+
}
|
|
481
|
+
if (existingOwner && controllerSessionBlocksRecovery(workStore, workId, { nowMs })) {
|
|
482
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({ lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId }));
|
|
483
|
+
return decideOccurrence(controllerHome, schedule, occurrence, 'nothing_to_do', 'skipped', `Work ${workId} has a recently active Controller ${existingOwner.controllerId}.`);
|
|
482
484
|
}
|
|
483
485
|
const launchReservation = getExternalControllerLaunchReservation(workStore, workId);
|
|
484
486
|
if (launchReservation) {
|
|
485
|
-
|
|
487
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({ lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId }));
|
|
486
488
|
return decideOccurrence(controllerHome, schedule, occurrence, 'nothing_to_do', 'skipped', `Work ${workId} already has a pending external Controller launch ${launchReservation.reservationId}.`);
|
|
487
489
|
}
|
|
488
490
|
const wakeDecision = decideOccurrence(
|
|
@@ -512,37 +514,79 @@ async function executeExternalControllerWake(
|
|
|
512
514
|
if (controllerType === 'chatgpt') {
|
|
513
515
|
const reasoning = args.reasoning === 'medium' || args.reasoning === 'xhigh' ? args.reasoning : 'high';
|
|
514
516
|
const tabPolicy = args.tab_policy === 'reuse' || args.tab_policy === 'new' ? args.tab_policy : 'auto';
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
517
|
+
const timeoutMs = externalControllerWakeTimeoutMs(args.timeout_ms);
|
|
518
|
+
let relay;
|
|
519
|
+
try {
|
|
520
|
+
relay = beginInitialControllerRoundDispatch(workStore, {
|
|
521
|
+
workId,
|
|
522
|
+
identity: {
|
|
523
|
+
controllerId: `schedule:${schedule.scheduleId}`,
|
|
524
|
+
principalId: 'forge-scheduler',
|
|
525
|
+
controllerInstanceId: 'forge-runtime-scheduler',
|
|
526
|
+
sessionId: occurrence.occurrenceId,
|
|
527
|
+
},
|
|
528
|
+
requirementId: work.requirementId,
|
|
529
|
+
browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : undefined,
|
|
530
|
+
conversationUrl: typeof args.conversation_url === 'string' ? args.conversation_url : undefined,
|
|
531
|
+
});
|
|
532
|
+
if (relay.status === 'blocked') throw new Error(`CONTROLLER_RELAY_LAUNCH_BLOCKED: ${relay.blockedReason ?? relay.relayScopeId}`);
|
|
533
|
+
const relayPrompt = `${buildControllerRoundRelayPrompt(workStore, relay, { exactOriginWork: true })}\n\nScheduled continuation hint: ${continuationPrompt}`;
|
|
534
|
+
const dispatched = await awaitExternalControllerWake(runWorkChatgptContinuation({
|
|
535
|
+
controllerHome,
|
|
536
|
+
repoId: schedule.repoId,
|
|
537
|
+
repoRoot: repository.canonicalRoot,
|
|
538
|
+
workId,
|
|
539
|
+
prompt: relayPrompt,
|
|
540
|
+
title: schedule.name,
|
|
541
|
+
browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : undefined,
|
|
542
|
+
conversationUrl: typeof args.conversation_url === 'string' ? args.conversation_url : undefined,
|
|
543
|
+
model: typeof args.model === 'string' ? args.model : 'gpt-5.6',
|
|
544
|
+
reasoning,
|
|
545
|
+
tabPolicy,
|
|
546
|
+
timeoutMs,
|
|
547
|
+
}), timeoutMs);
|
|
548
|
+
if (dispatched.status === 'failed') throw new Error(dispatched.error?.message ?? 'CHATGPT_WORK_CONTINUATION_FAILED');
|
|
549
|
+
const completedRelay = finishControllerRoundRelayDispatch(workStore, {
|
|
550
|
+
workId,
|
|
551
|
+
ok: true,
|
|
552
|
+
browserSessionId: dispatched.browserSessionId,
|
|
553
|
+
conversationUrl: dispatched.conversationUrl,
|
|
554
|
+
});
|
|
555
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, (current) => ({
|
|
556
|
+
lastTriggeredAt: timestamp,
|
|
557
|
+
lastOccurrenceId: occurrence.occurrenceId,
|
|
558
|
+
consecutiveFailures: 0,
|
|
559
|
+
nextEligibleAt: undefined,
|
|
560
|
+
...(current.enabled ? { pausedReason: undefined } : {}),
|
|
561
|
+
}));
|
|
562
|
+
const succeededOccurrence = saveOccurrence(controllerHome, {
|
|
563
|
+
...wakeDecision,
|
|
564
|
+
status: 'succeeded',
|
|
565
|
+
reason: `ChatGPT dispatch action succeeded via ${dispatched.browserSessionId}; relay ${completedRelay?.relayScopeId ?? relay.relayScopeId} remains dispatched until the new Controller claims Work ${workId}.`,
|
|
566
|
+
});
|
|
567
|
+
appendWorkEvidence(workStore, workId, {
|
|
568
|
+
evidenceId: succeededOccurrence.occurrenceId,
|
|
569
|
+
title: 'scheduled ChatGPT continuation dispatched',
|
|
570
|
+
summary: `Schedule ${schedule.scheduleId} timer occurrence ${succeededOccurrence.occurrenceId} dispatched the Work-bound ChatGPT continuation successfully.`,
|
|
571
|
+
detailLevel: 'summary',
|
|
572
|
+
});
|
|
573
|
+
return succeededOccurrence;
|
|
574
|
+
} catch (error) {
|
|
575
|
+
if (relay) {
|
|
576
|
+
finishControllerRoundRelayDispatch(workStore, {
|
|
577
|
+
workId,
|
|
578
|
+
ok: false,
|
|
579
|
+
error: error instanceof Error ? error.message : String(error),
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
if (error instanceof Error && error.message.startsWith('CONTROLLER_RELAY_ROUND_ALREADY_OPEN:')) {
|
|
583
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({ lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId }));
|
|
584
|
+
return decideOccurrence(controllerHome, schedule, wakeDecision, 'nothing_to_do', 'skipped', `Work ${workId} already has an open ChatGPT controller round awaiting claim or disposition.`);
|
|
585
|
+
}
|
|
586
|
+
throw error;
|
|
587
|
+
}
|
|
544
588
|
}
|
|
545
|
-
const launched = launchSuperController({
|
|
589
|
+
const launched = await launchSuperController({
|
|
546
590
|
work: { controllerHome, repoId: schedule.repoId },
|
|
547
591
|
handoff: { controllerHome, repoId: schedule.repoId },
|
|
548
592
|
}, {
|
|
@@ -557,15 +601,13 @@ async function executeExternalControllerWake(
|
|
|
557
601
|
continuationPrompt,
|
|
558
602
|
cwd: repository.canonicalRoot,
|
|
559
603
|
});
|
|
560
|
-
|
|
561
|
-
saveSchedule(controllerHome, {
|
|
562
|
-
...latest,
|
|
604
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, (current) => ({
|
|
563
605
|
lastTriggeredAt: timestamp,
|
|
564
606
|
lastOccurrenceId: occurrence.occurrenceId,
|
|
565
607
|
consecutiveFailures: 0,
|
|
566
608
|
nextEligibleAt: undefined,
|
|
567
|
-
pausedReason: undefined,
|
|
568
|
-
});
|
|
609
|
+
...(current.enabled ? { pausedReason: undefined } : {}),
|
|
610
|
+
}));
|
|
569
611
|
return saveOccurrence(controllerHome, {
|
|
570
612
|
...wakeDecision,
|
|
571
613
|
status: 'succeeded',
|
|
@@ -573,10 +615,33 @@ async function executeExternalControllerWake(
|
|
|
573
615
|
});
|
|
574
616
|
} catch (error) {
|
|
575
617
|
const reason = error instanceof Error ? error.message : String(error);
|
|
618
|
+
const failureClass = controllerType === 'chatgpt' ? classifyChatgptWakeFailure(reason) : 'ordinary_failure';
|
|
619
|
+
if (options.transientWakeFailure || failureClass === 'retryable_readiness' || failureClass === 'semantic_wait') {
|
|
620
|
+
const semanticWait = failureClass === 'semantic_wait';
|
|
621
|
+
const deferred = applyScheduleRetryableFailure(controllerHome, schedule.scheduleId, schedule.repoId, occurrence.occurrenceId, {
|
|
622
|
+
outcome: semanticWait ? 'skipped' : 'failed',
|
|
623
|
+
decision: semanticWait ? 'nothing_to_do' : 'execute',
|
|
624
|
+
reason: semanticWait
|
|
625
|
+
? `Controller wake deferred until durable state changes; schedule remains active: ${reason}`
|
|
626
|
+
: `Controller wake deferred by transient readiness; schedule remains active with bounded backoff: ${reason}`,
|
|
627
|
+
countFailure: !semanticWait,
|
|
628
|
+
});
|
|
629
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({
|
|
630
|
+
lastTriggeredAt: timestamp,
|
|
631
|
+
lastOccurrenceId: occurrence.occurrenceId,
|
|
632
|
+
}));
|
|
633
|
+
return deferred.occurrence ?? saveOccurrence(controllerHome, {
|
|
634
|
+
...wakeDecision,
|
|
635
|
+
status: semanticWait ? 'skipped' : 'failed',
|
|
636
|
+
decision: semanticWait ? 'nothing_to_do' : 'execute',
|
|
637
|
+
reason,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
576
640
|
const failed = applyScheduleFailure(controllerHome, schedule.scheduleId, schedule.repoId, occurrence.occurrenceId, {
|
|
577
641
|
outcome: 'failed',
|
|
578
642
|
decision: 'execute',
|
|
579
643
|
reason,
|
|
644
|
+
...(failureClass === 'user_action_required' ? { pauseReason: reason } : {}),
|
|
580
645
|
handoff: {
|
|
581
646
|
title: `External Controller wake ${occurrence.occurrenceId} failed`,
|
|
582
647
|
summary: 'Forge recorded the schedule trigger but could not wake the configured external Controller.',
|
|
@@ -590,8 +655,7 @@ async function executeExternalControllerWake(
|
|
|
590
655
|
attemptedActions: [`schedule:${schedule.scheduleId}`, `work:${workId}`, `controller:${controllerType}`],
|
|
591
656
|
},
|
|
592
657
|
});
|
|
593
|
-
|
|
594
|
-
saveSchedule(controllerHome, { ...latest, lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId });
|
|
658
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({ lastTriggeredAt: timestamp, lastOccurrenceId: occurrence.occurrenceId }));
|
|
595
659
|
return failed.occurrence ?? saveOccurrence(controllerHome, { ...wakeDecision, status: 'failed', reason });
|
|
596
660
|
}
|
|
597
661
|
}
|
|
@@ -602,6 +666,13 @@ export async function evaluateSchedule(
|
|
|
602
666
|
force = false,
|
|
603
667
|
triggerContext?: ScheduleTriggerContext,
|
|
604
668
|
): Promise<ScheduleOccurrence | undefined> {
|
|
669
|
+
// The persisted Schedule record is the authority. Callers may legitimately
|
|
670
|
+
// retain the object returned by create/list/get across multiple trigger
|
|
671
|
+
// attempts, so never let an older in-memory revision become execution state.
|
|
672
|
+
schedule = getSchedule(controllerHome, schedule.repoId, schedule.scheduleId);
|
|
673
|
+
// Preserve retired schedule records as evidence, but do not create a new
|
|
674
|
+
// occurrence or resume a controller/external action from their old payload.
|
|
675
|
+
if (retiredScheduleReason(schedule)) return undefined;
|
|
605
676
|
if (!schedule.enabled && !force) return undefined;
|
|
606
677
|
const due = await triggerDue(controllerHome, schedule, force, triggerContext);
|
|
607
678
|
if (!due.due) return undefined;
|
|
@@ -653,7 +724,7 @@ export async function evaluateSchedule(
|
|
|
653
724
|
attemptedActions: [`operation:${schedule.action.operation}`],
|
|
654
725
|
},
|
|
655
726
|
);
|
|
656
|
-
|
|
727
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({ lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId }));
|
|
657
728
|
return externalControllerHandoff;
|
|
658
729
|
}
|
|
659
730
|
|
|
@@ -690,7 +761,10 @@ export async function evaluateSchedule(
|
|
|
690
761
|
return decideOccurrence(controllerHome, schedule, occurrence, 'active_occurrence', 'skipped', 'Maximum active occurrences reached.');
|
|
691
762
|
}
|
|
692
763
|
if (schedule.consecutiveFailures >= schedule.policy.maxFailures) {
|
|
693
|
-
|
|
764
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, (current) => ({
|
|
765
|
+
enabled: false,
|
|
766
|
+
pausedReason: current.enabled ? 'Maximum consecutive failures reached.' : current.pausedReason,
|
|
767
|
+
}));
|
|
694
768
|
return decideOccurrence(controllerHome, schedule, occurrence, 'stopped', 'skipped', 'Schedule paused after repeated failures.');
|
|
695
769
|
}
|
|
696
770
|
if (schedule.nextEligibleAt && Date.parse(schedule.nextEligibleAt) > Date.now() && !force) {
|
|
@@ -744,14 +818,14 @@ export async function evaluateSchedule(
|
|
|
744
818
|
readyForExecution: preview.status.readyForExecution,
|
|
745
819
|
}));
|
|
746
820
|
}
|
|
747
|
-
|
|
821
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({ lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId }));
|
|
748
822
|
return decideOccurrence(controllerHome, schedule, occurrence, 'would_execute', 'shadowed', 'Shadow mode records the live maintenance decision without applying it.', occurrenceDecisionEvidence({
|
|
749
823
|
actionId: preview.actionId,
|
|
750
824
|
safeCandidates: preview.selectedCandidateIds.length,
|
|
751
825
|
typedSafeCandidates: preview.selectedTypedCandidateIds.length,
|
|
752
826
|
}));
|
|
753
827
|
}
|
|
754
|
-
|
|
828
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({ lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId }));
|
|
755
829
|
return decideOccurrence(controllerHome, schedule, occurrence, 'would_execute', 'shadowed', 'Shadow mode records the decision without modifying the repository.', due.evidence);
|
|
756
830
|
}
|
|
757
831
|
|
|
@@ -803,24 +877,93 @@ export async function evaluateSchedule(
|
|
|
803
877
|
return executeExternalControllerWake(controllerHome, schedule, occurrence, timestamp, args);
|
|
804
878
|
}
|
|
805
879
|
|
|
806
|
-
if (schedule.action.operation ===
|
|
807
|
-
|
|
880
|
+
if (schedule.action.operation === GITHUB_ISSUE_WATCH_OPERATION) {
|
|
881
|
+
try {
|
|
882
|
+
const repository = getRepository(schedule.repoId, controllerHome, { includeRemoved: true });
|
|
883
|
+
const observation = await executeScheduledGithubIssueWatch({ repoRoot: repository.canonicalRoot, args, observedAt: timestamp });
|
|
884
|
+
const evidence = occurrenceDecisionEvidence({
|
|
885
|
+
githubRepository: typeof args.github_repository === 'string' ? args.github_repository : undefined,
|
|
886
|
+
observationStatus: observation.status,
|
|
887
|
+
observedIssueCount: observation.issues.length,
|
|
888
|
+
changedIssueNumbers: observation.changedOpenIssues.map((issue) => issue.number),
|
|
889
|
+
});
|
|
890
|
+
const persistObservation = () => updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, (current) => {
|
|
891
|
+
if (current.action.operation !== GITHUB_ISSUE_WATCH_OPERATION) throw new Error('SCHEDULE_ACTION_CHANGED_DURING_GITHUB_OBSERVATION');
|
|
892
|
+
return {
|
|
893
|
+
action: {
|
|
894
|
+
...current.action,
|
|
895
|
+
arguments: { ...(current.action.arguments ?? {}), issue_watch_state: observation.nextState, issue_watch_since: undefined },
|
|
896
|
+
},
|
|
897
|
+
lastTriggeredAt: timestamp,
|
|
898
|
+
lastOccurrenceId: occurrenceId,
|
|
899
|
+
lastObservationAt: timestamp,
|
|
900
|
+
lastObservationStatus: observation.status,
|
|
901
|
+
lastObservationChangedAt: observation.shouldWake ? timestamp : current.lastObservationChangedAt,
|
|
902
|
+
consecutiveFailures: 0,
|
|
903
|
+
consecutiveNoops: observation.shouldWake ? 0 : (current.consecutiveNoops ?? 0) + 1,
|
|
904
|
+
nextEligibleAt: undefined,
|
|
905
|
+
...(current.enabled ? { pausedReason: undefined } : {}),
|
|
906
|
+
};
|
|
907
|
+
});
|
|
908
|
+
if (!observation.shouldWake) {
|
|
909
|
+
const observedSchedule = persistObservation();
|
|
910
|
+
const reason = observation.status === 'baseline'
|
|
911
|
+
? 'GitHub issue watcher baseline recorded; no Controller wake was emitted.'
|
|
912
|
+
: 'GitHub issue watcher found no new, reopened, or updated open issue.';
|
|
913
|
+
return decideOccurrence(controllerHome, observedSchedule, occurrence, 'nothing_to_do', 'skipped', reason, evidence);
|
|
914
|
+
}
|
|
915
|
+
const issueSummary = observation.changedOpenIssues.slice(0, 10)
|
|
916
|
+
.map((issue) => `#${issue.number} ${issue.title}`)
|
|
917
|
+
.join('; ');
|
|
918
|
+
const wakeArgs = {
|
|
919
|
+
...args,
|
|
920
|
+
continuation_prompt: typeof args.continuation_prompt === 'string'
|
|
921
|
+
? `${args.continuation_prompt}\n\nGitHub issue watcher detected: ${issueSummary}`
|
|
922
|
+
: `GitHub issue watcher detected actionable issue changes: ${issueSummary}. Read current Work/Plan/evidence and process these issues through the bounded repair workflow.`,
|
|
923
|
+
};
|
|
924
|
+
const wake = await executeExternalControllerWake(controllerHome, schedule, occurrence, timestamp, wakeArgs, evidence, { transientWakeFailure: true });
|
|
925
|
+
if (wake.status !== 'failed') persistObservation();
|
|
926
|
+
return wake;
|
|
927
|
+
} catch (error) {
|
|
928
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
929
|
+
const failed = applyScheduleFailure(controllerHome, schedule.scheduleId, schedule.repoId, occurrence.occurrenceId, {
|
|
930
|
+
outcome: 'failed',
|
|
931
|
+
decision: 'execute',
|
|
932
|
+
reason,
|
|
933
|
+
handoff: {
|
|
934
|
+
title: `GitHub issue watcher occurrence ${occurrence.occurrenceId} failed`,
|
|
935
|
+
summary: 'The local GitHub issue poll failed before Forge could compare issue state.',
|
|
936
|
+
reason,
|
|
937
|
+
creationReason: 'repeated_infrastructure_failure',
|
|
938
|
+
blockingDecision: 'Repair GitHub CLI authentication/connectivity before unattended issue monitoring resumes.',
|
|
939
|
+
recommendedDecision: 'Restore gh read access, then retrigger the watcher.',
|
|
940
|
+
recommendedPrompt: `Inspect GitHub issue watcher occurrence ${occurrence.occurrenceId} and restore the local read-only gh issue polling path.`,
|
|
941
|
+
statusSummary: 'GitHub issue watcher poll failed.',
|
|
942
|
+
blockedBy: ['github_issue_watch_failed'],
|
|
943
|
+
attemptedActions: [`schedule:${schedule.scheduleId}`, 'gh:api:issues'],
|
|
944
|
+
},
|
|
945
|
+
});
|
|
946
|
+
return failed.occurrence ?? occurrence;
|
|
947
|
+
}
|
|
808
948
|
}
|
|
809
949
|
|
|
810
950
|
if (schedule.action.operation === BROWSER_PROBE_OPERATION) {
|
|
811
951
|
const workId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
|
|
812
|
-
|
|
952
|
+
const keepaliveOnly = args.keepalive_only === true;
|
|
953
|
+
if (!workId && !keepaliveOnly) {
|
|
813
954
|
return decideOccurrence(controllerHome, schedule, occurrence, 'operation_blocked', 'skipped', 'SCHEDULE_BROWSER_PROBE_WORK_ID_REQUIRED');
|
|
814
955
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
956
|
+
if (workId) {
|
|
957
|
+
const workStore = { controllerHome, repoId: schedule.repoId };
|
|
958
|
+
const work = getWorkContract(workStore, workId);
|
|
959
|
+
if (!work) {
|
|
960
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({ enabled: false, pausedReason: `Work ${workId} no longer exists.`, lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId }));
|
|
961
|
+
return decideOccurrence(controllerHome, schedule, occurrence, 'operation_blocked', 'skipped', `SCHEDULE_BROWSER_PROBE_WORK_NOT_FOUND:${workId}`);
|
|
962
|
+
}
|
|
963
|
+
if (isTerminalWorkContractStatus(work.status)) {
|
|
964
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({ enabled: false, pausedReason: `Work ${workId} is terminal (${work.status}).`, lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId }));
|
|
965
|
+
return decideOccurrence(controllerHome, schedule, occurrence, 'nothing_to_do', 'skipped', `Work ${workId} is terminal (${work.status}); browser watcher stopped before probing.`);
|
|
966
|
+
}
|
|
824
967
|
}
|
|
825
968
|
|
|
826
969
|
try {
|
|
@@ -836,33 +979,58 @@ export async function evaluateSchedule(
|
|
|
836
979
|
});
|
|
837
980
|
|
|
838
981
|
if (probe.status === 'auth_required') {
|
|
839
|
-
const observedSchedule =
|
|
840
|
-
...latest,
|
|
982
|
+
const observedSchedule = updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, (current) => ({
|
|
841
983
|
lastTriggeredAt: timestamp,
|
|
842
984
|
lastOccurrenceId: occurrenceId,
|
|
843
985
|
lastObservationAt: timestamp,
|
|
844
986
|
lastObservationStatus: 'auth_required',
|
|
845
987
|
consecutiveFailures: 0,
|
|
846
988
|
nextEligibleAt: undefined,
|
|
847
|
-
pausedReason: undefined,
|
|
848
|
-
});
|
|
989
|
+
...(current.enabled ? { pausedReason: undefined } : {}),
|
|
990
|
+
}));
|
|
849
991
|
if (args.wake_on_auth_required !== false) {
|
|
850
|
-
const
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
? args.
|
|
854
|
-
:
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
992
|
+
const authPrompt = typeof args.auth_required_prompt === 'string'
|
|
993
|
+
? args.auth_required_prompt
|
|
994
|
+
: typeof args.continuation_prompt === 'string'
|
|
995
|
+
? args.continuation_prompt
|
|
996
|
+
: `Scheduled browser watcher ${schedule.scheduleId} detected that authentication is required (${probe.authReason ?? 'login marker'}). Inspect the external dependency and request user login only if it cannot be restored safely.`;
|
|
997
|
+
if (workId) {
|
|
998
|
+
const wakeArgs = { ...args, continuation_prompt: authPrompt };
|
|
999
|
+
return executeExternalControllerWake(controllerHome, observedSchedule, occurrence, timestamp, wakeArgs, { ...observationEvidence, authReason: probe.authReason });
|
|
1000
|
+
}
|
|
1001
|
+
const controllerType = typeof args.controller_type === 'string' ? args.controller_type.trim() : 'chatgpt';
|
|
1002
|
+
if (controllerType !== 'chatgpt') {
|
|
1003
|
+
return decideOccurrence(controllerHome, observedSchedule, occurrence, 'operation_blocked', 'skipped', 'STANDALONE_BROWSER_KEEPALIVE_CHATGPT_REQUIRED', observationEvidence);
|
|
1004
|
+
}
|
|
1005
|
+
const dispatched = await runStandaloneChatgptPrompt({
|
|
1006
|
+
controllerHome,
|
|
1007
|
+
repoId: schedule.repoId,
|
|
1008
|
+
scopeId: `schedule:${schedule.scheduleId}`,
|
|
1009
|
+
prompt: authPrompt,
|
|
1010
|
+
browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : undefined,
|
|
1011
|
+
conversationUrl: typeof args.conversation_url === 'string' ? args.conversation_url : undefined,
|
|
1012
|
+
});
|
|
1013
|
+
if (dispatched.status === 'failed') throw new Error(dispatched.error?.message ?? 'CHATGPT_STANDALONE_PROMPT_FAILED');
|
|
1014
|
+
return decideOccurrence(
|
|
1015
|
+
controllerHome,
|
|
1016
|
+
observedSchedule,
|
|
1017
|
+
occurrence,
|
|
1018
|
+
'execute',
|
|
1019
|
+
'succeeded',
|
|
1020
|
+
'Standalone browser keepalive auth-required prompt dispatched to ChatGPT.',
|
|
1021
|
+
{
|
|
1022
|
+
...observationEvidence,
|
|
1023
|
+
authReason: probe.authReason,
|
|
1024
|
+
browserSessionId: dispatched.browserSessionId,
|
|
1025
|
+
conversationUrl: dispatched.conversationUrl,
|
|
1026
|
+
},
|
|
1027
|
+
);
|
|
859
1028
|
}
|
|
860
|
-
return decideOccurrence(controllerHome, observedSchedule, occurrence, 'operation_blocked', 'skipped', `Browser watcher requires authentication: ${probe.authReason ?? 'login marker matched'}`, observationEvidence);
|
|
1029
|
+
return decideOccurrence(controllerHome, observedSchedule, occurrence, 'operation_blocked', 'skipped', `Browser ${workId ? 'watcher' : 'keepalive'} requires authentication: ${probe.authReason ?? 'login marker matched'}`, observationEvidence);
|
|
861
1030
|
}
|
|
862
1031
|
|
|
863
1032
|
if (probe.status === 'keepalive') {
|
|
864
|
-
const observedSchedule =
|
|
865
|
-
...latest,
|
|
1033
|
+
const observedSchedule = updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, (current) => ({
|
|
866
1034
|
lastTriggeredAt: timestamp,
|
|
867
1035
|
lastOccurrenceId: occurrenceId,
|
|
868
1036
|
lastObservationAt: timestamp,
|
|
@@ -870,8 +1038,8 @@ export async function evaluateSchedule(
|
|
|
870
1038
|
consecutiveFailures: 0,
|
|
871
1039
|
consecutiveNoops: 0,
|
|
872
1040
|
nextEligibleAt: undefined,
|
|
873
|
-
pausedReason: undefined,
|
|
874
|
-
});
|
|
1041
|
+
...(current.enabled ? { pausedReason: undefined } : {}),
|
|
1042
|
+
}));
|
|
875
1043
|
return decideOccurrence(controllerHome, observedSchedule, occurrence, 'execute', 'succeeded', 'Browser session keepalive refreshed successfully.', observationEvidence);
|
|
876
1044
|
}
|
|
877
1045
|
|
|
@@ -883,19 +1051,18 @@ export async function evaluateSchedule(
|
|
|
883
1051
|
const changed = observation.status === 'changed';
|
|
884
1052
|
const shouldWake = args.wake_on_change !== false && observation.shouldWake;
|
|
885
1053
|
const observationStatus: RepositorySchedule['lastObservationStatus'] = observation.status;
|
|
886
|
-
const observedSchedule =
|
|
887
|
-
...latest,
|
|
1054
|
+
const observedSchedule = updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, (current) => ({
|
|
888
1055
|
lastTriggeredAt: timestamp,
|
|
889
1056
|
lastOccurrenceId: occurrenceId,
|
|
890
1057
|
lastObservationAt: timestamp,
|
|
891
1058
|
lastObservationFingerprint: probe.fingerprint,
|
|
892
|
-
lastObservationChangedAt: changed ? timestamp :
|
|
1059
|
+
lastObservationChangedAt: changed ? timestamp : current.lastObservationChangedAt,
|
|
893
1060
|
lastObservationStatus: observationStatus,
|
|
894
1061
|
consecutiveFailures: 0,
|
|
895
|
-
consecutiveNoops: shouldWake ? 0 : (
|
|
1062
|
+
consecutiveNoops: shouldWake ? 0 : (current.consecutiveNoops ?? 0) + 1,
|
|
896
1063
|
nextEligibleAt: undefined,
|
|
897
|
-
pausedReason: undefined,
|
|
898
|
-
});
|
|
1064
|
+
...(current.enabled ? { pausedReason: undefined } : {}),
|
|
1065
|
+
}));
|
|
899
1066
|
|
|
900
1067
|
if (!shouldWake) {
|
|
901
1068
|
const reason = observation.status === 'baseline'
|
|
@@ -930,8 +1097,7 @@ export async function evaluateSchedule(
|
|
|
930
1097
|
attemptedActions: [`schedule:${schedule.scheduleId}`, `work:${workId}`, 'operation:browser_probe'],
|
|
931
1098
|
},
|
|
932
1099
|
});
|
|
933
|
-
|
|
934
|
-
saveSchedule(controllerHome, { ...latest, lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId });
|
|
1100
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({ lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId }));
|
|
935
1101
|
return failed.occurrence ?? saveOccurrence(controllerHome, { ...occurrence, status: 'failed', decision: 'execute', reason });
|
|
936
1102
|
}
|
|
937
1103
|
}
|
|
@@ -976,15 +1142,13 @@ export async function evaluateSchedule(
|
|
|
976
1142
|
cancelPendingApprovals: false,
|
|
977
1143
|
});
|
|
978
1144
|
const appliedCount = result.applied.filter((candidate) => candidate.applied).length;
|
|
979
|
-
|
|
980
|
-
saveSchedule(controllerHome, {
|
|
981
|
-
...latest,
|
|
1145
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, (current) => ({
|
|
982
1146
|
lastTriggeredAt: timestamp,
|
|
983
1147
|
lastOccurrenceId: occurrenceId,
|
|
984
1148
|
consecutiveFailures: 0,
|
|
985
1149
|
nextEligibleAt: undefined,
|
|
986
|
-
pausedReason: undefined,
|
|
987
|
-
});
|
|
1150
|
+
...(current.enabled ? { pausedReason: undefined } : {}),
|
|
1151
|
+
}));
|
|
988
1152
|
return saveOccurrence(controllerHome, {
|
|
989
1153
|
...decision,
|
|
990
1154
|
status: 'succeeded',
|
|
@@ -1013,12 +1177,10 @@ export async function evaluateSchedule(
|
|
|
1013
1177
|
],
|
|
1014
1178
|
},
|
|
1015
1179
|
});
|
|
1016
|
-
|
|
1017
|
-
saveSchedule(controllerHome, {
|
|
1018
|
-
...latest,
|
|
1180
|
+
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, () => ({
|
|
1019
1181
|
lastTriggeredAt: timestamp,
|
|
1020
1182
|
lastOccurrenceId: occurrenceId,
|
|
1021
|
-
});
|
|
1183
|
+
}));
|
|
1022
1184
|
return failed.occurrence ?? saveOccurrence(controllerHome, {
|
|
1023
1185
|
...decision,
|
|
1024
1186
|
status: 'failed',
|