@moretea-labs/forge 1.6.0 → 1.7.0
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 +2 -0
- package/CHANGELOG.md +26 -0
- package/CLAUDE.md +2 -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/03-orchestration.partial.md +1 -1
- package/assets/plugin-registry.v1.json +1 -1
- 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-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/migrate-project-template.sh +22 -45
- 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/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 +170 -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-runtime-self-healing-loop.md +1 -1
- package/docs/operations/controller-browser-plugin.md +26 -6
- package/docs/operations/controller-performance-and-502.md +124 -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 +29 -10
- package/scripts/activate-source-baseline.ts +17 -13
- 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/capture-plan.sh +3 -3
- package/scripts/check-agent-tooling.sh +8 -35
- package/scripts/check-architecture-sync.sh +17 -86
- package/scripts/check-mcp-compatibility.ts +14 -0
- package/scripts/check-runtime-architecture.mjs +107 -64
- package/scripts/check-stable-baseline.ts +92 -0
- package/scripts/check-task-workflow.sh +4 -4
- package/scripts/contract-worktree.sh +1 -21
- package/scripts/ensure-task-workflow.sh +22 -55
- package/scripts/host-temporary-cloud-mcp-e2e.sh +265 -0
- package/scripts/install-standalone-recovery.ts +24 -0
- package/scripts/lib/project-init-lib.sh +15 -44
- package/scripts/migrate-project-template.sh +23 -1179
- package/scripts/new-plan.sh +1 -1
- package/scripts/plan-to-todo.sh +11 -36
- package/scripts/run-governed-gate.ts +2 -1
- package/scripts/ship-worktrees.sh +2 -29
- 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/verify-contract.sh +1 -111
- package/scripts/verify-forge-runtime.sh +0 -1
- package/scripts/verify-sprint.sh +3 -180
- package/scripts/workflow-contract.ts +1 -5
- package/src/cli/commands/init-hook.ts +3 -3
- package/src/cli/commands/mcp.ts +9 -5
- package/src/cli/commands/plugin.ts +1 -1
- 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 +106 -3
- 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 +785 -469
- 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/github.ts +133 -0
- 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 +0 -217
- 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 +0 -513
- 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 +0 -189
- package/src/cli/mcp/multi-repository.ts +16 -5
- package/src/cli/mcp/oauth.ts +175 -26
- package/src/cli/mcp/repository-tools.ts +247 -107
- package/src/cli/mcp/server.ts +537 -111
- package/src/cli/mcp/tool-capability-cache.ts +1 -1
- package/src/cli/mcp/toolset-selection.ts +1 -1
- package/src/cli/mcp/toolset.ts +42 -5
- package/src/cli/mcp/transports/http.ts +101 -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 +101 -19
- package/src/cli/repositories/command-executor.ts +55 -521
- 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 +121 -0
- package/src/cli/repositories/selected-path-actions.ts +17 -9
- package/src/cli/repositories/structured-git.ts +61 -3
- package/src/cli/repository/inspector.ts +553 -63
- 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.ts +275 -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 +47 -8
- package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +86 -24
- 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-handle-store.ts +27 -2
- package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +258 -0
- package/src/runtime/control-plane/execution/work-remote-delivery.ts +215 -0
- package/src/runtime/control-plane/execution/work-task-receipt.ts +54 -11
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +143 -8
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +22 -14
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +85 -8
- package/src/runtime/control-plane/facade/capability-registry.ts +129 -25
- package/src/runtime/control-plane/facade/controller-round-relay.ts +991 -0
- package/src/runtime/control-plane/facade/controller-session-store.ts +132 -1
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +2 -0
- package/src/runtime/control-plane/facade/goal-workloop.ts +686 -129
- package/src/runtime/control-plane/facade/index.ts +1 -0
- package/src/runtime/control-plane/facade/plan-contract-store.ts +185 -20
- 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 +22 -5
- package/src/runtime/control-plane/facade/work-contract-store.ts +175 -73
- 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 +145 -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 +273 -542
- 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 +50 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +355 -116
- package/src/runtime/control-plane/launcher/launch-reservation-store.ts +40 -0
- package/src/runtime/control-plane/launcher/thin-launcher.ts +194 -10
- package/src/runtime/control-plane/persistence/controller-home-migration.ts +487 -0
- 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/routing/route-policy.ts +7 -16
- 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/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 +53 -0
- package/src/runtime/execution/process-runtime/check-scheduling.ts +114 -0
- package/src/runtime/execution/process-runtime/command-facade.ts +345 -40
- package/src/runtime/execution/process-runtime/gc.ts +38 -6
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +1184 -0
- package/src/runtime/execution/process-runtime/persisted-check.ts +47 -4
- package/src/runtime/execution/process-runtime/resource-claims.ts +10 -6
- package/src/runtime/execution/process-runtime/runtime.ts +43 -3
- package/src/runtime/execution/process-runtime/store.ts +1 -0
- package/src/runtime/execution/process-runtime/types.ts +2 -0
- 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/workers/executor.ts +0 -48
- package/src/runtime/gateway/mcp/execution-tools.ts +1192 -114
- package/src/runtime/gateway/mcp/process-tools.ts +83 -37
- package/src/runtime/gateway/mcp/router.ts +184 -82
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +546 -0
- package/src/runtime/gateway/mcp/runtime-tools.ts +2224 -1589
- package/src/runtime/plugins/app-store-connect-adapter.ts +123 -19
- package/src/runtime/plugins/browser-adapter.ts +2901 -241
- package/src/runtime/plugins/browser-automation-service.ts +10 -0
- package/src/runtime/plugins/browser-handoff.ts +92 -2
- package/src/runtime/plugins/browser-macos-bridge.ts +582 -49
- 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 +152 -0
- package/src/runtime/plugins/browser-runtime.ts +183 -0
- package/src/runtime/plugins/browser-session-authority.ts +332 -0
- package/src/runtime/plugins/capability-authorization-grants.ts +354 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +8 -2
- package/src/runtime/plugins/external-adapter.ts +324 -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 +55 -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 +269 -29
- package/src/runtime/plugins/types.ts +35 -2
- 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 +393 -28
- package/src/runtime/root/mcp-transport.ts +180 -10
- package/src/runtime/root/package-connector-service.ts +186 -8
- package/src/runtime/root/package-runtime-release.ts +143 -17
- package/src/runtime/root/package-runtime-service.ts +430 -22
- package/src/runtime/root/release-manifest.ts +7 -2
- package/src/runtime/root/release-materialize.ts +19 -0
- 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 +2 -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 +700 -92
- package/src/runtime/standalone-recovery/entry.ts +99 -23
- package/src/runtime/standalone-recovery/installer.ts +19 -3
- 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 +287 -151
- package/src/runtime/workflow/schedules/github-issue-watch.ts +158 -0
- package/src/runtime/workflow/schedules/settlement.ts +48 -4
- package/src/runtime/workflow/schedules/store.ts +131 -10
- 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
|
+
}
|
|
@@ -11,14 +11,21 @@ import {
|
|
|
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
30
|
pausedReason: shouldPause ? 'Maximum consecutive failures reached.' : undefined,
|
|
24
31
|
};
|
|
@@ -68,6 +75,43 @@ export function applyScheduleFailure(
|
|
|
68
75
|
return { schedule: nextSchedule, occurrence: nextOccurrence };
|
|
69
76
|
}
|
|
70
77
|
|
|
78
|
+
export function applyScheduleRetryableFailure(
|
|
79
|
+
controllerHome: string,
|
|
80
|
+
scheduleId: string,
|
|
81
|
+
repoId: string,
|
|
82
|
+
occurrenceId: string,
|
|
83
|
+
options: {
|
|
84
|
+
outcome: 'failed' | 'skipped';
|
|
85
|
+
decision: ScheduleDecisionType;
|
|
86
|
+
reason: string;
|
|
87
|
+
countFailure?: boolean;
|
|
88
|
+
},
|
|
89
|
+
): { schedule: ReturnType<typeof getSchedule>; occurrence?: ScheduleOccurrence } {
|
|
90
|
+
const schedule = getSchedule(controllerHome, repoId, scheduleId);
|
|
91
|
+
const occurrence = getOccurrence(controllerHome, repoId, occurrenceId);
|
|
92
|
+
let nextOccurrence = occurrence;
|
|
93
|
+
if (occurrence && !TERMINAL_OCCURRENCE_STATUSES.has(occurrence.status)) {
|
|
94
|
+
nextOccurrence = saveOccurrence(controllerHome, {
|
|
95
|
+
...occurrence,
|
|
96
|
+
status: options.outcome,
|
|
97
|
+
decision: options.decision,
|
|
98
|
+
reason: options.reason,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const countFailure = options.countFailure !== false;
|
|
102
|
+
const nextFailures = countFailure ? schedule.consecutiveFailures + 1 : schedule.consecutiveFailures;
|
|
103
|
+
const nextSchedule = saveSchedule(controllerHome, {
|
|
104
|
+
...schedule,
|
|
105
|
+
consecutiveFailures: nextFailures,
|
|
106
|
+
nextEligibleAt: computeScheduleBackoff(schedule, Math.max(1, nextFailures)),
|
|
107
|
+
// Retryable readiness/semantic-wait failures never turn a durable schedule
|
|
108
|
+
// into a manual-resume task. Respect a concurrent explicit pause, however.
|
|
109
|
+
enabled: schedule.enabled,
|
|
110
|
+
pausedReason: schedule.enabled ? undefined : schedule.pausedReason,
|
|
111
|
+
});
|
|
112
|
+
return { schedule: nextSchedule, occurrence: nextOccurrence };
|
|
113
|
+
}
|
|
114
|
+
|
|
71
115
|
export function markScheduledExecutionRunning(controllerHome: string, job: ExecutionJob): void {
|
|
72
116
|
if (job.type !== 'scheduled-occurrence') return;
|
|
73
117
|
const occurrenceId = typeof job.payload.occurrenceId === 'string' ? job.payload.occurrenceId : undefined;
|
|
@@ -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';
|
|
@@ -159,17 +164,120 @@ export function listSchedules(controllerHome: string, repoId: string): Repositor
|
|
|
159
164
|
} catch { return []; }
|
|
160
165
|
}
|
|
161
166
|
|
|
167
|
+
export function deleteSchedule(controllerHome: string, repoId: string, scheduleId: string): RepositorySchedule {
|
|
168
|
+
return withControllerLock(controllerHome, { scope: 'task', repoId, taskId: `schedule-${scheduleId}` }, `delete-schedule:${scheduleId}`, () => {
|
|
169
|
+
const current = getSchedule(controllerHome, repoId, scheduleId);
|
|
170
|
+
if (current.enabled) throw new Error(`SCHEDULE_DELETE_REQUIRES_PAUSED: ${scheduleId}`);
|
|
171
|
+
if (listActiveOccurrences(controllerHome, repoId, scheduleId).length > 0) {
|
|
172
|
+
throw new Error(`SCHEDULE_DELETE_ACTIVE_OCCURRENCE: ${scheduleId}`);
|
|
173
|
+
}
|
|
174
|
+
rmSync(schedulePath(controllerHome, repoId, scheduleId), { force: true });
|
|
175
|
+
const requestRecordPath = requestPath(controllerHome, repoId, current.requestId);
|
|
176
|
+
if (existsSync(requestRecordPath)) {
|
|
177
|
+
const record = readJsonFile<ScheduleRequestRecord>(requestRecordPath);
|
|
178
|
+
if (record.scheduleId === scheduleId) rmSync(requestRecordPath, { force: true });
|
|
179
|
+
}
|
|
180
|
+
appendRuntimeEvent(controllerHome, {
|
|
181
|
+
repoId,
|
|
182
|
+
entityType: 'schedule',
|
|
183
|
+
entityId: scheduleId,
|
|
184
|
+
eventType: 'schedule_deleted',
|
|
185
|
+
requestId: current.requestId,
|
|
186
|
+
revision: current.revision + 1,
|
|
187
|
+
data: { name: current.name },
|
|
188
|
+
});
|
|
189
|
+
return current;
|
|
190
|
+
}, 10_000);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function scheduleFailureClass(reason: string): string {
|
|
194
|
+
const trimmed = reason.trim();
|
|
195
|
+
const explicitCode = trimmed.match(/^([A-Z][A-Z0-9_]{2,})(?::|\b)/)?.[1];
|
|
196
|
+
if (explicitCode) return explicitCode;
|
|
197
|
+
const prefix = (trimmed.split(':', 1)[0] ?? trimmed)
|
|
198
|
+
.replace(/\s+/g, ' ')
|
|
199
|
+
.trim()
|
|
200
|
+
.toLowerCase()
|
|
201
|
+
.slice(0, 120);
|
|
202
|
+
return prefix || 'unknown_failure';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function activeScheduleFailureHandoff(
|
|
206
|
+
controllerHome: string,
|
|
207
|
+
repoId: string,
|
|
208
|
+
scheduleId: string,
|
|
209
|
+
creationReason: ScheduleOccurrenceHandoffInput['creationReason'],
|
|
210
|
+
reason: string,
|
|
211
|
+
) {
|
|
212
|
+
const failureClass = scheduleFailureClass(reason);
|
|
213
|
+
return listHandoffItems({ controllerHome, repoId, status: 'active', limit: 100 }).find((item) => (
|
|
214
|
+
item.currentState?.taskId === scheduleId
|
|
215
|
+
&& item.creationReason === creationReason
|
|
216
|
+
&& scheduleFailureClass(item.reason) === failureClass
|
|
217
|
+
));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function stableScheduleFailureHandoffId(
|
|
221
|
+
controllerHome: string,
|
|
222
|
+
repoId: string,
|
|
223
|
+
scheduleId: string,
|
|
224
|
+
creationReason: ScheduleOccurrenceHandoffInput['creationReason'],
|
|
225
|
+
reason: string,
|
|
226
|
+
occurrenceId: string,
|
|
227
|
+
): string {
|
|
228
|
+
const digest = createHash('sha256')
|
|
229
|
+
.update(`${scheduleId}:${creationReason}:${scheduleFailureClass(reason)}`)
|
|
230
|
+
.digest('hex')
|
|
231
|
+
.slice(0, 20);
|
|
232
|
+
const baseId = `schedule-failure-${digest}`;
|
|
233
|
+
if (!getHandoffItem({ controllerHome, repoId }, baseId)) return baseId;
|
|
234
|
+
// A previous failure streak may already have resolved the stable item. Handoff
|
|
235
|
+
// records are immutable apart from lifecycle status, so use one new epoch id;
|
|
236
|
+
// subsequent failures in this streak will discover and reuse the active item.
|
|
237
|
+
const epoch = createHash('sha256').update(occurrenceId).digest('hex').slice(0, 8);
|
|
238
|
+
return `${baseId}-${epoch}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function resolveRecoveredScheduleFailureHandoffs(controllerHome: string, occurrence: ScheduleOccurrence): void {
|
|
242
|
+
try {
|
|
243
|
+
const active = listHandoffItems({ controllerHome, repoId: occurrence.repoId, status: 'active', limit: 100 })
|
|
244
|
+
.filter((item) => (
|
|
245
|
+
item.currentState?.taskId === occurrence.scheduleId
|
|
246
|
+
&& item.creationReason === 'repeated_infrastructure_failure'
|
|
247
|
+
));
|
|
248
|
+
for (const item of active) {
|
|
249
|
+
try {
|
|
250
|
+
resolveHandoffItem(
|
|
251
|
+
{ controllerHome, repoId: occurrence.repoId },
|
|
252
|
+
item.id,
|
|
253
|
+
{
|
|
254
|
+
decision: `Schedule ${occurrence.scheduleId} recovered on occurrence ${occurrence.occurrenceId}.`,
|
|
255
|
+
resolver: 'schedule-runtime',
|
|
256
|
+
},
|
|
257
|
+
);
|
|
258
|
+
} catch {
|
|
259
|
+
// The succeeded occurrence remains authoritative. Inbox cleanup is
|
|
260
|
+
// best-effort and must never turn schedule recovery into a failure.
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
} catch {
|
|
264
|
+
// Preserve successful Schedule settlement even if Inbox storage is unavailable.
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
162
268
|
export function saveOccurrence(controllerHome: string, occurrence: ScheduleOccurrence): ScheduleOccurrence {
|
|
163
|
-
|
|
269
|
+
const next = withControllerLock(controllerHome, { scope: 'task', repoId: occurrence.repoId, taskId: `schedule-${occurrence.scheduleId}` }, `save-occurrence:${occurrence.occurrenceId}`, () => {
|
|
164
270
|
const existingPath = occurrencePath(controllerHome, occurrence.repoId, occurrence.occurrenceId);
|
|
165
271
|
const previous = existsSync(existingPath) ? readJsonFile<ScheduleOccurrence>(existingPath) : undefined;
|
|
166
|
-
const
|
|
167
|
-
writeJsonAtomic(existingPath,
|
|
168
|
-
upsertOccurrenceIndexUnlocked(controllerHome,
|
|
169
|
-
const schedule = getSchedule(controllerHome,
|
|
170
|
-
appendRuntimeEvent(controllerHome, { repoId:
|
|
171
|
-
return
|
|
272
|
+
const saved = { ...occurrence, revision: (previous?.revision ?? occurrence.revision ?? 0) + 1, updatedAt: new Date().toISOString() };
|
|
273
|
+
writeJsonAtomic(existingPath, saved);
|
|
274
|
+
upsertOccurrenceIndexUnlocked(controllerHome, saved);
|
|
275
|
+
const schedule = getSchedule(controllerHome, saved.repoId, saved.scheduleId);
|
|
276
|
+
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 } });
|
|
277
|
+
return saved;
|
|
172
278
|
}, 10_000);
|
|
279
|
+
if (next.status === 'succeeded') resolveRecoveredScheduleFailureHandoffs(controllerHome, next);
|
|
280
|
+
return next;
|
|
173
281
|
}
|
|
174
282
|
|
|
175
283
|
export function getOccurrence(controllerHome: string, repoId: string, occurrenceId: string): ScheduleOccurrence | undefined {
|
|
@@ -184,7 +292,20 @@ export function recordScheduleOccurrenceHandoff(
|
|
|
184
292
|
input: ScheduleOccurrenceHandoffInput,
|
|
185
293
|
): ScheduleOccurrence {
|
|
186
294
|
if (occurrence.handoffId) return occurrence;
|
|
187
|
-
const
|
|
295
|
+
const reusable = input.creationReason === 'repeated_infrastructure_failure'
|
|
296
|
+
? activeScheduleFailureHandoff(controllerHome, occurrence.repoId, schedule.scheduleId, input.creationReason, input.reason)
|
|
297
|
+
: undefined;
|
|
298
|
+
if (reusable) return saveOccurrence(controllerHome, { ...occurrence, handoffId: reusable.id });
|
|
299
|
+
const handoffId = input.creationReason === 'repeated_infrastructure_failure'
|
|
300
|
+
? stableScheduleFailureHandoffId(
|
|
301
|
+
controllerHome,
|
|
302
|
+
occurrence.repoId,
|
|
303
|
+
schedule.scheduleId,
|
|
304
|
+
input.creationReason,
|
|
305
|
+
input.reason,
|
|
306
|
+
occurrence.occurrenceId,
|
|
307
|
+
)
|
|
308
|
+
: `schedule-${occurrence.occurrenceId}`;
|
|
188
309
|
const evidenceRefs: NonNullable<ScheduleOccurrenceHandoffInput['evidenceRefs']> = [
|
|
189
310
|
{ title: `Occurrence ${occurrence.occurrenceId}`, summary: occurrence.reason ?? input.reason, detailLevel: 'summary' as const },
|
|
190
311
|
];
|
|
@@ -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;
|
|
@@ -22,7 +22,7 @@ export type ContinuationControllerType = 'chatgpt' | 'codex' | 'claude' | 'grok'
|
|
|
22
22
|
export type WorkScheduleMode = 'continuation' | 'browser_watch' | 'browser_keepalive';
|
|
23
23
|
|
|
24
24
|
export interface WorkContinuationScheduleInput {
|
|
25
|
-
workId
|
|
25
|
+
workId?: string;
|
|
26
26
|
scheduleMode?: WorkScheduleMode;
|
|
27
27
|
controllerType?: ContinuationControllerType;
|
|
28
28
|
executable?: string;
|
|
@@ -106,8 +106,9 @@ function normalizedTrigger(input: WorkContinuationScheduleInput): ScheduleTrigge
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
function wakeArguments(input: WorkContinuationScheduleInput, controllerType: ContinuationControllerType): Record<string, unknown> {
|
|
109
|
+
const workId = input.workId?.trim();
|
|
109
110
|
return {
|
|
110
|
-
work_id:
|
|
111
|
+
...(workId ? { work_id: workId } : {}),
|
|
111
112
|
controller_type: controllerType,
|
|
112
113
|
...(input.executable?.trim() ? { executable: input.executable.trim() } : {}),
|
|
113
114
|
...(input.launchArgs ? { launch_args: input.launchArgs.map(String) } : {}),
|
|
@@ -146,10 +147,13 @@ export function createWorkContinuationSchedule(
|
|
|
146
147
|
controllerHome: string,
|
|
147
148
|
repoId: string,
|
|
148
149
|
input: WorkContinuationScheduleInput,
|
|
149
|
-
): { schedule: RepositorySchedule; work
|
|
150
|
-
const work = activeWork(controllerHome, repoId, input.workId);
|
|
150
|
+
): { schedule: RepositorySchedule; work?: WorkContract } {
|
|
151
151
|
const controllerType = input.controllerType ?? 'chatgpt';
|
|
152
152
|
const scheduleMode = input.scheduleMode ?? 'continuation';
|
|
153
|
+
const requestedWorkId = input.workId?.trim();
|
|
154
|
+
if (!requestedWorkId && scheduleMode !== 'browser_keepalive') throw new Error('WORK_ID_REQUIRED');
|
|
155
|
+
if (!requestedWorkId && scheduleMode === 'browser_keepalive' && controllerType !== 'chatgpt') throw new Error('STANDALONE_BROWSER_KEEPALIVE_CHATGPT_REQUIRED');
|
|
156
|
+
const work = requestedWorkId ? activeWork(controllerHome, repoId, requestedWorkId) : undefined;
|
|
153
157
|
const trigger = normalizedTrigger(input);
|
|
154
158
|
const operation = scheduleMode === 'continuation' ? 'external_controller_wake' : 'browser_probe';
|
|
155
159
|
const actionArguments = scheduleMode === 'continuation'
|
|
@@ -157,9 +161,10 @@ export function createWorkContinuationSchedule(
|
|
|
157
161
|
: probeArguments(input, controllerType, scheduleMode === 'browser_keepalive');
|
|
158
162
|
assertAutomatedOperationAllowed(operation, actionArguments);
|
|
159
163
|
const name = input.scheduleName?.trim()
|
|
160
|
-
|| (scheduleMode === 'browser_watch' ? `Watch external state for Work ${work
|
|
161
|
-
: scheduleMode === 'browser_keepalive'
|
|
162
|
-
|
|
164
|
+
|| (scheduleMode === 'browser_watch' ? `Watch external state for Work ${work!.workId}`
|
|
165
|
+
: scheduleMode === 'browser_keepalive'
|
|
166
|
+
? (work ? `Keep browser session alive for Work ${work.workId}` : 'Keep browser session alive')
|
|
167
|
+
: `Continue Work ${work!.workId}`);
|
|
163
168
|
const policy = {
|
|
164
169
|
maxActiveOccurrences: 1,
|
|
165
170
|
maxFailures: input.maxFailures !== undefined ? Math.max(1, Math.trunc(input.maxFailures)) : 3,
|
|
@@ -169,7 +174,7 @@ export function createWorkContinuationSchedule(
|
|
|
169
174
|
backoffBaseMinutes: input.backoffBaseMinutes !== undefined ? Math.max(1, Math.trunc(input.backoffBaseMinutes)) : 5,
|
|
170
175
|
backoffMaxMinutes: input.backoffMaxMinutes !== undefined ? Math.max(1, Math.trunc(input.backoffMaxMinutes)) : 24 * 60,
|
|
171
176
|
};
|
|
172
|
-
const stopConditions = input.stopConditions ?? ['work_terminal', 'human_review_required', 'external_blocker'];
|
|
177
|
+
const stopConditions = input.stopConditions ?? (work ? ['work_terminal', 'human_review_required', 'external_blocker'] : []);
|
|
173
178
|
|
|
174
179
|
// A durable Work has one authoritative continuation lane. Changing cadence,
|
|
175
180
|
// prompt, controller, or policy updates that lane instead of minting another
|
|
@@ -178,7 +183,7 @@ export function createWorkContinuationSchedule(
|
|
|
178
183
|
if (scheduleMode === 'continuation') {
|
|
179
184
|
const existing = listSchedules(controllerHome, repoId)
|
|
180
185
|
.filter((candidate) => candidate.action.operation === 'external_controller_wake')
|
|
181
|
-
.filter((candidate) => candidate.action.arguments?.work_id === work
|
|
186
|
+
.filter((candidate) => candidate.action.arguments?.work_id === work!.workId);
|
|
182
187
|
const authoritative = [...existing].reverse().find((candidate) => candidate.enabled) ?? existing.at(-1);
|
|
183
188
|
if (authoritative) {
|
|
184
189
|
const schedule = saveSchedule(controllerHome, {
|
|
@@ -196,7 +201,7 @@ export function createWorkContinuationSchedule(
|
|
|
196
201
|
saveSchedule(controllerHome, {
|
|
197
202
|
...duplicate,
|
|
198
203
|
enabled: false,
|
|
199
|
-
pausedReason: `Superseded by authoritative continuation ${schedule.scheduleId} for Work ${work
|
|
204
|
+
pausedReason: `Superseded by authoritative continuation ${schedule.scheduleId} for Work ${work!.workId}.`,
|
|
200
205
|
});
|
|
201
206
|
}
|
|
202
207
|
return { schedule, work };
|
|
@@ -205,7 +210,7 @@ export function createWorkContinuationSchedule(
|
|
|
205
210
|
|
|
206
211
|
const semantic = JSON.stringify({
|
|
207
212
|
repoId,
|
|
208
|
-
workId: work
|
|
213
|
+
workId: work?.workId,
|
|
209
214
|
controllerType,
|
|
210
215
|
scheduleMode,
|
|
211
216
|
trigger,
|
|
@@ -215,7 +220,9 @@ export function createWorkContinuationSchedule(
|
|
|
215
220
|
stopConditions,
|
|
216
221
|
});
|
|
217
222
|
const requestId = input.requestId?.trim()
|
|
218
|
-
||
|
|
223
|
+
|| (work
|
|
224
|
+
? `work-continuation:${repoId}:${work.workId}:${createHash('sha256').update(semantic).digest('hex').slice(0, 16)}`
|
|
225
|
+
: `browser-keepalive:${repoId}:${createHash('sha256').update(semantic).digest('hex').slice(0, 16)}`);
|
|
219
226
|
const schedule = createSchedule(controllerHome, {
|
|
220
227
|
requestId,
|
|
221
228
|
repoId,
|
|
@@ -277,7 +284,13 @@ export function resumeWorkContinuationSchedule(controllerHome: string, repoId: s
|
|
|
277
284
|
const schedule = continuationSchedule(getSchedule(controllerHome, repoId, scheduleId));
|
|
278
285
|
const workId = typeof schedule.action.arguments?.work_id === 'string' ? schedule.action.arguments.work_id : '';
|
|
279
286
|
activeWork(controllerHome, repoId, workId);
|
|
280
|
-
return saveSchedule(controllerHome, {
|
|
287
|
+
return saveSchedule(controllerHome, {
|
|
288
|
+
...schedule,
|
|
289
|
+
enabled: true,
|
|
290
|
+
pausedReason: undefined,
|
|
291
|
+
consecutiveFailures: 0,
|
|
292
|
+
nextEligibleAt: undefined,
|
|
293
|
+
});
|
|
281
294
|
}
|
|
282
295
|
|
|
283
296
|
export async function triggerWorkContinuationSchedule(
|
|
@@ -26,30 +26,24 @@ export type TestModule = (typeof TEST_MODULES)[number];
|
|
|
26
26
|
export type TestResource = (typeof TEST_RESOURCES)[number];
|
|
27
27
|
export type TestGate = 'affected' | 'core' | 'integration' | 'infrastructure' | 'fault' | 'full';
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Acceptance evidence is the default. Tests must protect externally observable
|
|
31
|
+
* contracts, migrations/rollback, failure injection, security boundaries, or
|
|
32
|
+
* a recorded production incident. Do not add tests for private helpers,
|
|
33
|
+
* implementation payloads, copy, enum order, or mock-only lifecycles.
|
|
34
|
+
*/
|
|
35
|
+
export const TEST_GOVERNANCE_POLICY = 'acceptance-first-no-internal-unit-tests' as const;
|
|
36
|
+
|
|
29
37
|
export interface TestManifestEntry {
|
|
30
38
|
module: TestModule;
|
|
31
39
|
resource: TestResource;
|
|
32
40
|
smoke?: boolean;
|
|
33
41
|
}
|
|
34
42
|
|
|
35
|
-
export interface TestBudgetPolicy {
|
|
36
|
-
maxTestFiles: number;
|
|
37
|
-
maxTestLines: number;
|
|
38
|
-
maxPureFiles: number;
|
|
39
|
-
maxTempIsolatedFiles: number;
|
|
40
|
-
maxProcessTreeFiles: number;
|
|
41
|
-
maxGitWorktreeFiles: number;
|
|
42
|
-
maxFixedPortFiles: number;
|
|
43
|
-
maxRuntimeSingletonFiles: number;
|
|
44
|
-
maxControllerHomeIsolatedFiles: number;
|
|
45
|
-
maxDestructiveFiles: number;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
43
|
export interface TestManifest {
|
|
49
44
|
version: 1;
|
|
50
45
|
modules: TestModule[];
|
|
51
46
|
resources: TestResource[];
|
|
52
|
-
policy: TestBudgetPolicy;
|
|
53
47
|
pathModuleRules: Array<{ prefix: string; modules: TestModule[] }>;
|
|
54
48
|
tests: Record<string, TestManifestEntry>;
|
|
55
49
|
}
|
|
@@ -232,32 +226,6 @@ export function validateTestManifest(repoRoot: string, manifest = loadTestManife
|
|
|
232
226
|
if (!TEST_RESOURCES.includes(entry.resource)) errors.push(`${file}: invalid resource ${String(entry.resource)}`);
|
|
233
227
|
}
|
|
234
228
|
if (!Object.values(manifest.tests).some((entry) => entry.smoke)) errors.push('manifest has no core smoke tests');
|
|
235
|
-
const resourceCounts = Object.values(manifest.tests).reduce<Record<string, number>>((counts, entry) => {
|
|
236
|
-
counts[entry.resource] = (counts[entry.resource] ?? 0) + 1;
|
|
237
|
-
return counts;
|
|
238
|
-
}, {});
|
|
239
|
-
const testLines = declaredFiles.reduce((total, file) => {
|
|
240
|
-
try {
|
|
241
|
-
return total + readFileSync(resolve(repoRoot, file), 'utf8').split('\n').length;
|
|
242
|
-
} catch (_error) {
|
|
243
|
-
return total;
|
|
244
|
-
}
|
|
245
|
-
}, 0);
|
|
246
|
-
const budgetChecks: Array<[string, number, number | undefined]> = [
|
|
247
|
-
['maxTestFiles', declaredFiles.length, manifest.policy?.maxTestFiles],
|
|
248
|
-
['maxTestLines', testLines, manifest.policy?.maxTestLines],
|
|
249
|
-
['maxPureFiles', resourceCounts.pure ?? 0, manifest.policy?.maxPureFiles],
|
|
250
|
-
['maxTempIsolatedFiles', resourceCounts['temp-isolated'] ?? 0, manifest.policy?.maxTempIsolatedFiles],
|
|
251
|
-
['maxProcessTreeFiles', resourceCounts['process-tree'] ?? 0, manifest.policy?.maxProcessTreeFiles],
|
|
252
|
-
['maxGitWorktreeFiles', resourceCounts['git-worktree'] ?? 0, manifest.policy?.maxGitWorktreeFiles],
|
|
253
|
-
['maxFixedPortFiles', resourceCounts['fixed-port'] ?? 0, manifest.policy?.maxFixedPortFiles],
|
|
254
|
-
['maxRuntimeSingletonFiles', resourceCounts['runtime-singleton'] ?? 0, manifest.policy?.maxRuntimeSingletonFiles],
|
|
255
|
-
['maxControllerHomeIsolatedFiles', resourceCounts['controller-home-isolated'] ?? 0, manifest.policy?.maxControllerHomeIsolatedFiles],
|
|
256
|
-
['maxDestructiveFiles', resourceCounts.destructive ?? 0, manifest.policy?.maxDestructiveFiles],
|
|
257
|
-
];
|
|
258
|
-
for (const [name, actual, limit] of budgetChecks) {
|
|
259
|
-
if (typeof limit !== 'number' || actual > limit) errors.push(`test budget ${name} exceeded: ${actual} > ${String(limit)}`);
|
|
260
|
-
}
|
|
261
229
|
return errors;
|
|
262
230
|
}
|
|
263
231
|
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# Heartbeat Triage
|
|
2
|
-
|
|
3
|
-
Heartbeat triage is the repo-local scheduled discovery loop. It records findings
|
|
4
|
-
for a human to review; it does not approve plans, execute fixes, spawn agents,
|
|
5
|
-
open PRs, or install a persistent scheduler.
|
|
6
|
-
|
|
7
|
-
## Command
|
|
8
|
-
|
|
9
|
-
Run from the repository root:
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
bash .ai/harness/scripts/heartbeat-triage.sh run --source scheduled
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
The command appends a run section to `.ai/harness/triage/inbox.md` and writes a
|
|
16
|
-
JSON snapshot under `.ai/harness/runs/`. Each run records:
|
|
17
|
-
|
|
18
|
-
- `workflow-check`: `bash .ai/harness/scripts/check-task-workflow.sh --strict`
|
|
19
|
-
- `sprint-next`: `bash .ai/harness/scripts/sprint-backlog.sh next` when an active sprint
|
|
20
|
-
marker exists, with a read-only sprint-file fallback when it does not
|
|
21
|
-
- `drift-requests`: pending files under `docs/architecture/requests/`
|
|
22
|
-
|
|
23
|
-
## cron
|
|
24
|
-
|
|
25
|
-
Example cron entry for a local checkout:
|
|
26
|
-
|
|
27
|
-
```cron
|
|
28
|
-
17 8 * * * cd /path/to/repo && bash .ai/harness/scripts/heartbeat-triage.sh run --source scheduled >/tmp/forge-heartbeat.log 2>&1
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## loop
|
|
32
|
-
|
|
33
|
-
Example long-running loop for a supervised terminal, launch wrapper, or local
|
|
34
|
-
process manager:
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
while true; do
|
|
38
|
-
cd /path/to/repo && bash .ai/harness/scripts/heartbeat-triage.sh run --source scheduled
|
|
39
|
-
sleep 86400
|
|
40
|
-
done
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## Adoption Review
|
|
44
|
-
|
|
45
|
-
Every run includes an "Adoption review due" date fourteen days out. The review
|
|
46
|
-
question is narrow: did the inbox produce any human-accepted triage item? If no
|
|
47
|
-
item is accepted during the review window, remove the scheduler and keep the
|
|
48
|
-
runner as an on-demand diagnostic command.
|
|
49
|
-
|
|
50
|
-
## Guardrails
|
|
51
|
-
|
|
52
|
-
- Keep scheduler installation manual and host-local.
|
|
53
|
-
- Treat workflow failures and drift requests as inbox entries, not process
|
|
54
|
-
crashes.
|
|
55
|
-
- Keep `.ai/harness/triage/inbox.md` as runtime state, not a source-of-truth
|
|
56
|
-
plan or task ledger.
|
|
57
|
-
- Keep execution authority in the normal plan -> contract -> verify flow.
|