@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
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { resolveTrustedExecutable } from '../../shared/trusted-executable';
|
|
4
|
+
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
|
|
7
|
+
export interface GithubIssueWatchIssue {
|
|
8
|
+
number: number;
|
|
9
|
+
title: string;
|
|
10
|
+
state: 'open' | 'closed';
|
|
11
|
+
updatedAt: string;
|
|
12
|
+
url: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface GithubIssueWatchState {
|
|
16
|
+
schemaVersion: 1;
|
|
17
|
+
observedAt: string;
|
|
18
|
+
signatures: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface GithubIssueWatchObservation {
|
|
22
|
+
status: 'baseline' | 'unchanged' | 'changed';
|
|
23
|
+
shouldWake: boolean;
|
|
24
|
+
changedOpenIssues: GithubIssueWatchIssue[];
|
|
25
|
+
issues: GithubIssueWatchIssue[];
|
|
26
|
+
nextState: GithubIssueWatchState;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function stringValue(value: unknown): string | undefined {
|
|
30
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function boundedNumber(value: unknown, fallback: number, min: number, max: number): number {
|
|
34
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
35
|
+
? Math.max(min, Math.min(Math.trunc(value), max))
|
|
36
|
+
: fallback;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function issueSignature(issue: GithubIssueWatchIssue): string {
|
|
40
|
+
return `${issue.state}:${issue.updatedAt}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeIssue(value: unknown): GithubIssueWatchIssue | undefined {
|
|
44
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
45
|
+
const record = value as Record<string, unknown>;
|
|
46
|
+
const number = typeof record.number === 'number' && Number.isFinite(record.number) ? Math.trunc(record.number) : undefined;
|
|
47
|
+
const title = stringValue(record.title);
|
|
48
|
+
const state = record.state === 'open' || record.state === 'closed' ? record.state : undefined;
|
|
49
|
+
const updatedAt = stringValue(record.updatedAt);
|
|
50
|
+
const url = stringValue(record.url);
|
|
51
|
+
if (!number || !title || !state || !updatedAt || !url) return undefined;
|
|
52
|
+
return { number, title, state, updatedAt, url };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function normalizeGithubIssueWatchState(value: unknown): GithubIssueWatchState | undefined {
|
|
56
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
57
|
+
const record = value as Record<string, unknown>;
|
|
58
|
+
const signaturesValue = record.signatures;
|
|
59
|
+
if (!signaturesValue || typeof signaturesValue !== 'object' || Array.isArray(signaturesValue)) return undefined;
|
|
60
|
+
const signatures = Object.fromEntries(Object.entries(signaturesValue as Record<string, unknown>)
|
|
61
|
+
.filter((entry): entry is [string, string] => typeof entry[1] === 'string'));
|
|
62
|
+
return {
|
|
63
|
+
schemaVersion: 1,
|
|
64
|
+
observedAt: stringValue(record.observedAt) ?? new Date(0).toISOString(),
|
|
65
|
+
signatures,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function classifyGithubIssueWatchObservation(
|
|
70
|
+
issues: GithubIssueWatchIssue[],
|
|
71
|
+
previousState: GithubIssueWatchState | undefined,
|
|
72
|
+
options: { since?: string; wakeOnFirstObservation?: boolean; observedAt?: string } = {},
|
|
73
|
+
): GithubIssueWatchObservation {
|
|
74
|
+
const observedAt = options.observedAt ?? new Date().toISOString();
|
|
75
|
+
const signatures = Object.fromEntries(issues.map((issue) => [String(issue.number), issueSignature(issue)]));
|
|
76
|
+
const firstObservation = !previousState;
|
|
77
|
+
const sinceMs = options.since ? Date.parse(options.since) : Number.NaN;
|
|
78
|
+
const changedOpenIssues = issues.filter((issue) => {
|
|
79
|
+
if (issue.state !== 'open') return false;
|
|
80
|
+
if (!firstObservation) return previousState.signatures[String(issue.number)] !== issueSignature(issue);
|
|
81
|
+
if (options.wakeOnFirstObservation === true) return true;
|
|
82
|
+
return Number.isFinite(sinceMs) && Date.parse(issue.updatedAt) > sinceMs;
|
|
83
|
+
});
|
|
84
|
+
return {
|
|
85
|
+
status: firstObservation ? 'baseline' : changedOpenIssues.length > 0 ? 'changed' : 'unchanged',
|
|
86
|
+
shouldWake: changedOpenIssues.length > 0,
|
|
87
|
+
changedOpenIssues,
|
|
88
|
+
issues,
|
|
89
|
+
nextState: { schemaVersion: 1, observedAt, signatures },
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const DEFAULT_GH_EXECUTABLE_CANDIDATES = [
|
|
94
|
+
'/opt/homebrew/bin/gh',
|
|
95
|
+
'/usr/local/bin/gh',
|
|
96
|
+
'/usr/bin/gh',
|
|
97
|
+
] as const;
|
|
98
|
+
|
|
99
|
+
export function resolveGithubIssueWatchExecutable(
|
|
100
|
+
args: Record<string, unknown>,
|
|
101
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
102
|
+
preferredPaths: readonly string[] = DEFAULT_GH_EXECUTABLE_CANDIDATES,
|
|
103
|
+
): string {
|
|
104
|
+
const configured = stringValue(args.gh_executable) ?? stringValue(env.FORGE_GH_EXECUTABLE);
|
|
105
|
+
const resolved = resolveTrustedExecutable({
|
|
106
|
+
name: 'gh',
|
|
107
|
+
configured,
|
|
108
|
+
preferredPaths,
|
|
109
|
+
env,
|
|
110
|
+
});
|
|
111
|
+
if (resolved.configuredInvalid) {
|
|
112
|
+
throw new Error('SCHEDULE_GITHUB_ISSUE_WATCH_GH_EXECUTABLE_INVALID: gh_executable/FORGE_GH_EXECUTABLE must be an absolute executable path.');
|
|
113
|
+
}
|
|
114
|
+
if (!resolved.executable) {
|
|
115
|
+
throw new Error('SCHEDULE_GITHUB_ISSUE_WATCH_GH_EXECUTABLE_NOT_FOUND: GitHub CLI was not found in trusted Homebrew/system locations or PATH.');
|
|
116
|
+
}
|
|
117
|
+
return resolved.executable;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function executeScheduledGithubIssueWatch(input: {
|
|
121
|
+
repoRoot: string;
|
|
122
|
+
args: Record<string, unknown>;
|
|
123
|
+
observedAt?: string;
|
|
124
|
+
}): Promise<GithubIssueWatchObservation> {
|
|
125
|
+
const repository = stringValue(input.args.github_repository);
|
|
126
|
+
if (!repository || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) {
|
|
127
|
+
throw new Error('SCHEDULE_GITHUB_ISSUE_WATCH_REPOSITORY_REQUIRED');
|
|
128
|
+
}
|
|
129
|
+
const limit = boundedNumber(input.args.issue_limit, 50, 10, 100);
|
|
130
|
+
const timeoutMs = boundedNumber(input.args.timeout_ms, 15_000, 1_000, 60_000);
|
|
131
|
+
try {
|
|
132
|
+
const ghExecutable = resolveGithubIssueWatchExecutable(input.args);
|
|
133
|
+
const result = await execFileAsync(ghExecutable, [
|
|
134
|
+
'api',
|
|
135
|
+
'-H', 'Accept: application/vnd.github+json',
|
|
136
|
+
`repos/${repository}/issues?state=all&sort=updated&direction=desc&per_page=${limit}`,
|
|
137
|
+
'--jq', 'map(select(.pull_request|not)) | map({number,title,state,updatedAt:.updated_at,url:.html_url})',
|
|
138
|
+
], {
|
|
139
|
+
cwd: input.repoRoot,
|
|
140
|
+
encoding: 'utf8',
|
|
141
|
+
timeout: timeoutMs,
|
|
142
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
143
|
+
});
|
|
144
|
+
const parsed = JSON.parse(String(result.stdout)) as unknown;
|
|
145
|
+
if (!Array.isArray(parsed)) throw new Error('GitHub issue response was not an array.');
|
|
146
|
+
const issues = parsed.map(normalizeIssue).filter((issue): issue is GithubIssueWatchIssue => Boolean(issue));
|
|
147
|
+
const previousState = normalizeGithubIssueWatchState(input.args.issue_watch_state);
|
|
148
|
+
return classifyGithubIssueWatchObservation(issues, previousState, {
|
|
149
|
+
since: stringValue(input.args.issue_watch_since),
|
|
150
|
+
wakeOnFirstObservation: input.args.wake_on_first_observation === true,
|
|
151
|
+
observedAt: input.observedAt,
|
|
152
|
+
});
|
|
153
|
+
} catch (error) {
|
|
154
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
155
|
+
if (message.startsWith('SCHEDULE_GITHUB_ISSUE_WATCH_GH_EXECUTABLE_')) throw error;
|
|
156
|
+
throw new Error(`SCHEDULE_GITHUB_ISSUE_WATCH_FAILED: ${message}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -5,22 +5,29 @@ import {
|
|
|
5
5
|
getSchedule,
|
|
6
6
|
recordScheduleOccurrenceHandoff,
|
|
7
7
|
saveOccurrence,
|
|
8
|
-
|
|
8
|
+
updateSchedule,
|
|
9
9
|
type ScheduleOccurrenceHandoffInput,
|
|
10
10
|
} from './store';
|
|
11
11
|
|
|
12
12
|
const TERMINAL_OCCURRENCE_STATUSES = new Set<ScheduleOccurrence['status']>(['succeeded', 'failed', 'shadowed', 'skipped']);
|
|
13
13
|
|
|
14
|
-
function
|
|
15
|
-
const shouldPause = nextFailures >= schedule.policy.maxFailures;
|
|
14
|
+
function computeScheduleBackoff(schedule: ReturnType<typeof getSchedule>, nextFailures: number): string {
|
|
16
15
|
const backoffBase = Math.max(1, schedule.policy.backoffBaseMinutes ?? schedule.policy.cooldownMinutes ?? 1);
|
|
17
16
|
const backoffMax = Math.max(backoffBase, schedule.policy.backoffMaxMinutes ?? 24 * 60);
|
|
18
|
-
|
|
17
|
+
// Persistent readiness failures may span hours or days. Cap the exponent so
|
|
18
|
+
// the calculation stays bounded even while the observable failure count grows.
|
|
19
|
+
const exponent = Math.min(16, Math.max(0, nextFailures - 1));
|
|
20
|
+
const backoffMinutes = Math.min(backoffMax, backoffBase * (2 ** exponent));
|
|
21
|
+
return new Date(Date.now() + backoffMinutes * 60_000).toISOString();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function computeScheduleFailureState(schedule: ReturnType<typeof getSchedule>, nextFailures: number): Pick<ReturnType<typeof getSchedule>, 'consecutiveFailures' | 'nextEligibleAt' | 'enabled' | 'pausedReason'> {
|
|
25
|
+
const shouldPause = nextFailures >= schedule.policy.maxFailures;
|
|
19
26
|
return {
|
|
20
27
|
consecutiveFailures: nextFailures,
|
|
21
|
-
nextEligibleAt:
|
|
28
|
+
nextEligibleAt: computeScheduleBackoff(schedule, nextFailures),
|
|
22
29
|
enabled: shouldPause ? false : schedule.enabled,
|
|
23
|
-
pausedReason: shouldPause ? 'Maximum consecutive failures reached.' : undefined,
|
|
30
|
+
pausedReason: shouldPause ? 'Maximum consecutive failures reached.' : schedule.enabled ? undefined : schedule.pausedReason,
|
|
24
31
|
};
|
|
25
32
|
}
|
|
26
33
|
|
|
@@ -55,16 +62,58 @@ export function applyScheduleFailure(
|
|
|
55
62
|
|
|
56
63
|
const countFailure = options.countFailure !== false;
|
|
57
64
|
const nextSchedule = options.pauseReason
|
|
58
|
-
?
|
|
59
|
-
...schedule,
|
|
65
|
+
? updateSchedule(controllerHome, repoId, scheduleId, (current) => ({
|
|
60
66
|
enabled: false,
|
|
61
67
|
pausedReason: options.pauseReason,
|
|
62
68
|
nextEligibleAt: undefined,
|
|
63
|
-
consecutiveFailures: countFailure ?
|
|
64
|
-
})
|
|
69
|
+
consecutiveFailures: countFailure ? current.consecutiveFailures + 1 : current.consecutiveFailures,
|
|
70
|
+
}))
|
|
65
71
|
: countFailure
|
|
66
|
-
?
|
|
67
|
-
:
|
|
72
|
+
? updateSchedule(controllerHome, repoId, scheduleId, (current) => computeScheduleFailureState(current, current.consecutiveFailures + 1))
|
|
73
|
+
: getSchedule(controllerHome, repoId, scheduleId);
|
|
74
|
+
return { schedule: nextSchedule, occurrence: nextOccurrence };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function applyScheduleRetryableFailure(
|
|
78
|
+
controllerHome: string,
|
|
79
|
+
scheduleId: string,
|
|
80
|
+
repoId: string,
|
|
81
|
+
occurrenceId: string,
|
|
82
|
+
options: {
|
|
83
|
+
outcome: 'failed' | 'skipped';
|
|
84
|
+
decision: ScheduleDecisionType;
|
|
85
|
+
reason: string;
|
|
86
|
+
countFailure?: boolean;
|
|
87
|
+
},
|
|
88
|
+
): { schedule: ReturnType<typeof getSchedule>; occurrence?: ScheduleOccurrence } {
|
|
89
|
+
const occurrence = getOccurrence(controllerHome, repoId, occurrenceId);
|
|
90
|
+
let nextOccurrence = occurrence;
|
|
91
|
+
if (occurrence && !TERMINAL_OCCURRENCE_STATUSES.has(occurrence.status)) {
|
|
92
|
+
nextOccurrence = saveOccurrence(controllerHome, {
|
|
93
|
+
...occurrence,
|
|
94
|
+
status: options.outcome,
|
|
95
|
+
decision: options.decision,
|
|
96
|
+
reason: options.reason,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const countFailure = options.countFailure !== false;
|
|
100
|
+
const nextSchedule = updateSchedule(controllerHome, repoId, scheduleId, (current) => {
|
|
101
|
+
const nextFailures = countFailure ? current.consecutiveFailures + 1 : current.consecutiveFailures;
|
|
102
|
+
const failureState = countFailure && current.enabled
|
|
103
|
+
? computeScheduleFailureState(current, nextFailures)
|
|
104
|
+
: {
|
|
105
|
+
consecutiveFailures: nextFailures,
|
|
106
|
+
nextEligibleAt: countFailure
|
|
107
|
+
? current.nextEligibleAt
|
|
108
|
+
: computeScheduleBackoff(current, Math.max(1, nextFailures)),
|
|
109
|
+
enabled: current.enabled,
|
|
110
|
+
pausedReason: current.enabled ? undefined : current.pausedReason,
|
|
111
|
+
};
|
|
112
|
+
// One transient fault backs off and re-arms. Repeated faults have crossed
|
|
113
|
+
// the already-declared failure budget and must stop before they consume
|
|
114
|
+
// further controller sessions or browser interactions.
|
|
115
|
+
return failureState;
|
|
116
|
+
});
|
|
68
117
|
return { schedule: nextSchedule, occurrence: nextOccurrence };
|
|
69
118
|
}
|
|
70
119
|
|
|
@@ -98,7 +147,6 @@ export function settleScheduledExecution(
|
|
|
98
147
|
const occurrenceId = typeof job.payload.occurrenceId === 'string' ? job.payload.occurrenceId : undefined;
|
|
99
148
|
if (!scheduleId || !occurrenceId) return;
|
|
100
149
|
try {
|
|
101
|
-
const schedule = getSchedule(controllerHome, job.repoId, scheduleId);
|
|
102
150
|
const occurrence = getOccurrence(controllerHome, job.repoId, occurrenceId);
|
|
103
151
|
if (occurrence && !['succeeded', 'failed', 'shadowed', 'skipped'].includes(occurrence.status)) {
|
|
104
152
|
saveOccurrence(controllerHome, {
|
|
@@ -131,12 +179,11 @@ export function settleScheduledExecution(
|
|
|
131
179
|
});
|
|
132
180
|
return;
|
|
133
181
|
}
|
|
134
|
-
|
|
135
|
-
...schedule,
|
|
182
|
+
updateSchedule(controllerHome, job.repoId, scheduleId, (current) => ({
|
|
136
183
|
consecutiveFailures: 0,
|
|
137
184
|
nextEligibleAt: undefined,
|
|
138
|
-
pausedReason: undefined,
|
|
139
|
-
});
|
|
185
|
+
...(current.enabled ? { pausedReason: undefined } : {}),
|
|
186
|
+
}));
|
|
140
187
|
} catch {
|
|
141
188
|
// Job terminal state remains authoritative even if an old schedule record
|
|
142
189
|
// has already been removed. Reconciliation must not be blocked by it.
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'crypto';
|
|
2
|
-
import { existsSync, readdirSync } from 'fs';
|
|
2
|
+
import { existsSync, readdirSync, rmSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { repositoryControllerRoot } from '../../../cli/repositories/controller-home';
|
|
5
5
|
import { withControllerLock } from '../../../cli/repositories/locks';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
createHandoffItem,
|
|
8
|
+
getHandoffItem,
|
|
9
|
+
listHandoffItems,
|
|
10
|
+
resolveHandoffItem,
|
|
11
|
+
} from '../../control-plane/facade/handoff-inbox-store';
|
|
7
12
|
import { readJsonFile, writeJsonAtomic } from '../../shared/json-files';
|
|
8
13
|
import { appendRuntimeEvent } from '../../evidence/event-ledger';
|
|
9
14
|
import type { RepositorySchedule, ScheduleDecision, ScheduleOccurrence } from './types';
|
|
@@ -23,6 +28,7 @@ interface ScheduleRequestRecord {
|
|
|
23
28
|
createdAt: string;
|
|
24
29
|
}
|
|
25
30
|
type CreateScheduleInput = Omit<RepositorySchedule, 'schemaVersion' | 'revision' | 'scheduleId' | 'createdAt' | 'updatedAt' | 'consecutiveFailures'>;
|
|
31
|
+
type ScheduleMutableUpdate = Partial<Omit<RepositorySchedule, 'schemaVersion' | 'revision' | 'scheduleId' | 'repoId' | 'requestId' | 'createdAt' | 'updatedAt'>>;
|
|
26
32
|
|
|
27
33
|
export interface ScheduleOccurrenceHandoffInput {
|
|
28
34
|
title: string;
|
|
@@ -57,6 +63,30 @@ function occurrenceIndexPath(controllerHome: string, repoId: string): string {
|
|
|
57
63
|
function requestPath(controllerHome: string, repoId: string, requestId: string): string {
|
|
58
64
|
return join(schedulesRoot(controllerHome, repoId), 'indexes', 'requests', `${createHash('sha256').update(requestId).digest('hex')}.json`);
|
|
59
65
|
}
|
|
66
|
+
|
|
67
|
+
const EXTERNAL_CONTROLLER_WAKE_MAX_FAILURES = 3;
|
|
68
|
+
const EXTERNAL_CONTROLLER_WAKE_DAILY_BUDGET_MINUTES = 60;
|
|
69
|
+
const EXTERNAL_CONTROLLER_WAKE_MIN_COOLDOWN_MINUTES = 10;
|
|
70
|
+
|
|
71
|
+
function boundedSchedulePolicy(schedule: RepositorySchedule): RepositorySchedule['policy'] {
|
|
72
|
+
const policy = {
|
|
73
|
+
...schedule.policy,
|
|
74
|
+
backoffBaseMinutes: Math.max(1, schedule.policy.backoffBaseMinutes ?? schedule.policy.cooldownMinutes ?? 1),
|
|
75
|
+
backoffMaxMinutes: Math.max(1, schedule.policy.backoffMaxMinutes ?? 24 * 60),
|
|
76
|
+
};
|
|
77
|
+
if (schedule.action.operation !== 'external_controller_wake') return policy;
|
|
78
|
+
// A Work-bound controller wake is expensive and can affect the user's
|
|
79
|
+
// browser. No schedule payload may convert a transient tool outage into an
|
|
80
|
+
// all-day retry loop. Larger work needs an explicit future user action after
|
|
81
|
+
// the bounded lane records useful failure evidence.
|
|
82
|
+
return {
|
|
83
|
+
...policy,
|
|
84
|
+
maxFailures: Math.max(1, Math.min(policy.maxFailures, EXTERNAL_CONTROLLER_WAKE_MAX_FAILURES)),
|
|
85
|
+
dailyBudgetMinutes: Math.max(1, Math.min(policy.dailyBudgetMinutes, EXTERNAL_CONTROLLER_WAKE_DAILY_BUDGET_MINUTES)),
|
|
86
|
+
cooldownMinutes: Math.max(policy.cooldownMinutes, EXTERNAL_CONTROLLER_WAKE_MIN_COOLDOWN_MINUTES),
|
|
87
|
+
backoffBaseMinutes: Math.max(policy.backoffBaseMinutes, EXTERNAL_CONTROLLER_WAKE_MIN_COOLDOWN_MINUTES),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
60
90
|
function canonical(value: unknown): unknown {
|
|
61
91
|
if (Array.isArray(value)) return value.map(canonical);
|
|
62
92
|
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value as Record<string, unknown>)
|
|
@@ -131,22 +161,62 @@ export function getSchedule(controllerHome: string, repoId: string, scheduleId:
|
|
|
131
161
|
...schedule,
|
|
132
162
|
revision: Number.isFinite(schedule.revision) ? schedule.revision : 1,
|
|
133
163
|
requestId: schedule.requestId || `legacy-schedule:${schedule.scheduleId}`,
|
|
134
|
-
policy:
|
|
135
|
-
...schedule.policy,
|
|
136
|
-
backoffBaseMinutes: Math.max(1, schedule.policy.backoffBaseMinutes ?? schedule.policy.cooldownMinutes ?? 1),
|
|
137
|
-
backoffMaxMinutes: Math.max(1, schedule.policy.backoffMaxMinutes ?? 24 * 60),
|
|
138
|
-
},
|
|
164
|
+
policy: boundedSchedulePolicy(schedule),
|
|
139
165
|
consecutiveNoops: Math.max(0, schedule.consecutiveNoops ?? 0),
|
|
140
166
|
};
|
|
141
167
|
}
|
|
142
168
|
|
|
169
|
+
function persistScheduleRevision(
|
|
170
|
+
controllerHome: string,
|
|
171
|
+
current: RepositorySchedule,
|
|
172
|
+
candidate: RepositorySchedule,
|
|
173
|
+
): RepositorySchedule {
|
|
174
|
+
const next: RepositorySchedule = {
|
|
175
|
+
...candidate,
|
|
176
|
+
schemaVersion: current.schemaVersion,
|
|
177
|
+
revision: current.revision + 1,
|
|
178
|
+
scheduleId: current.scheduleId,
|
|
179
|
+
repoId: current.repoId,
|
|
180
|
+
requestId: current.requestId,
|
|
181
|
+
createdAt: current.createdAt,
|
|
182
|
+
updatedAt: new Date().toISOString(),
|
|
183
|
+
};
|
|
184
|
+
writeJsonAtomic(schedulePath(controllerHome, next.repoId, next.scheduleId), next);
|
|
185
|
+
appendRuntimeEvent(controllerHome, { repoId: next.repoId, entityType: 'schedule', entityId: next.scheduleId, eventType: 'schedule_updated', requestId: next.requestId, revision: next.revision, data: { enabled: next.enabled, pausedReason: next.pausedReason } });
|
|
186
|
+
return next;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Persist a caller-owned Schedule snapshot only when its revision is still the
|
|
191
|
+
* current authority. User/configuration mutations use this path so stale
|
|
192
|
+
* writers fail closed instead of overwriting a newer decision.
|
|
193
|
+
*/
|
|
143
194
|
export function saveSchedule(controllerHome: string, schedule: RepositorySchedule): RepositorySchedule {
|
|
144
195
|
return withControllerLock(controllerHome, { scope: 'task', repoId: schedule.repoId, taskId: `schedule-${schedule.scheduleId}` }, `save-schedule:${schedule.scheduleId}`, () => {
|
|
145
196
|
const current = getSchedule(controllerHome, schedule.repoId, schedule.scheduleId);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return
|
|
197
|
+
if (schedule.revision !== current.revision) {
|
|
198
|
+
throw new Error(`SCHEDULE_REVISION_CONFLICT: ${schedule.scheduleId}:expected=${schedule.revision}:actual=${current.revision}`);
|
|
199
|
+
}
|
|
200
|
+
return persistScheduleRevision(controllerHome, current, schedule);
|
|
201
|
+
}, 10_000);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Apply runtime-owned settlement/observation fields to the latest Schedule
|
|
206
|
+
* authority while holding the Schedule lock. Identity and revision fields are
|
|
207
|
+
* never writable through this path. Long-running occurrences use this instead
|
|
208
|
+
* of replaying a stale whole-record snapshot after user state may have changed.
|
|
209
|
+
*/
|
|
210
|
+
export function updateSchedule(
|
|
211
|
+
controllerHome: string,
|
|
212
|
+
repoId: string,
|
|
213
|
+
scheduleId: string,
|
|
214
|
+
update: (current: RepositorySchedule) => ScheduleMutableUpdate,
|
|
215
|
+
): RepositorySchedule {
|
|
216
|
+
return withControllerLock(controllerHome, { scope: 'task', repoId, taskId: `schedule-${scheduleId}` }, `update-schedule:${scheduleId}`, () => {
|
|
217
|
+
const current = getSchedule(controllerHome, repoId, scheduleId);
|
|
218
|
+
const changes = update(current);
|
|
219
|
+
return persistScheduleRevision(controllerHome, current, { ...current, ...changes });
|
|
150
220
|
}, 10_000);
|
|
151
221
|
}
|
|
152
222
|
|
|
@@ -159,17 +229,124 @@ export function listSchedules(controllerHome: string, repoId: string): Repositor
|
|
|
159
229
|
} catch { return []; }
|
|
160
230
|
}
|
|
161
231
|
|
|
232
|
+
export function deleteSchedule(controllerHome: string, repoId: string, scheduleId: string): RepositorySchedule {
|
|
233
|
+
return withControllerLock(controllerHome, { scope: 'task', repoId, taskId: `schedule-${scheduleId}` }, `delete-schedule:${scheduleId}`, () => {
|
|
234
|
+
const current = getSchedule(controllerHome, repoId, scheduleId);
|
|
235
|
+
if (current.enabled) throw new Error(`SCHEDULE_DELETE_REQUIRES_PAUSED: ${scheduleId}`);
|
|
236
|
+
if (listActiveOccurrences(controllerHome, repoId, scheduleId).length > 0) {
|
|
237
|
+
throw new Error(`SCHEDULE_DELETE_ACTIVE_OCCURRENCE: ${scheduleId}`);
|
|
238
|
+
}
|
|
239
|
+
rmSync(schedulePath(controllerHome, repoId, scheduleId), { force: true });
|
|
240
|
+
const requestRecordPath = requestPath(controllerHome, repoId, current.requestId);
|
|
241
|
+
if (existsSync(requestRecordPath)) {
|
|
242
|
+
const record = readJsonFile<ScheduleRequestRecord>(requestRecordPath);
|
|
243
|
+
if (record.scheduleId === scheduleId) rmSync(requestRecordPath, { force: true });
|
|
244
|
+
}
|
|
245
|
+
appendRuntimeEvent(controllerHome, {
|
|
246
|
+
repoId,
|
|
247
|
+
entityType: 'schedule',
|
|
248
|
+
entityId: scheduleId,
|
|
249
|
+
eventType: 'schedule_deleted',
|
|
250
|
+
requestId: current.requestId,
|
|
251
|
+
revision: current.revision + 1,
|
|
252
|
+
data: { name: current.name },
|
|
253
|
+
});
|
|
254
|
+
return current;
|
|
255
|
+
}, 10_000);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function scheduleFailureClass(reason: string): string {
|
|
259
|
+
const trimmed = reason.trim();
|
|
260
|
+
const explicitCode = trimmed.match(/^([A-Z][A-Z0-9_]{2,})(?::|\b)/)?.[1];
|
|
261
|
+
if (explicitCode) return explicitCode;
|
|
262
|
+
const prefix = (trimmed.split(':', 1)[0] ?? trimmed)
|
|
263
|
+
.replace(/\s+/g, ' ')
|
|
264
|
+
.trim()
|
|
265
|
+
.toLowerCase()
|
|
266
|
+
.slice(0, 120);
|
|
267
|
+
return prefix || 'unknown_failure';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function activeScheduleFailureHandoff(
|
|
271
|
+
controllerHome: string,
|
|
272
|
+
repoId: string,
|
|
273
|
+
scheduleId: string,
|
|
274
|
+
creationReason: ScheduleOccurrenceHandoffInput['creationReason'],
|
|
275
|
+
reason: string,
|
|
276
|
+
) {
|
|
277
|
+
const failureClass = scheduleFailureClass(reason);
|
|
278
|
+
return listHandoffItems({ controllerHome, repoId, status: 'active', limit: 100 }).find((item) => (
|
|
279
|
+
item.currentState?.taskId === scheduleId
|
|
280
|
+
&& item.creationReason === creationReason
|
|
281
|
+
&& scheduleFailureClass(item.reason) === failureClass
|
|
282
|
+
));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function stableScheduleFailureHandoffId(
|
|
286
|
+
controllerHome: string,
|
|
287
|
+
repoId: string,
|
|
288
|
+
scheduleId: string,
|
|
289
|
+
creationReason: ScheduleOccurrenceHandoffInput['creationReason'],
|
|
290
|
+
reason: string,
|
|
291
|
+
occurrenceId: string,
|
|
292
|
+
): string {
|
|
293
|
+
const digest = createHash('sha256')
|
|
294
|
+
.update(`${scheduleId}:${creationReason}:${scheduleFailureClass(reason)}`)
|
|
295
|
+
.digest('hex')
|
|
296
|
+
.slice(0, 20);
|
|
297
|
+
const baseId = `schedule-failure-${digest}`;
|
|
298
|
+
if (!getHandoffItem({ controllerHome, repoId }, baseId)) return baseId;
|
|
299
|
+
// A previous failure streak may already have resolved the stable item. Handoff
|
|
300
|
+
// records are immutable apart from lifecycle status, so use one new epoch id;
|
|
301
|
+
// subsequent failures in this streak will discover and reuse the active item.
|
|
302
|
+
const epoch = createHash('sha256').update(occurrenceId).digest('hex').slice(0, 8);
|
|
303
|
+
return `${baseId}-${epoch}`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function resolveRecoveredScheduleFailureHandoffs(controllerHome: string, occurrence: ScheduleOccurrence): void {
|
|
307
|
+
try {
|
|
308
|
+
const active = listHandoffItems({ controllerHome, repoId: occurrence.repoId, status: 'active', limit: 100 })
|
|
309
|
+
.filter((item) => (
|
|
310
|
+
item.currentState?.taskId === occurrence.scheduleId
|
|
311
|
+
&& item.creationReason === 'repeated_infrastructure_failure'
|
|
312
|
+
));
|
|
313
|
+
for (const item of active) {
|
|
314
|
+
try {
|
|
315
|
+
resolveHandoffItem(
|
|
316
|
+
{ controllerHome, repoId: occurrence.repoId },
|
|
317
|
+
item.id,
|
|
318
|
+
{
|
|
319
|
+
decision: `Schedule ${occurrence.scheduleId} recovered on occurrence ${occurrence.occurrenceId}.`,
|
|
320
|
+
resolver: 'schedule-runtime',
|
|
321
|
+
},
|
|
322
|
+
);
|
|
323
|
+
} catch {
|
|
324
|
+
// The succeeded occurrence remains authoritative. Inbox cleanup is
|
|
325
|
+
// best-effort and must never turn schedule recovery into a failure.
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
} catch {
|
|
329
|
+
// Preserve successful Schedule settlement even if Inbox storage is unavailable.
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
162
333
|
export function saveOccurrence(controllerHome: string, occurrence: ScheduleOccurrence): ScheduleOccurrence {
|
|
163
|
-
|
|
334
|
+
const next = withControllerLock(controllerHome, { scope: 'task', repoId: occurrence.repoId, taskId: `schedule-${occurrence.scheduleId}` }, `save-occurrence:${occurrence.occurrenceId}`, () => {
|
|
164
335
|
const existingPath = occurrencePath(controllerHome, occurrence.repoId, occurrence.occurrenceId);
|
|
165
336
|
const previous = existsSync(existingPath) ? readJsonFile<ScheduleOccurrence>(existingPath) : undefined;
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
337
|
+
const expectedRevision = previous?.revision ?? 0;
|
|
338
|
+
if (occurrence.revision !== expectedRevision) {
|
|
339
|
+
throw new Error(`SCHEDULE_OCCURRENCE_REVISION_CONFLICT: ${occurrence.occurrenceId}:expected=${occurrence.revision}:actual=${expectedRevision}`);
|
|
340
|
+
}
|
|
341
|
+
const saved = { ...occurrence, revision: expectedRevision + 1, updatedAt: new Date().toISOString() };
|
|
342
|
+
writeJsonAtomic(existingPath, saved);
|
|
343
|
+
upsertOccurrenceIndexUnlocked(controllerHome, saved);
|
|
344
|
+
const schedule = getSchedule(controllerHome, saved.repoId, saved.scheduleId);
|
|
345
|
+
appendRuntimeEvent(controllerHome, { repoId: saved.repoId, entityType: 'occurrence', entityId: saved.occurrenceId, eventType: `occurrence_${saved.status}`, requestId: `${schedule.requestId}:${saved.windowKey}`, revision: saved.revision, correlationId: saved.scheduleId, data: { decision: saved.decision, jobId: saved.jobId, reason: saved.reason } });
|
|
346
|
+
return saved;
|
|
172
347
|
}, 10_000);
|
|
348
|
+
if (next.status === 'succeeded') resolveRecoveredScheduleFailureHandoffs(controllerHome, next);
|
|
349
|
+
return next;
|
|
173
350
|
}
|
|
174
351
|
|
|
175
352
|
export function getOccurrence(controllerHome: string, repoId: string, occurrenceId: string): ScheduleOccurrence | undefined {
|
|
@@ -184,7 +361,20 @@ export function recordScheduleOccurrenceHandoff(
|
|
|
184
361
|
input: ScheduleOccurrenceHandoffInput,
|
|
185
362
|
): ScheduleOccurrence {
|
|
186
363
|
if (occurrence.handoffId) return occurrence;
|
|
187
|
-
const
|
|
364
|
+
const reusable = input.creationReason === 'repeated_infrastructure_failure'
|
|
365
|
+
? activeScheduleFailureHandoff(controllerHome, occurrence.repoId, schedule.scheduleId, input.creationReason, input.reason)
|
|
366
|
+
: undefined;
|
|
367
|
+
if (reusable) return saveOccurrence(controllerHome, { ...occurrence, handoffId: reusable.id });
|
|
368
|
+
const handoffId = input.creationReason === 'repeated_infrastructure_failure'
|
|
369
|
+
? stableScheduleFailureHandoffId(
|
|
370
|
+
controllerHome,
|
|
371
|
+
occurrence.repoId,
|
|
372
|
+
schedule.scheduleId,
|
|
373
|
+
input.creationReason,
|
|
374
|
+
input.reason,
|
|
375
|
+
occurrence.occurrenceId,
|
|
376
|
+
)
|
|
377
|
+
: `schedule-${occurrence.occurrenceId}`;
|
|
188
378
|
const evidenceRefs: NonNullable<ScheduleOccurrenceHandoffInput['evidenceRefs']> = [
|
|
189
379
|
{ title: `Occurrence ${occurrence.occurrenceId}`, summary: occurrence.reason ?? input.reason, detailLevel: 'summary' as const },
|
|
190
380
|
];
|
|
@@ -10,10 +10,8 @@ export type ScheduleTriggerType =
|
|
|
10
10
|
| 'manual';
|
|
11
11
|
|
|
12
12
|
export interface ScheduleCondition {
|
|
13
|
-
kind: 'repository_clean' | 'job_succeeded' | 'job_terminal'
|
|
13
|
+
kind: 'repository_clean' | 'job_succeeded' | 'job_terminal';
|
|
14
14
|
jobId?: string;
|
|
15
|
-
semanticKey?: string;
|
|
16
|
-
observationThreshold?: number;
|
|
17
15
|
}
|
|
18
16
|
|
|
19
17
|
export interface ScheduleTrigger {
|
|
@@ -73,7 +71,7 @@ export interface RepositorySchedule {
|
|
|
73
71
|
consecutiveNoops?: number;
|
|
74
72
|
nextEligibleAt?: string;
|
|
75
73
|
pausedReason?: string;
|
|
76
|
-
/** Last deterministic external observation for
|
|
74
|
+
/** Last deterministic external observation for browser or issue watchers. */
|
|
77
75
|
lastObservationAt?: string;
|
|
78
76
|
lastObservationFingerprint?: string;
|
|
79
77
|
lastObservationChangedAt?: string;
|