@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
|
@@ -5,9 +5,12 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
|
5
5
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
6
6
|
import type { CallToolResult as SdkCallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
7
7
|
import { collectRuntimePerformanceDiagnostics, inferLocalControllerProcess } from '../../diagnostics/performance';
|
|
8
|
+
import { defaultSemanticProviderRegistry, type SemanticNavigationKind, type SemanticNavigationRequest } from '../../context/semantic-navigation';
|
|
8
9
|
import type { McpToolDefinition, CallToolResult } from '../../../cli/mcp/tools';
|
|
9
10
|
import type { MultiRepositoryMcpToolContext } from '../../../cli/mcp/multi-repository';
|
|
10
11
|
import { repositoryScopedToolArgs } from '../../../cli/mcp/multi-repository';
|
|
12
|
+
import { resolveMcpPath } from '../../../cli/mcp/paths';
|
|
13
|
+
import { freshGitIdentity } from '../../../cli/repository/inspector';
|
|
11
14
|
import { reconcileReadinessProjectionSource } from '../../../cli/mcp/readiness-projection';
|
|
12
15
|
import { listRepositories, repositorySummary, resolveRepositorySelection, selectRepositoryCheckout } from '../../../cli/repositories/registry';
|
|
13
16
|
import { repositoryGitStatus } from '../../../cli/repositories/structured-git';
|
|
@@ -15,10 +18,14 @@ import { repositoryControllerRoot } from '../../../cli/repositories/controller-h
|
|
|
15
18
|
import { cancelExecutionJob, findExecutionJob, getExecutionJob, getExecutionJobByRequestId, listExecutionJobs } from '../../execution/jobs/store';
|
|
16
19
|
import { waitForExecutionJob } from '../../execution/jobs/wait';
|
|
17
20
|
import type { ExecutionJob } from '../../execution/jobs/types';
|
|
18
|
-
import { getProcessHandle, getProcessRecord, isManagedProcessActive, listProcessRecords, processCheckCompletionReceipt, processRuntimeResourceDiagnostics, readPersistedCheckResultReceipt, runPersistedCheckViaProcessRuntime, waitForProcess } from '../../execution/process-runtime';
|
|
21
|
+
import { DEFAULT_WORK_CHECK_LEASE_WAIT_MS, checkRequiresDurableWorkflow, getProcessHandle, getProcessRecord, isManagedProcessActive, listProcessRecords, listRecoverableProcessRecords, processCheckCompletionReceipt, processRuntimeResourceDiagnostics, readPersistedCheckResultReceipt, runPersistedCheckViaProcessRuntime, waitForProcess } from '../../execution/process-runtime';
|
|
22
|
+
import { getRepositoryCommandProcess, waitRepositoryCommandProcess } from '../../execution/process-runtime/command-facade';
|
|
19
23
|
import { buildJobOperationDigest } from '../../control-plane/facade/operation-digest';
|
|
20
|
-
import { readWorkHandle, transitionWorkHandle, writeWorkHandle, type WorkHandleState } from '../../control-plane/execution/work-handle-store';
|
|
24
|
+
import { readWorkHandle, transitionWorkHandle, workDeliveryBaseRevision, writeWorkHandle, type WorkHandleState } from '../../control-plane/execution/work-handle-store';
|
|
25
|
+
import { recoverTerminalWorkHandle } from '../../control-plane/execution/work-terminal-cleanup';
|
|
26
|
+
import { gitCommitAtRef, gitWorktreeSnapshot } from '../../control-plane/execution/work-lifecycle-audit';
|
|
21
27
|
import { executionIdentityForRepository } from '../../control-plane/execution/execution-identity';
|
|
28
|
+
import { commandFingerprint, verificationInputFingerprint, workspaceValidationFingerprint } from '../../control-plane/execution/verification-evidence';
|
|
22
29
|
import { acceptReviewedDirectEditWorkReconciliation, reconcileFinalizedDirectEditWorksAfterCommit } from '../../control-plane/execution/direct-edit-work-completion';
|
|
23
30
|
import { readJobEvents } from '../../evidence/event-ledger';
|
|
24
31
|
import { readExecutionArtifact } from '../../evidence/artifact-store';
|
|
@@ -41,26 +48,23 @@ import {
|
|
|
41
48
|
type RuntimeOperationalView,
|
|
42
49
|
type GradedObservation,
|
|
43
50
|
} from '../../health';
|
|
44
|
-
import { applyScheduleDedupe, buildScheduleDedupeReport,
|
|
45
|
-
import { evaluateSchedule } from '../../workflow/schedules/engine';
|
|
51
|
+
import { applyScheduleDedupe, buildScheduleDedupeReport, deleteSchedule } from '../../workflow/schedules/store';
|
|
46
52
|
import {
|
|
47
53
|
createWorkContinuationSchedule,
|
|
54
|
+
getWorkContinuationSchedule,
|
|
55
|
+
listWorkContinuationSchedules,
|
|
56
|
+
pauseWorkContinuationSchedule,
|
|
57
|
+
resumeWorkContinuationSchedule,
|
|
58
|
+
triggerWorkContinuationSchedule,
|
|
48
59
|
type ContinuationControllerType,
|
|
49
60
|
} from '../../workflow/schedules/work-continuation';
|
|
50
|
-
import { createPortfolioWorkflow, getPortfolioWorkflow, listPortfolioWorkflows } from '../../workflow/portfolio/store';
|
|
51
|
-
import { claimsForMcpOperation } from './resource-policy';
|
|
52
61
|
import {
|
|
53
62
|
gatewayRouteBehaviorSnapshot,
|
|
54
63
|
getMcpToolDefinition,
|
|
55
|
-
hashMcpToolArguments,
|
|
56
|
-
injectDurableCommandFields,
|
|
57
|
-
operationMetadataForTool,
|
|
58
64
|
RETIRED_AGENT_OPERATIONS,
|
|
59
65
|
routeDurableMcpCall,
|
|
60
|
-
validateMcpToolArguments,
|
|
61
66
|
} from './router';
|
|
62
67
|
import { assertAutomatedOperationAllowed } from '../../control-plane/governance/external-effects';
|
|
63
|
-
import { getCandidateFinding, listCandidateFindings, recordCandidateFinding } from '../../workflow/findings/store';
|
|
64
68
|
import { ensureRepositoryRuntimeStorage } from '../../../cli/repositories/runtime-storage';
|
|
65
69
|
import { assessWorkMode, parseExplicitTaskMode } from '../../../cli/controller/work-mode';
|
|
66
70
|
import { projectBoard } from '../../../cli/controller/issue-store';
|
|
@@ -68,10 +72,11 @@ import {
|
|
|
68
72
|
buildControllerTaskLedgerProjection,
|
|
69
73
|
writeControllerTaskLedgerArtifacts,
|
|
70
74
|
} from '../../../cli/controller/task-ledger';
|
|
71
|
-
import { buildControllerContextPack, CONTROLLER_CONTEXT_IMPACT_DOMAINS, type ControllerContextImpactDomain } from '../../../cli/controller/context-pack';
|
|
75
|
+
import { buildControllerContextPack, buildControllerContextPackAsync, CONTROLLER_CONTEXT_IMPACT_DOMAINS, type ControllerContextImpactDomain } from '../../../cli/controller/context-pack';
|
|
72
76
|
import { legacyIssueAuthorityRetired } from '../../../cli/controller/legacy-issue-cutover';
|
|
73
77
|
import { buildControllerOperationalPlan } from '../../../cli/controller/operational-plan';
|
|
74
78
|
import { listControllerChecks, readLatestControllerCheckEvidence } from '../../../cli/controller/check-runner';
|
|
79
|
+
import { buildCheckExecutionSchedule } from '../../execution/process-runtime/check-scheduling';
|
|
75
80
|
import { repositoryChangeVerify } from '../../../cli/controller/composite-operations';
|
|
76
81
|
import { listActiveAgentJobSnapshots } from '../../../cli/agent-jobs/job-manager';
|
|
77
82
|
import { readAgentExecutableReadinessSnapshot } from '../../../cli/agent-jobs/executable-resolver';
|
|
@@ -102,7 +107,8 @@ import {
|
|
|
102
107
|
} from '../../../cli/controller/runtime-config';
|
|
103
108
|
import { redactMcpText } from '../../../cli/mcp/redaction';
|
|
104
109
|
import { resolveLocalBridgeSurface, summarizeRecentJobs } from '../../shared/local-bridge-surface';
|
|
105
|
-
import { assistantPluginScope, controllerPluginRepository, executeAssistantPluginReadDirect, getAssistantPluginManifest, isDirectPluginReadAction, listAssistantPluginManifests, submitAssistantPluginAction } from '../../plugins/store';
|
|
110
|
+
import { assistantPluginScope, controllerPluginRepository, executeAssistantPluginReadDirect, finalizeRemoteEffectWorkFromActionReceipt, getAssistantPluginManifest, isDirectPluginReadAction, listAssistantPluginManifests, submitAssistantPluginAction } from '../../plugins/store';
|
|
111
|
+
import { startLightweightPluginAction, waitLightweightPluginAction } from '../../plugins/lightweight-action';
|
|
106
112
|
import { mcpPluginExecutionOrigin } from '../../plugins/execution-origin';
|
|
107
113
|
import {
|
|
108
114
|
summarizeExecutionJobForMcp,
|
|
@@ -114,28 +120,12 @@ import {
|
|
|
114
120
|
prepareWorkspaceAuthLogin,
|
|
115
121
|
previewExternalFilesystemGrant,
|
|
116
122
|
readExternalFilesystemSnapshot,
|
|
117
|
-
iosXcodeStatus,
|
|
118
|
-
iosSimulatorsList,
|
|
119
|
-
iosProjectDiscover,
|
|
120
|
-
iosSchemesList,
|
|
121
|
-
iosSimulatorBoot,
|
|
122
|
-
iosAppBuild,
|
|
123
|
-
iosAppInstall,
|
|
124
|
-
iosAppLaunch,
|
|
125
|
-
iosSimulatorScreenshot,
|
|
126
|
-
iosSimulatorLogTail,
|
|
127
|
-
iosUiSmokeTest,
|
|
128
123
|
buildReviewArtifactIndex,
|
|
129
124
|
ensureReviewArtifactRoots,
|
|
130
125
|
prepareBrowserReviewPacket,
|
|
131
126
|
prepareIosReviewPacket,
|
|
132
127
|
} from '../../safe-tooling';
|
|
133
128
|
import { buildModelClientSummary, buildModelControlPlaneSummary, deepSeekControllerManifest, deepSeekFunctionToolManifest, prepareDeepSeekControllerHandoff, prepareDeepSeekControllerRequest, prepareDeepSeekToolCall } from '../../model-clients';
|
|
134
|
-
import { buildAssistantReadinessReport } from '../../assistant/readiness';
|
|
135
|
-
import { approveAssistantActionProposal, getAssistantActionProposal, listAssistantActionProposals, rejectAssistantActionProposal } from '../../assistant/action-proposals';
|
|
136
|
-
import { assistantModelReadiness } from '../../assistant/model-provider';
|
|
137
|
-
import { createAssistantStandingGrant, listAssistantStandingGrants, revokeAssistantStandingGrant } from '../../assistant/standing-grants';
|
|
138
|
-
import { buildGmailTriagePlan, readGmailTriageRules, upsertGmailTriageRule } from '../../personal-assistant/gmail-triage-manager';
|
|
139
129
|
import { sessionCacheGlobalDiagnostics } from '../../../cli/repository/session-cache';
|
|
140
130
|
import { cachedGitIdentity, gitIdentityPerformanceSnapshot, gitSnapshot, gitSnapshotPerformanceSnapshot } from '../../../cli/repository/inspector';
|
|
141
131
|
import { buildWorkflowWatchdogReport } from '../../watchdog/workflow-watchdog';
|
|
@@ -170,7 +160,9 @@ import {
|
|
|
170
160
|
dismissHandoffItem,
|
|
171
161
|
getHandoffItem,
|
|
172
162
|
listCapabilityDescriptors,
|
|
163
|
+
getCapabilityDescriptor,
|
|
173
164
|
getPluginActionCapabilitySchema,
|
|
165
|
+
searchCapabilityDescriptors,
|
|
174
166
|
summarizeCapabilityGroups,
|
|
175
167
|
listHandoffItems,
|
|
176
168
|
normalizeCheckIds,
|
|
@@ -182,12 +174,18 @@ import {
|
|
|
182
174
|
listWorkContracts,
|
|
183
175
|
getWorkContract,
|
|
184
176
|
getWorkContractByRequestId,
|
|
177
|
+
updateWorkContract,
|
|
178
|
+
resumeRetainedCancelledWorkContract,
|
|
185
179
|
buildWorkContinuationSnapshot,
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
180
|
+
acceptPlanStepEvidence,
|
|
181
|
+
admitPlanContractAsync,
|
|
182
|
+
approvePlanContractAsync,
|
|
189
183
|
getPlanContract,
|
|
190
184
|
listPlanContracts,
|
|
185
|
+
resolvePlanAdmission,
|
|
186
|
+
withPrimaryWorkAdmissionLockAsync,
|
|
187
|
+
repairDanglingPlanStepWorkBinding,
|
|
188
|
+
completePlanStepForWork,
|
|
191
189
|
summarizePlanContract,
|
|
192
190
|
supersedePlanContract,
|
|
193
191
|
verifyGoalWorkloop,
|
|
@@ -195,764 +193,44 @@ import {
|
|
|
195
193
|
claimControllerSession,
|
|
196
194
|
getControllerSession,
|
|
197
195
|
releaseControllerSession,
|
|
196
|
+
releaseControllerSessionWithAuthority,
|
|
198
197
|
resumeControllerSession,
|
|
198
|
+
withControllerSessionTerminalizationFence,
|
|
199
|
+
type ControllerTerminalizationAuthority,
|
|
199
200
|
} from '../../control-plane/facade';
|
|
200
201
|
import { currentControllerInstanceId, readExecutionSession, startExecutionSession, updateExecutionSession } from '../../control-plane/execution/session-store';
|
|
202
|
+
import { changedPaths as workChangedPaths } from '../../control-plane/execution/work-task-receipt';
|
|
203
|
+
import { createRequirement, readRequirement } from '../../control-plane/persistence/requirement-store';
|
|
201
204
|
import { ensureManagedWorkspace } from '../../execution/managed-workspace';
|
|
202
205
|
import { currentPermissionSnapshotVersion } from '../../control-plane/execution/validation';
|
|
203
206
|
import { observeRuntimeStatus } from '../../root/status';
|
|
204
207
|
import { reconcileWorkValidation } from './work-validation-reconciler';
|
|
205
208
|
import { callExecutionTool } from './execution-tools';
|
|
206
209
|
import { launchSuperController } from '../../control-plane/launcher/thin-launcher';
|
|
210
|
+
import { runStandaloneChatgptPrompt, runWorkChatgptContinuation, settleWorkChatgptAutomationTab } from '../../control-plane/launcher/chatgpt-work-continuation';
|
|
211
|
+
import { getChatgptWorkConversationBinding } from '../../control-plane/launcher/chatgpt-work-binding-store';
|
|
207
212
|
import {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
goalStop,
|
|
220
|
-
goalTickOnce,
|
|
221
|
-
providerConfigStatusAction,
|
|
222
|
-
providerHealthAction,
|
|
223
|
-
providerListAction,
|
|
224
|
-
repairContinue,
|
|
225
|
-
repairPlan,
|
|
226
|
-
summarizeGoalContract,
|
|
227
|
-
tickActiveGoals,
|
|
228
|
-
type GoalContract,
|
|
229
|
-
type GoalLoopContext,
|
|
230
|
-
type GoalStatus,
|
|
231
|
-
type TaskIntent,
|
|
232
|
-
} from '../../control-plane/goal-loop';
|
|
233
|
-
|
|
234
|
-
function summarizeGoalPublic(goal: GoalContract) {
|
|
235
|
-
return summarizeGoalContract(goal);
|
|
236
|
-
}
|
|
213
|
+
acknowledgeControllerRoundClaim,
|
|
214
|
+
beginControllerRoundRelayAfterRelease,
|
|
215
|
+
beginInitialControllerRoundDispatch,
|
|
216
|
+
buildControllerRoundRelayPrompt,
|
|
217
|
+
finishControllerRoundRelayDispatch,
|
|
218
|
+
getControllerRoundRelay,
|
|
219
|
+
parseControllerDispositionCompatibilityCapability,
|
|
220
|
+
recordControllerRoundTabSettlement,
|
|
221
|
+
submitControllerRoundDisposition,
|
|
222
|
+
type ControllerRoundDisposition,
|
|
223
|
+
} from '../../control-plane/facade/controller-round-relay';
|
|
237
224
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
export const runtimeToolDefinitions: McpToolDefinition[] = [
|
|
248
|
-
definition('rh_status', 'Preferred ChatGPT facade: bounded controller status, capability readiness, and self-healing diagnose/repair.', {
|
|
249
|
-
repo_id: repoId,
|
|
250
|
-
operation: { type: 'string', enum: ['list', 'get', 'repair'], description: 'Defaults to get.' },
|
|
251
|
-
detail_level: { type: 'string', enum: ['summary', 'detail'], description: 'Defaults to summary.' },
|
|
252
|
-
repair_operation: { type: 'string', enum: ['diagnose', 'repair', 'verify', 'handoff'] },
|
|
253
|
-
dry_run: { type: 'boolean', description: 'Defaults to true for repair/diagnose.' },
|
|
254
|
-
approval_confirmed: { type: 'boolean' },
|
|
255
|
-
chatgpt_pull_failed: { type: 'boolean' },
|
|
256
|
-
destructive: { type: 'boolean' },
|
|
257
|
-
}),
|
|
258
|
-
definition('rh_inbox', 'Preferred ChatGPT facade: pending handoff decisions only (not logs).', {
|
|
259
|
-
repo_id: repoId,
|
|
260
|
-
operation: { type: 'string', enum: ['list', 'get', 'ack', 'accept', 'resolve', 'dismiss', 'create'], description: 'Defaults to list (pending).' },
|
|
261
|
-
handoff_id: { type: 'string' },
|
|
262
|
-
work_id: { type: 'string' },
|
|
263
|
-
title: { type: 'string' },
|
|
264
|
-
reason: { type: 'string' },
|
|
265
|
-
summary: { type: 'string' },
|
|
266
|
-
recommended_decision: { type: 'string' },
|
|
267
|
-
recommended_prompt: { type: 'string' },
|
|
268
|
-
decision: { type: 'string', description: 'Recorded on resolve/dismiss.' },
|
|
269
|
-
resolver: { type: 'string', description: 'Who resolved or dismissed the handoff.' },
|
|
270
|
-
detail_level: { type: 'string', enum: ['summary', 'detail'] },
|
|
271
|
-
limit: { type: 'number' },
|
|
272
|
-
}),
|
|
273
|
-
definition('rh_context', 'Preferred ChatGPT facade: bounded repository context and the default code retrieval router, with optional CodeGraph structural augmentation.', {
|
|
274
|
-
repo_id: repoId,
|
|
275
|
-
checkout_id: { type: 'string', description: 'Optional checkout identity for repositories with multiple worktrees.' },
|
|
276
|
-
operation: { type: 'string', enum: ['list', 'get', 'search'], description: 'Defaults to get. Use search as the default code-location path when an exact file is unknown.' },
|
|
277
|
-
query: { type: 'string', description: 'Code/content intent for operation=search.' },
|
|
278
|
-
known_paths: { type: 'array', items: { type: 'string' }, description: 'Optional exact paths or globs that should receive highest retrieval priority.' },
|
|
279
|
-
include_globs: { type: 'array', items: { type: 'string' } },
|
|
280
|
-
exclude_globs: { type: 'array', items: { type: 'string' } },
|
|
281
|
-
retrieval_mode: { type: 'string', enum: ['implementation', 'plan', 'debug', 'review'], description: 'Defaults to implementation. Implementation returns current raw impact evidence for ChatGPT to judge; plan/debug/review intentionally widen evidence.' },
|
|
282
|
-
impact_domains: { type: 'array', items: { type: 'string', enum: [...CONTROLLER_CONTEXT_IMPACT_DOMAINS] }, description: 'Optional GPT-selected cross-cutting evidence dimensions. Forge expands these mechanically in the same retrieval call; it never treats them as proof of semantic completeness.' },
|
|
283
|
-
structural_context: { type: 'string', enum: ['off', 'auto', 'required'], description: 'Optional override. Defaults to auto for implementation/review and required for plan/debug.' },
|
|
284
|
-
max_files: { type: 'number' },
|
|
285
|
-
max_snippets: { type: 'number' },
|
|
286
|
-
requested_check_ids: { type: 'array', items: { type: 'string' } },
|
|
287
|
-
capability_id: { type: 'string', description: 'Optional capability identity. For plugin.<pluginId>.<actionId>, returns the exact typed action schema/policy for plugin_action_execute.' },
|
|
288
|
-
work_id: { type: 'string' },
|
|
289
|
-
detail_level: { type: 'string', enum: ['summary', 'detail', 'raw'], description: 'Defaults to summary; raw is still bounded.' },
|
|
290
|
-
}),
|
|
291
|
-
definition('rh_work', 'Preferred ChatGPT facade: bounded planning, direct control, controller ownership, and external SuperController launch.', {
|
|
292
|
-
repo_id: repoId,
|
|
293
|
-
operation: { type: 'string', enum: ['start', 'continue', 'verify', 'repair', 'finalize', 'stop', 'delegate', 'controller_claim', 'controller_release', 'controller_get_owner', 'launcher_start', 'plan_create', 'plan_get', 'plan_list', 'plan_approve', 'plan_supersede', 'schedule_create', 'schedule_list', 'schedule_get', 'schedule_pause', 'schedule_resume', 'schedule_trigger'], description: 'Defaults to start. Complex starts require plan_id and plan_step_id. schedule_* manages generic recurring ChatGPT workflow schedules plus Work-bound continuation/browser subtypes.' },
|
|
294
|
-
objective: { type: 'string' },
|
|
295
|
-
work_id: { type: 'string' },
|
|
296
|
-
related_work_id: { type: 'string', description: 'Optional explicit existing Work ownership target for continue/extend semantics. Unrelated active Work is never inferred as a semantic target.' },
|
|
297
|
-
work_relation: { type: 'string', enum: ['continue', 'extend', 'parallel', 'new_goal'], description: 'Optional semantic relation to an explicitly related Work. This field never selects Direct/Process/Durable execution depth; checkout conflicts are resolved by placement.' },
|
|
298
|
-
related_plan_id: { type: 'string', description: 'Existing active Plan selected when resolving another slice under the same Requirement.' },
|
|
299
|
-
plan_relation: { type: 'string', enum: ['extend', 'parallel'], description: 'Relationship to another active Plan under the same Requirement. Omit to receive candidates first. parallel explicitly permits a distinct Plan slice; extend reuses the existing Plan authority and requires replanning/supersession instead of creating another draft.' },
|
|
300
|
-
requested_by: { type: 'string', enum: ['chatgpt', 'user', 'system', 'scheduler'], description: 'Origin of the request. Scheduler-origin starts are continuation-only and cannot create new durable Work.' },
|
|
301
|
-
requirement_id: { type: 'string', description: 'Stable Requirement authority used to find related active Work before creation.' },
|
|
302
|
-
handoff_id: { type: 'string', description: 'Optional existing HandoffItem used to resume controller context.' },
|
|
303
|
-
controller_id: { type: 'string', description: 'Stable external SuperController identity for controller_claim/release.' },
|
|
304
|
-
controller_type: { type: 'string', enum: ['chatgpt', 'codex', 'grok', 'claude', 'human'], description: 'Controller kind for claims and launcher_start.' },
|
|
305
|
-
session_id: { type: 'string', description: 'Controller-owned session identity used for an exclusive Work lease.' },
|
|
306
|
-
lease_ms: { type: 'number', description: 'Authenticated Controller ownership lease duration, bounded to one hour. Legacy launcher callers may still use this as a launch-reservation TTL.' },
|
|
307
|
-
launch_reservation_ms: { type: 'number', description: 'Short duplicate-spawn reservation TTL for launcher_start or schedule_create; does not grant Work ownership.' },
|
|
308
|
-
executable: { type: 'string', description: 'External controller executable for launcher_start. This is owned by the Launcher, not the Kernel.' },
|
|
309
|
-
launch_args: { type: 'array', items: { type: 'string' }, description: 'Provider-specific arguments for launcher_start.' },
|
|
310
|
-
browser_session_id: { type: 'string', description: 'Saved Forge ChatGPT browser session to continue when controller_type=chatgpt.' },
|
|
311
|
-
conversation_url: { type: 'string', description: 'Explicit https://chatgpt.com conversation URL used when no saved Forge browser session exists.' },
|
|
312
|
-
continuation_prompt: { type: 'string', description: 'Bounded Work continuation instruction, or the recurring ChatGPT prompt when schedule_mode=workflow and objective is omitted.' },
|
|
313
|
-
schedule_id: { type: 'string', description: 'Schedule identity for schedule_get/pause/resume/trigger.' },
|
|
314
|
-
schedule_name: { type: 'string', description: 'Human-readable schedule name; generic workflow schedules use the objective/prompt when omitted.' },
|
|
315
|
-
schedule_request_id: { type: 'string', description: 'Stable idempotency key for schedule_create.' },
|
|
316
|
-
schedule_mode: { type: 'string', enum: ['workflow', 'continuation', 'browser_watch', 'browser_keepalive'], description: 'Defaults to workflow when work_id is omitted and continuation when work_id is present. workflow dispatches one bounded ChatGPT prompt per finite Occurrence without requiring Work; continuation/browser modes remain Work-bound.' },
|
|
317
|
-
probe_url: { type: 'string', description: 'Allowed URL to navigate or refresh for browser_watch/browser_keepalive.' },
|
|
318
|
-
probe_browser_session_id: { type: 'string', description: 'Existing Forge browser session used as the external observation target.' },
|
|
319
|
-
probe_selector: { type: 'string', description: 'Optional selector limiting text extraction for browser_watch.' },
|
|
320
|
-
probe_max_chars: { type: 'number', description: 'Bounded extracted text size for browser_watch.' },
|
|
321
|
-
probe_timeout_ms: { type: 'number', description: 'Bounded browser probe timeout.' },
|
|
322
|
-
include_terms: { type: 'array', items: { type: 'string' }, description: 'Optional case-insensitive terms; only matching normalized lines contribute to the observation fingerprint.' },
|
|
323
|
-
ignore_patterns: { type: 'array', items: { type: 'string' }, description: 'Optional regex patterns stripped before fingerprinting volatile text such as relative timestamps.' },
|
|
324
|
-
login_url_terms: { type: 'array', items: { type: 'string' }, description: 'Configured URL markers that classify the browser observation as authentication-required.' },
|
|
325
|
-
login_text_terms: { type: 'array', items: { type: 'string' }, description: 'Configured page-text markers that classify the browser observation as authentication-required.' },
|
|
326
|
-
wake_on_first_observation: { type: 'boolean', description: 'Defaults false so the first browser_watch occurrence records a silent baseline.' },
|
|
327
|
-
wake_on_auth_required: { type: 'boolean', description: 'Defaults true for Work-bound browser schedules.' },
|
|
328
|
-
auth_required_prompt: { type: 'string', description: 'Optional ChatGPT continuation instruction when the probe detects a login marker.' },
|
|
329
|
-
trigger_type: { type: 'string', enum: ['interval', 'cron', 'calendar', 'condition', 'repository-event', 'dependency-checkpoint', 'manual'] },
|
|
330
|
-
every_minutes: { type: 'number', description: 'Interval trigger cadence in minutes.' },
|
|
331
|
-
cron_expression: { type: 'string' },
|
|
332
|
-
schedule_timezone: { type: 'string' },
|
|
333
|
-
catch_up_minutes: { type: 'number' },
|
|
334
|
-
calendar_at: { type: 'string' },
|
|
335
|
-
condition: { type: 'object' },
|
|
336
|
-
event_name: { type: 'string' },
|
|
337
|
-
event_id: { type: 'string' },
|
|
338
|
-
event_data: { type: 'object' },
|
|
339
|
-
dependency_job_ids: { type: 'array', items: { type: 'string' } },
|
|
340
|
-
max_failures: { type: 'number' },
|
|
341
|
-
cooldown_minutes: { type: 'number' },
|
|
342
|
-
daily_budget_minutes: { type: 'number' },
|
|
343
|
-
shadow_mode: { type: 'boolean', description: 'Defaults to true. Set false only after the continuation path has been validated.' },
|
|
344
|
-
backoff_base_minutes: { type: 'number' },
|
|
345
|
-
backoff_max_minutes: { type: 'number' },
|
|
346
|
-
include_occurrences: { type: 'boolean' },
|
|
347
|
-
plan_id: { type: 'string' },
|
|
348
|
-
plan_step_id: { type: 'string', description: 'Approved PlanContract step to bind to a complex Goal Workloop start.' },
|
|
349
|
-
scope_key: { type: 'string', description: 'Stable normalized scope identity used to prevent overlapping active plans.' },
|
|
350
|
-
source_revision: { type: 'string', description: 'Repository revision observed during read-only planning preflight.' },
|
|
351
|
-
plan_steps: { type: 'array', items: { type: 'object' }, description: 'Bounded plan steps with id, objective, dependencies, authoritative_files, allowed_paths, forbidden_paths, check_ids, and acceptance_criteria.' },
|
|
352
|
-
non_goals: { type: 'array', items: { type: 'string' } },
|
|
353
|
-
assumptions: { type: 'array', items: { type: 'string' } },
|
|
354
|
-
resolved_decisions: { type: 'array', items: { type: 'string' } },
|
|
355
|
-
stop_conditions: { type: 'array', items: { type: 'string' } },
|
|
356
|
-
replan_conditions: { type: 'array', items: { type: 'string' } },
|
|
357
|
-
integration_strategy: { type: 'string' },
|
|
358
|
-
superseded_by: { type: 'string' },
|
|
359
|
-
expected_files: { type: 'number' },
|
|
360
|
-
expected_changed_lines: { type: 'number' },
|
|
361
|
-
scope_clear: { type: 'boolean' },
|
|
362
|
-
requires_investigation: { type: 'boolean' },
|
|
363
|
-
requires_long_running_checks: { type: 'boolean' },
|
|
364
|
-
requires_parallelism: { type: 'boolean' },
|
|
365
|
-
needs_dependencies: { type: 'boolean' },
|
|
366
|
-
requires_recovery: { type: 'boolean' },
|
|
367
|
-
requires_worker: { type: 'boolean', description: 'Opt-in only. Set true only when the user explicitly requests Codex/Claude or another agent worker; never infer it from task complexity.' },
|
|
368
|
-
requires_external_effect: { type: 'boolean' },
|
|
369
|
-
requires_approval: { type: 'boolean' },
|
|
370
|
-
requires_user_approval: { type: 'boolean' },
|
|
371
|
-
destructive: { type: 'boolean' },
|
|
372
|
-
remote_write: { type: 'boolean' },
|
|
373
|
-
secret_access: { type: 'boolean' },
|
|
374
|
-
capability_id: { type: 'string' },
|
|
375
|
-
approval_confirmed: { type: 'boolean' },
|
|
376
|
-
dry_run: { type: 'boolean', description: 'Defaults to true for repair.' },
|
|
377
|
-
check_ids: { type: 'array', items: { type: 'string' } },
|
|
378
|
-
check_id: { type: 'string' },
|
|
379
|
-
acceptance_criteria: { type: 'array', items: { type: 'string' } },
|
|
380
|
-
allowed_paths: { type: 'array', items: { type: 'string' } },
|
|
381
|
-
forbidden_paths: { type: 'array', items: { type: 'string' } },
|
|
382
|
-
constraints: { type: 'object' },
|
|
383
|
-
repair_operation: { type: 'string', enum: ['diagnose', 'repair', 'verify', 'handoff'] },
|
|
384
|
-
target: { type: 'string', enum: ['codex', 'grok', 'claude'], description: 'Explicit small-brain delegate target for operation=delegate. Delegation is never the default execution path.' },
|
|
385
|
-
available: { type: 'boolean', description: 'Whether the delegate target is currently available.' },
|
|
386
|
-
codex_available: { type: 'boolean' },
|
|
387
|
-
simulate_check: { type: 'boolean', description: 'Test hook: skip real check execution and use infrastructure_failed/check_failed flags.' },
|
|
388
|
-
infrastructure_failed: { type: 'boolean' },
|
|
389
|
-
check_failed: { type: 'boolean' },
|
|
390
|
-
authorize_destructive_cleanup: { type: 'boolean' },
|
|
391
|
-
commit: { type: 'boolean', description: 'Finalize override. Defaults to true only when the Work owns uncommitted changes.' },
|
|
392
|
-
merge: { type: 'boolean', description: 'Finalize override. Defaults to true for managed Work delivery.' },
|
|
393
|
-
cleanup: { type: 'boolean', description: 'Finalize/stop override. Defaults to true so managed worktrees close automatically.' },
|
|
394
|
-
target_branch: { type: 'string', description: 'Delivery branch; defaults to the repository default branch.' },
|
|
395
|
-
delete_branch: { type: 'boolean', description: 'Defaults to true after target-branch containment is proven.' },
|
|
396
|
-
no_ff: { type: 'boolean' },
|
|
397
|
-
completion_outcome: { type: 'string', enum: ['completed_changed', 'completed_no_change'] },
|
|
398
|
-
no_change_evidence: { type: 'string', description: 'Optional explicit proof for a no-change completion; otherwise the lifecycle coordinator derives proof only for an unchanged validated HEAD.' },
|
|
399
|
-
reconcile_historical_delivery: { type: 'boolean', description: 'Explicitly request reviewed reconciliation for a historically delivered Direct Edit Work whose original Work binding/receipt is missing.' },
|
|
400
|
-
reconcile_target_revision: { type: 'string', description: 'Exact already-integrated commit accepted by the reviewer.' },
|
|
401
|
-
reconcile_compared_paths: { type: 'array', items: { type: 'string' }, description: 'Complete exact path set changed from the Work base revision to reconcile_target_revision.' },
|
|
402
|
-
reconcile_rationale: { type: 'string', description: 'Bounded reviewer rationale for accepting the historical delivery.' },
|
|
403
|
-
reconcile_cleanup_proof: { type: 'string', description: 'Concrete proof that no owned cleanup remains or that cleanup is complete.' },
|
|
404
|
-
detail_level: { type: 'string', enum: ['summary', 'detail'] },
|
|
405
|
-
reason: { type: 'string', description: 'Bounded reason for a work stop or repair record.' },
|
|
406
|
-
}, [], false),
|
|
407
|
-
definition('work_submit', 'Submit one durable repository operation and return a resumable Work handle.', {
|
|
408
|
-
repo_id: repoId,
|
|
409
|
-
request_id: { type: 'string', description: 'Stable idempotency key used to resume the same execution-child Work after reconnecting.' },
|
|
410
|
-
parent_work_id: { type: 'string', description: 'Optional active primary Work that owns this low-level execution child. work_submit never creates a second business-workflow authority.' },
|
|
411
|
-
operation: { type: 'string', description: 'Existing durable controller operation, for example run_check, dispatch_task, or create_issue.' },
|
|
412
|
-
arguments: { type: 'object', description: 'Arguments passed to the durable operation.' },
|
|
413
|
-
timeout_ms: { type: 'number' },
|
|
414
|
-
}, ['request_id', 'operation'], false),
|
|
415
|
-
definition('work_get', 'Resume one Work by work_id or request_id. Repository scope is always enforced.', {
|
|
416
|
-
repo_id: repoId,
|
|
417
|
-
work_id: { type: 'string' },
|
|
418
|
-
request_id: { type: 'string' },
|
|
419
|
-
include_events: { type: 'boolean' },
|
|
420
|
-
wait: { type: 'boolean', description: 'When true, wait for terminal status before returning digest.' },
|
|
421
|
-
wait_ms: { type: 'number' },
|
|
422
|
-
}),
|
|
423
|
-
definition('work_list', 'List recent resumable Work for one repository.', {
|
|
424
|
-
repo_id: repoId,
|
|
425
|
-
limit: { type: 'number' },
|
|
426
|
-
}),
|
|
427
|
-
definition('work_cancel', 'Cancel one queued or running Work by work_id or request_id.', {
|
|
428
|
-
repo_id: repoId,
|
|
429
|
-
work_id: { type: 'string' },
|
|
430
|
-
request_id: { type: 'string' },
|
|
431
|
-
reason: { type: 'string' },
|
|
432
|
-
}, [], false),
|
|
433
|
-
definition('work_wait', 'Wait for one Work/ExecutionJob/managed process to reach a terminal status and return a bounded operation digest.', {
|
|
434
|
-
repo_id: repoId,
|
|
435
|
-
work_id: { type: 'string' },
|
|
436
|
-
request_id: { type: 'string' },
|
|
437
|
-
wait_ms: { type: 'number', description: 'Max wait in ms. Default 15000, max 120000.' },
|
|
438
|
-
}),
|
|
439
|
-
definition('git_diff_paths', 'Return a bounded Git diff and status for explicit repository-relative paths only.', {
|
|
440
|
-
repo_id: repoId,
|
|
441
|
-
paths: { type: 'array', items: { type: 'string' } },
|
|
442
|
-
staged: { type: 'boolean' },
|
|
443
|
-
max_bytes: { type: 'number' },
|
|
444
|
-
}, ['paths']),
|
|
445
|
-
definition('git_stage_paths', 'Stage explicit repository-relative paths only using git add --all -- <paths>.', {
|
|
446
|
-
repo_id: repoId,
|
|
447
|
-
paths: { type: 'array', items: { type: 'string' } },
|
|
448
|
-
}, ['paths'], false),
|
|
449
|
-
definition('git_commit_paths', 'Stage and commit explicit repository-relative paths only, leaving unrelated staged changes untouched.', {
|
|
450
|
-
repo_id: repoId,
|
|
451
|
-
paths: { type: 'array', items: { type: 'string' } },
|
|
452
|
-
message: { type: 'string' },
|
|
453
|
-
}, ['paths', 'message'], false),
|
|
454
|
-
definition('prepare_transfer_artifacts', 'Refresh rebuildable session continuation plus a selected-path patch transfer artifact when recovery context is needed.', {
|
|
455
|
-
repo_id: repoId,
|
|
456
|
-
reason: { type: 'string' },
|
|
457
|
-
}, [], false),
|
|
458
|
-
definition('get_job', 'Read one durable Execution Job. Summary is the default; opt in to full state only when needed.', {
|
|
459
|
-
job_id: { type: 'string' },
|
|
460
|
-
repo_id: repoId,
|
|
461
|
-
include_events: { type: 'boolean' },
|
|
462
|
-
detail_level: { type: 'string', enum: ['summary', 'full'] },
|
|
463
|
-
wait: { type: 'boolean', description: 'When true, wait for terminal status before returning digest.' },
|
|
464
|
-
wait_ms: { type: 'number' },
|
|
465
|
-
}, ['job_id']),
|
|
466
|
-
definition('get_artifact', 'Read one bounded Evidence Plane artifact by id. Large content remains bounded.', { artifact_id: { type: 'string' }, repo_id: repoId, max_bytes: { type: 'number' } }, ['artifact_id', 'repo_id']),
|
|
467
|
-
definition('repository_change_verify', 'Composite: verify checkout/SHA, apply bounded patch, run checks, return first failure inline without get_job/get_artifact follow-ups.', {
|
|
468
|
-
repo_id: repoId,
|
|
469
|
-
expected_branch: { type: 'string' },
|
|
470
|
-
expected_head: { type: 'string' },
|
|
471
|
-
expected_file_shas: { type: 'object', additionalProperties: { type: 'string' } },
|
|
472
|
-
patch: { type: 'string', description: 'Unified diff applied with git apply.' },
|
|
473
|
-
allowed_paths: { type: 'array', items: { type: 'string' } },
|
|
474
|
-
checks: { type: 'array', items: { type: 'string' } },
|
|
475
|
-
check_timeout_ms: { type: 'number' },
|
|
476
|
-
}, [], false),
|
|
477
|
-
definition('list_jobs', 'List durable Execution Jobs for one repository. Summary is the default.', {
|
|
478
|
-
repo_id: repoId,
|
|
479
|
-
limit: { type: 'number' },
|
|
480
|
-
detail_level: { type: 'string', enum: ['summary', 'full'] },
|
|
481
|
-
}),
|
|
482
|
-
definition('cancel_job', 'Cancel one queued or running durable Execution Job.', { job_id: { type: 'string' }, repo_id: repoId, reason: { type: 'string' } }, ['job_id'], false),
|
|
483
|
-
definition('controller_ready', 'Return one binary Runtime readiness decision with bounded diagnostic evidence.', {
|
|
484
|
-
repo_id: repoId,
|
|
485
|
-
}),
|
|
486
|
-
definition('repository_runtime_snapshot', 'Read the materialized runtime projection without scanning historical state.', { repo_id: repoId }),
|
|
487
|
-
definition('schedule_dedupe_report', 'Read-only report that groups duplicate schedules by semantic trigger/action identity.', {
|
|
488
|
-
repo_id: repoId,
|
|
489
|
-
}, ['repo_id']),
|
|
490
|
-
definition('schedule_dedupe_apply', 'Pause duplicate schedules after explicit authorization, keeping one canonical enabled schedule per semantic identity.', {
|
|
491
|
-
repo_id: repoId,
|
|
492
|
-
dry_run: { type: 'boolean', description: 'When true, only returns the proposed changes.' },
|
|
493
|
-
confirm_authorization: { type: 'boolean', description: 'Must be true unless dry_run is true.' },
|
|
494
|
-
}, ['repo_id']),
|
|
495
|
-
definition('runtime_performance_diagnostics', 'Diagnose forge host performance, orphan workers, Local Controller presence, and cleanup candidates.', {
|
|
496
|
-
repo_id: repoId,
|
|
497
|
-
include_processes: { type: 'boolean', description: 'Include bounded forge related process samples. Defaults to true.' },
|
|
498
|
-
include_temp_dirs: { type: 'boolean', description: 'Include bounded forge temporary directory scan. Defaults to true.' },
|
|
499
|
-
cleanup_preview: { type: 'boolean', description: 'Return a no-side-effect cleanup plan for orphan workers and stale temp entries.' },
|
|
500
|
-
}),
|
|
501
|
-
definition('capability_recovery_probe', 'Read-only Runtime diagnostic evidence. This tool never starts, stops, restarts, repairs, or replaces Runtime modules.', {
|
|
502
|
-
repo_id: repoId,
|
|
503
|
-
recent_errors: { type: 'array', items: { type: 'string' } },
|
|
504
|
-
command_preview_available: { type: 'boolean' },
|
|
505
|
-
command_execute_available: { type: 'boolean' },
|
|
506
|
-
issue_tools_available: { type: 'boolean' },
|
|
507
|
-
job_tools_available: { type: 'boolean' },
|
|
508
|
-
}),
|
|
509
|
-
definition('capability_recovery_plan', 'Return a diagnostic handoff plan without Runtime lifecycle actions or automatic source repair.', {
|
|
510
|
-
repo_id: repoId,
|
|
511
|
-
recent_errors: { type: 'array', items: { type: 'string' } },
|
|
512
|
-
}),
|
|
513
|
-
definition('capability_recovery_apply', 'Apply one currently supported bounded recovery action after explicit authorization. Runtime lifecycle actions are outside this surface.', {
|
|
514
|
-
repo_id: repoId,
|
|
515
|
-
action_id: { type: 'string' },
|
|
516
|
-
confirm_authorization: { type: 'boolean' },
|
|
517
|
-
authorization: { type: 'string', description: 'Must equal action_id for actions that require authorization.' },
|
|
518
|
-
reason: { type: 'string' },
|
|
519
|
-
min_age_minutes: { type: 'number' },
|
|
520
|
-
max_candidates: { type: 'number' },
|
|
521
|
-
}, ['action_id'], false),
|
|
522
|
-
definition('runtime_maintenance_status', 'Read the self-contained runtime maintenance plan without using repository_command_execute or Local Job tickets.', {
|
|
523
|
-
repo_id: repoId,
|
|
524
|
-
min_age_minutes: { type: 'number' },
|
|
525
|
-
max_candidates: { type: 'number' },
|
|
526
|
-
cancel_pending_approvals: { type: 'boolean' },
|
|
527
|
-
}),
|
|
528
|
-
definition('runtime_maintenance_apply', 'Apply one bounded runtime maintenance action without repository_command_execute or Local Job tickets.', {
|
|
529
|
-
repo_id: repoId,
|
|
530
|
-
action_id: { type: 'string', enum: ['local_jobs_reconcile', 'quarantine_unreadable_local_jobs', 'runtime_storage_finalize_relocation', 'rebuild_projection', 'full_maintenance_pass'] },
|
|
531
|
-
confirm_maintenance: { type: 'boolean' },
|
|
532
|
-
authorization: { type: 'string', description: 'Must equal action_id.' },
|
|
533
|
-
min_age_minutes: { type: 'number' },
|
|
534
|
-
max_candidates: { type: 'number' },
|
|
535
|
-
cancel_pending_approvals: { type: 'boolean' },
|
|
536
|
-
}, ['action_id', 'confirm_maintenance', 'authorization'], false),
|
|
537
|
-
definition('goal_create', 'Deprecated historical GoalContract creation. Use rh_work PlanContract/WorkContract instead.', {
|
|
538
|
-
repo_id: repoId,
|
|
539
|
-
title: { type: 'string' },
|
|
540
|
-
objective: { type: 'string' },
|
|
541
|
-
mode: { type: 'string', enum: ['manual', 'supervised', 'autonomous'] },
|
|
542
|
-
issue_id: { type: 'string' },
|
|
543
|
-
task_ids: { type: 'array', items: { type: 'string' } },
|
|
544
|
-
acceptance_criteria: { type: 'array', items: { type: 'string' } },
|
|
545
|
-
check_ids: { type: 'array', items: { type: 'string' } },
|
|
546
|
-
allowed_executors: { type: 'array', items: { type: 'string' } },
|
|
547
|
-
forbidden_executors: { type: 'array', items: { type: 'string' } },
|
|
548
|
-
retry_budget: { type: 'number' },
|
|
549
|
-
}, ['title', 'objective'], false),
|
|
550
|
-
definition('goal_list', 'List GoalContracts for a repository.', {
|
|
551
|
-
repo_id: repoId,
|
|
552
|
-
status: { type: 'string', enum: ['active', 'all', 'created', 'planning', 'ready', 'dispatching', 'running', 'verifying', 'repairing', 'waiting_for_user', 'handoff_ready', 'finalized', 'failed', 'stopped'] },
|
|
553
|
-
limit: { type: 'number' },
|
|
554
|
-
}),
|
|
555
|
-
definition('goal_get', 'Get one GoalContract by id.', {
|
|
556
|
-
repo_id: repoId,
|
|
557
|
-
goal_id: { type: 'string' },
|
|
558
|
-
}, ['goal_id']),
|
|
559
|
-
definition('goal_start', 'Deprecated historical GoalContract execution. Use controller_claim and launcher_start.', {
|
|
560
|
-
repo_id: repoId,
|
|
561
|
-
goal_id: { type: 'string' },
|
|
562
|
-
}, ['goal_id'], false),
|
|
563
|
-
definition('goal_continue', 'Deprecated historical GoalContract execution. Use WorkContract and HandoffItem.', {
|
|
564
|
-
repo_id: repoId,
|
|
565
|
-
goal_id: { type: 'string' },
|
|
566
|
-
}, ['goal_id'], false),
|
|
567
|
-
definition('goal_stop', 'Stop a GoalContract.', {
|
|
568
|
-
repo_id: repoId,
|
|
569
|
-
goal_id: { type: 'string' },
|
|
570
|
-
reason: { type: 'string' },
|
|
571
|
-
}, ['goal_id'], false),
|
|
572
|
-
definition('goal_finalize', 'Finalize a GoalContract when verification evidence exists.', {
|
|
573
|
-
repo_id: repoId,
|
|
574
|
-
goal_id: { type: 'string' },
|
|
575
|
-
force: { type: 'boolean' },
|
|
576
|
-
}, ['goal_id'], false),
|
|
577
|
-
definition('goal_status', 'User-facing goal loop status (active goals, stage, provider health, handoff).', {
|
|
578
|
-
repo_id: repoId,
|
|
579
|
-
goal_id: { type: 'string' },
|
|
580
|
-
}),
|
|
581
|
-
definition('goal_tick_once', 'Deprecated. Kernel no longer ticks or dispatches Goal providers.', {
|
|
582
|
-
repo_id: repoId,
|
|
583
|
-
goal_id: { type: 'string' },
|
|
584
|
-
task_intent: { type: 'string', enum: ['deterministic_edit', 'code_implementation', 'code_repair', 'architecture_planning', 'ios_build_or_sim', 'browser_automation', 'verification_repair', 'review', 'unknown'] },
|
|
585
|
-
verification_ok: { type: 'boolean' },
|
|
586
|
-
verification_check_id: { type: 'string' },
|
|
587
|
-
provider_failure: { type: 'boolean' },
|
|
588
|
-
external_write: { type: 'boolean' },
|
|
589
|
-
approval_confirmed: { type: 'boolean' },
|
|
590
|
-
}, [], false),
|
|
591
|
-
definition('goal_handoff_packet_create', 'Create a ChatGPT/supervisor continuation packet (handoff-only path).', {
|
|
592
|
-
repo_id: repoId,
|
|
593
|
-
goal_id: { type: 'string' },
|
|
594
|
-
blockers: { type: 'array', items: { type: 'string' } },
|
|
595
|
-
required_user_decision: { type: 'string' },
|
|
596
|
-
recommended_provider: { type: 'string' },
|
|
597
|
-
}, ['goal_id'], false),
|
|
598
|
-
definition('goal_handoff_packet_get', 'Read a durable goal handoff/continuation packet.', {
|
|
599
|
-
repo_id: repoId,
|
|
600
|
-
packet_id: { type: 'string' },
|
|
601
|
-
}, ['packet_id']),
|
|
602
|
-
definition('provider_list', 'List model/code executor providers (invokable vs handoff-only).', {
|
|
603
|
-
repo_id: repoId,
|
|
604
|
-
}),
|
|
605
|
-
definition('provider_health', 'Bounded redacted provider health (never returns tokens).', {
|
|
606
|
-
repo_id: repoId,
|
|
607
|
-
provider_id: { type: 'string' },
|
|
608
|
-
}),
|
|
609
|
-
definition('provider_config_status', 'Summarize invokable vs missing_auth vs handoff-only providers.', {
|
|
610
|
-
repo_id: repoId,
|
|
611
|
-
}),
|
|
612
|
-
definition('executor_route_preview', 'Preview which provider the ExecutorRouter would select.', {
|
|
613
|
-
repo_id: repoId,
|
|
614
|
-
goal_id: { type: 'string' },
|
|
615
|
-
task_intent: { type: 'string', enum: ['deterministic_edit', 'code_implementation', 'code_repair', 'architecture_planning', 'ios_build_or_sim', 'browser_automation', 'verification_repair', 'review', 'unknown'] },
|
|
616
|
-
risk: { type: 'string', enum: ['readonly', 'local_repo_write', 'workspace_write', 'remote_write', 'destructive', 'raw_secret_config'] },
|
|
617
|
-
objective: { type: 'string' },
|
|
618
|
-
}),
|
|
619
|
-
definition('executor_dispatch', 'Policy-gated dispatch to an invokable provider; forge applies/verifies patches (no raw shell exposure).', {
|
|
620
|
-
repo_id: repoId,
|
|
621
|
-
goal_id: { type: 'string' },
|
|
622
|
-
provider_id: { type: 'string' },
|
|
623
|
-
task_intent: { type: 'string', enum: ['deterministic_edit', 'code_implementation', 'code_repair', 'architecture_planning', 'ios_build_or_sim', 'browser_automation', 'verification_repair', 'review', 'unknown'] },
|
|
624
|
-
risk: { type: 'string', enum: ['readonly', 'local_repo_write', 'workspace_write', 'remote_write', 'destructive', 'raw_secret_config'] },
|
|
625
|
-
approval_confirmed: { type: 'boolean' },
|
|
626
|
-
external_write: { type: 'boolean' },
|
|
627
|
-
strong_confirmation_text: { type: 'string' },
|
|
628
|
-
}, ['goal_id'], false),
|
|
629
|
-
definition('repair_plan', 'Classify goal failure and recommend repair provider/status.', {
|
|
630
|
-
repo_id: repoId,
|
|
631
|
-
goal_id: { type: 'string' },
|
|
632
|
-
}, ['goal_id']),
|
|
633
|
-
definition('repair_continue', 'Continue the self-healing repair loop for a goal (one bounded transition).', {
|
|
634
|
-
repo_id: repoId,
|
|
635
|
-
goal_id: { type: 'string' },
|
|
636
|
-
force_failure_class: { type: 'string' },
|
|
637
|
-
}, ['goal_id'], false),
|
|
638
|
-
definition('workspace_auth_status', 'Summarize Workspace/Gmail auth readiness without returning or persisting secrets.', {
|
|
639
|
-
repo_id: repoId,
|
|
640
|
-
}),
|
|
641
|
-
definition('workspace_auth_login_prepare', 'Prepare a state-protected local Google Workspace/Gmail OAuth login with PKCE and Keychain persistence.', {
|
|
642
|
-
repo_id: repoId,
|
|
643
|
-
service: { type: 'string', enum: ['gmail', 'calendar', 'tasks', 'google-workspace'] },
|
|
644
|
-
scopes: { type: 'array', items: { type: 'string' } },
|
|
645
|
-
redirect_uri: { type: 'string' },
|
|
646
|
-
}),
|
|
647
|
-
definition('assistant_model_readiness', 'Read the optional bounded Assistant model provider configuration without returning secrets.', {
|
|
648
|
-
repo_id: repoId,
|
|
649
|
-
}),
|
|
650
|
-
definition('assistant_standing_grants', 'List scoped, expiring Assistant Standing Grants.', {
|
|
651
|
-
repo_id: repoId,
|
|
652
|
-
status: { type: 'string', enum: ['active', 'revoked', 'expired'] },
|
|
653
|
-
limit: { type: 'number' },
|
|
654
|
-
}),
|
|
655
|
-
definition('assistant_standing_grant_create', 'Create an explicitly authorized Standing Grant for a hardcoded low-risk action.', {
|
|
656
|
-
repo_id: repoId,
|
|
657
|
-
name: { type: 'string' },
|
|
658
|
-
plugin_id: { type: 'string' },
|
|
659
|
-
action_id: { type: 'string' },
|
|
660
|
-
routine_ids: { type: 'array', items: { type: 'string' } },
|
|
661
|
-
sender_allowlist: { type: 'array', items: { type: 'string' } },
|
|
662
|
-
subject_contains: { type: 'array', items: { type: 'string' } },
|
|
663
|
-
min_confidence: { type: 'number' },
|
|
664
|
-
max_per_run: { type: 'number' },
|
|
665
|
-
expires_in_days: { type: 'number' },
|
|
666
|
-
confirm_authorization: { type: 'boolean' },
|
|
667
|
-
}, ['plugin_id', 'action_id', 'confirm_authorization'], false),
|
|
668
|
-
definition('assistant_standing_grant_revoke', 'Revoke a Standing Grant with explicit authorization.', {
|
|
669
|
-
repo_id: repoId,
|
|
670
|
-
grant_id: { type: 'string' },
|
|
671
|
-
reason: { type: 'string' },
|
|
672
|
-
confirm_authorization: { type: 'boolean' },
|
|
673
|
-
}, ['grant_id', 'confirm_authorization'], false),
|
|
674
|
-
definition('assistant_action_proposals', 'List or get structured Assistant action proposals and execution status.', {
|
|
675
|
-
repo_id: repoId,
|
|
676
|
-
proposal_id: { type: 'string' },
|
|
677
|
-
status: { type: 'string', enum: ['proposed', 'approved', 'rejected', 'executed', 'failed', 'expired'] },
|
|
678
|
-
limit: { type: 'number' },
|
|
679
|
-
}),
|
|
680
|
-
definition('assistant_action_proposal_resolve', 'Approve or reject one Assistant action proposal. Approval submits a separate user-authorized plugin Job.', {
|
|
681
|
-
repo_id: repoId,
|
|
682
|
-
proposal_id: { type: 'string' },
|
|
683
|
-
decision: { type: 'string', enum: ['approve', 'reject'] },
|
|
684
|
-
request_id: { type: 'string' },
|
|
685
|
-
reason: { type: 'string' },
|
|
686
|
-
confirm_authorization: { type: 'boolean' },
|
|
687
|
-
confirmation_text: { type: 'string' },
|
|
688
|
-
}, ['proposal_id', 'decision'], false),
|
|
689
|
-
definition('external_filesystem_targets_list', 'List pre-authorized external filesystem targets. Does not expose arbitrary absolute-path access.', {
|
|
690
|
-
repo_id: repoId,
|
|
691
|
-
}),
|
|
692
|
-
definition('external_filesystem_grant_preview', 'Preview a read-only external filesystem grant for a narrow absolute directory without writing config.', {
|
|
693
|
-
repo_id: repoId,
|
|
694
|
-
grant_key: { type: 'string' },
|
|
695
|
-
root_path: { type: 'string' },
|
|
696
|
-
mode: { type: 'string', enum: ['read', 'copy_into_repo'] },
|
|
697
|
-
expires_in_minutes: { type: 'number', description: 'Optional expiry in minutes, capped at 24h. Defaults to 8h.' },
|
|
698
|
-
reason: { type: 'string' },
|
|
699
|
-
}, ['grant_key', 'root_path', 'reason']),
|
|
700
|
-
definition('external_filesystem_grant_apply', 'Apply a previously previewed read-only external filesystem grant with explicit authorization.', {
|
|
701
|
-
repo_id: repoId,
|
|
702
|
-
grant_key: { type: 'string' },
|
|
703
|
-
root_path: { type: 'string' },
|
|
704
|
-
mode: { type: 'string', enum: ['read', 'copy_into_repo'] },
|
|
705
|
-
expires_in_minutes: { type: 'number', description: 'Optional expiry in minutes, capped at 24h. Defaults to 8h.' },
|
|
706
|
-
reason: { type: 'string' },
|
|
707
|
-
preview_ticket_id: { type: 'string' },
|
|
708
|
-
confirm_authorization: { type: 'boolean' },
|
|
709
|
-
}, ['grant_key', 'root_path', 'reason', 'preview_ticket_id', 'confirm_authorization'], false),
|
|
710
|
-
definition('external_filesystem_text_snapshot', 'Read a bounded text/directory snapshot from a pre-authorized external filesystem target key.', {
|
|
711
|
-
repo_id: repoId,
|
|
712
|
-
target_key: { type: 'string' },
|
|
713
|
-
path: { type: 'string' },
|
|
714
|
-
max_chars: { type: 'number' },
|
|
715
|
-
}, ['target_key']),
|
|
716
|
-
definition('runtime_storage_repair_preview', 'Preview repo-scoped local-jobs runtime storage repair candidates without mutation.', {
|
|
717
|
-
repo_id: repoId,
|
|
718
|
-
min_age_minutes: { type: 'number' },
|
|
719
|
-
max_candidates: { type: 'number' },
|
|
720
|
-
}),
|
|
721
|
-
definition('runtime_storage_repair_apply', 'Apply safe repo-scoped local-jobs runtime storage repairs after explicit confirmation.', {
|
|
722
|
-
repo_id: repoId,
|
|
723
|
-
candidate_ids: { type: 'array', items: { type: 'string' } },
|
|
724
|
-
min_age_minutes: { type: 'number' },
|
|
725
|
-
max_candidates: { type: 'number' },
|
|
726
|
-
confirm_repair: { type: 'boolean' },
|
|
727
|
-
}, ['confirm_repair'], false),
|
|
728
|
-
definition('list_plugins', 'List repository plugins, or controller-scoped plugins when repo_id is omitted.', {
|
|
729
|
-
repo_id: repoId,
|
|
730
|
-
}),
|
|
731
|
-
definition('get_plugin', 'Read one repository or controller-scoped plugin manifest including action schemas and policy requirements.', {
|
|
732
|
-
repo_id: repoId,
|
|
733
|
-
plugin_id: { type: 'string' },
|
|
734
|
-
}, ['plugin_id']),
|
|
735
|
-
definition('plugin_action_execute', 'Submit one typed repository or controller-scoped plugin action through the durable execution layer.', {
|
|
736
|
-
repo_id: repoId,
|
|
737
|
-
plugin_id: { type: 'string' },
|
|
738
|
-
action_id: { type: 'string' },
|
|
739
|
-
request_id: { type: 'string' },
|
|
740
|
-
work_id: { type: 'string', description: 'Optional existing remote-effect Work that should own the successful typed plugin receipt.' },
|
|
741
|
-
arguments: { type: 'object' },
|
|
742
|
-
timeout_ms: { type: 'number' },
|
|
743
|
-
confirm_authorization: { type: 'boolean' },
|
|
744
|
-
confirmation_text: { type: 'string' },
|
|
745
|
-
}, ['plugin_id', 'action_id', 'request_id'], false),
|
|
746
|
-
definition('toolchain_plugin_summary', 'Return a redacted, low-interception plugin summary without raw config files or full action schemas.', {
|
|
747
|
-
repo_id: repoId,
|
|
748
|
-
plugin_id: { type: 'string' },
|
|
749
|
-
}, ['plugin_id']),
|
|
750
|
-
definition('work_result_summary', 'Return a redacted job result summary with failure class and suggested next actions. Does not return raw stdout or stderr.', {
|
|
751
|
-
repo_id: repoId,
|
|
752
|
-
job_id: { type: 'string' },
|
|
753
|
-
}, ['job_id']),
|
|
754
|
-
definition('work_status_digest', 'Return a redacted work status digest using a neutral work_ref. Does not return raw stdout or stderr.', {
|
|
755
|
-
repo_id: repoId,
|
|
756
|
-
work_ref: { type: 'string' },
|
|
757
|
-
}, ['work_ref']),
|
|
758
|
-
definition('model_clients_summary', 'List enabled model clients and DeepSeek adapter configuration state. Policy remains enforced by forge.', {
|
|
759
|
-
repo_id: repoId,
|
|
760
|
-
}),
|
|
761
|
-
definition('model_control_plane_summary', 'Summarize primary and backup model controllers, handoff modes, and concurrency policy.', {
|
|
762
|
-
repo_id: repoId,
|
|
763
|
-
}),
|
|
764
|
-
definition('deepseek_tool_manifest', 'Return DeepSeek function-calling tool manifests for the low-interception forge surface.', {
|
|
765
|
-
repo_id: repoId,
|
|
766
|
-
}),
|
|
767
|
-
definition('deepseek_tool_call_prepare', 'Translate one DeepSeek function call into a forge operation without executing it.', {
|
|
768
|
-
repo_id: repoId,
|
|
769
|
-
function_name: { type: 'string' },
|
|
770
|
-
function_arguments: { type: 'object' },
|
|
771
|
-
}, ['function_name', 'function_arguments']),
|
|
772
|
-
definition('deepseek_controller_manifest', 'Return DeepSeek backup-controller manifest, boundaries, and function tools.', {
|
|
773
|
-
repo_id: repoId,
|
|
774
|
-
}),
|
|
775
|
-
definition('deepseek_controller_handoff_prepare', 'Prepare a safe DeepSeek backup-controller handoff packet without calling the external model.', {
|
|
776
|
-
repo_id: repoId,
|
|
777
|
-
reason: { type: 'string', enum: ['manual', 'chatgpt_platform_blocked', 'chatgpt_unavailable', 'parallel_review', 'local_gui_request'] },
|
|
778
|
-
objective: { type: 'string' },
|
|
779
|
-
current_controller: { type: 'string' },
|
|
780
|
-
blocked_tool_name: { type: 'string' },
|
|
781
|
-
recent_safe_error: { type: 'string' },
|
|
782
|
-
}),
|
|
783
|
-
definition('deepseek_controller_request_prepare', 'Prepare a DeepSeek chat-completions request for backup controller mode without sending it.', {
|
|
784
|
-
repo_id: repoId,
|
|
785
|
-
reason: { type: 'string', enum: ['manual', 'chatgpt_platform_blocked', 'chatgpt_unavailable', 'parallel_review', 'local_gui_request'] },
|
|
786
|
-
objective: { type: 'string' },
|
|
787
|
-
user_message: { type: 'string' },
|
|
788
|
-
current_controller: { type: 'string' },
|
|
789
|
-
blocked_tool_name: { type: 'string' },
|
|
790
|
-
recent_safe_error: { type: 'string' },
|
|
791
|
-
model: { type: 'string' },
|
|
792
|
-
}),
|
|
793
|
-
definition('assistant_readiness', 'Summarize personal-assistant readiness, Google plugin state, routines, inbox, and recommended next actions.', {
|
|
794
|
-
repo_id: repoId,
|
|
795
|
-
}),
|
|
796
|
-
definition('gmail_triage_rules', 'Read repository-local Gmail triage rules used by the personal assistant manager.', { repo_id: repoId }),
|
|
797
|
-
definition('gmail_triage_rule_upsert', 'Create or update one repository-local Gmail triage rule. This only changes repo-local assistant configuration.', {
|
|
798
|
-
repo_id: repoId,
|
|
799
|
-
id: { type: 'string' },
|
|
800
|
-
enabled: { type: 'boolean' },
|
|
801
|
-
order: { type: 'number' },
|
|
802
|
-
match: { type: 'object' },
|
|
803
|
-
decision: { type: 'object' },
|
|
804
|
-
}, ['id', 'match', 'decision'], false),
|
|
805
|
-
definition('gmail_triage_plan', 'Build a safe Gmail triage plan from supplied message summaries and current Gmail plugin readiness. Does not mutate Gmail.', {
|
|
806
|
-
repo_id: repoId,
|
|
807
|
-
query: { type: 'string' },
|
|
808
|
-
items: { type: 'array', items: { type: 'object' } },
|
|
809
|
-
}),
|
|
810
|
-
definition('review_artifacts_prepare', 'Create bounded repo-local review artifact roots for browser/iOS screenshot review workflows.', { repo_id: repoId }, [], false),
|
|
811
|
-
definition('review_artifacts_index', 'Index bounded repo-local browser/iOS screenshots, logs, and build reports for visual review.', { repo_id: repoId, limit: { type: 'number' } }),
|
|
812
|
-
definition('browser_review_packet', 'Build a browser visual review packet from indexed browser screenshots.', { repo_id: repoId, limit: { type: 'number' } }),
|
|
813
|
-
definition('ios_review_packet', 'Build or capture an iOS visual review packet from simulator screenshots and logs.', {
|
|
814
|
-
repo_id: repoId,
|
|
815
|
-
udid: { type: 'string' },
|
|
816
|
-
label: { type: 'string' },
|
|
817
|
-
capture: { type: 'boolean' },
|
|
818
|
-
limit: { type: 'number' },
|
|
819
|
-
}, [], false),
|
|
820
|
-
definition('workflow_watchdog_report', 'Diagnose stuck repository workflows across execution jobs, local bridge jobs, schedules, and runtime processes.', {
|
|
821
|
-
repo_id: repoId,
|
|
822
|
-
stale_minutes: { type: 'number' },
|
|
823
|
-
include_processes: { type: 'boolean' },
|
|
824
|
-
}),
|
|
825
|
-
definition('ios_xcode_status', 'Check local Xcode, xcodebuild, and simctl availability without mutation.', { repo_id: repoId }),
|
|
826
|
-
definition('ios_simulators_list', 'List available iOS Simulator devices using structured simctl JSON output.', {
|
|
827
|
-
repo_id: repoId,
|
|
828
|
-
runtime: { type: 'string' },
|
|
829
|
-
name: { type: 'string' },
|
|
830
|
-
}),
|
|
831
|
-
definition('ios_project_discover', 'Discover iOS workspaces, projects, Package.swift, and Info.plist files inside the repository.', { repo_id: repoId }),
|
|
832
|
-
definition('ios_schemes_list', 'List Xcode schemes for a repository-bounded workspace or project.', {
|
|
833
|
-
repo_id: repoId,
|
|
834
|
-
workspace: { type: 'string' },
|
|
835
|
-
project: { type: 'string' },
|
|
836
|
-
}),
|
|
837
|
-
definition('ios_simulator_boot', 'Boot an explicitly selected iOS Simulator UDID. Requires authorization.', {
|
|
838
|
-
repo_id: repoId,
|
|
839
|
-
udid: { type: 'string' },
|
|
840
|
-
open_simulator: { type: 'boolean' },
|
|
841
|
-
confirm_authorization: { type: 'boolean' },
|
|
842
|
-
timeout_ms: { type: 'number' },
|
|
843
|
-
}, ['udid', 'confirm_authorization'], false),
|
|
844
|
-
definition('ios_app_build', 'Build an iOS app into .forge/ios/DerivedData using xcodebuild structured arguments.', {
|
|
845
|
-
repo_id: repoId,
|
|
846
|
-
scheme: { type: 'string' },
|
|
847
|
-
udid: { type: 'string' },
|
|
848
|
-
workspace: { type: 'string' },
|
|
849
|
-
project: { type: 'string' },
|
|
850
|
-
configuration: { type: 'string' },
|
|
851
|
-
timeout_ms: { type: 'number' },
|
|
852
|
-
}, ['scheme'], false),
|
|
853
|
-
definition('ios_app_install', 'Install a bounded .app product from .forge/ios/DerivedData into a booted simulator. Requires authorization.', {
|
|
854
|
-
repo_id: repoId,
|
|
855
|
-
udid: { type: 'string' },
|
|
856
|
-
app_path: { type: 'string' },
|
|
857
|
-
confirm_authorization: { type: 'boolean' },
|
|
858
|
-
}, ['udid', 'app_path', 'confirm_authorization'], false),
|
|
859
|
-
definition('ios_app_launch', 'Launch a simulator app by bundle id with structured launch arguments. Requires authorization.', {
|
|
860
|
-
repo_id: repoId,
|
|
861
|
-
udid: { type: 'string' },
|
|
862
|
-
bundle_id: { type: 'string' },
|
|
863
|
-
arguments: { type: 'array', items: { type: 'string' } },
|
|
864
|
-
confirm_authorization: { type: 'boolean' },
|
|
865
|
-
}, ['udid', 'bundle_id', 'confirm_authorization'], false),
|
|
866
|
-
definition('ios_simulator_screenshot', 'Capture a simulator screenshot to .forge/ios/screenshots as a bounded artifact.', {
|
|
867
|
-
repo_id: repoId,
|
|
868
|
-
udid: { type: 'string' },
|
|
869
|
-
label: { type: 'string' },
|
|
870
|
-
}, ['udid'], false),
|
|
871
|
-
definition('ios_simulator_log_tail', 'Collect a bounded recent simulator log tail into .forge/ios/logs.', {
|
|
872
|
-
repo_id: repoId,
|
|
873
|
-
udid: { type: 'string' },
|
|
874
|
-
process: { type: 'string' },
|
|
875
|
-
last: { type: 'string' },
|
|
876
|
-
max_bytes: { type: 'number' },
|
|
877
|
-
}),
|
|
878
|
-
definition('ios_ui_smoke_test', 'Compose build, boot, launch, screenshot, and bounded logs for simulator UI review. Requires authorization.', {
|
|
879
|
-
repo_id: repoId,
|
|
880
|
-
udid: { type: 'string' },
|
|
881
|
-
scheme: { type: 'string' },
|
|
882
|
-
bundle_id: { type: 'string' },
|
|
883
|
-
workspace: { type: 'string' },
|
|
884
|
-
project: { type: 'string' },
|
|
885
|
-
configuration: { type: 'string' },
|
|
886
|
-
app_path: { type: 'string' },
|
|
887
|
-
screenshot_label: { type: 'string' },
|
|
888
|
-
confirm_authorization: { type: 'boolean' },
|
|
889
|
-
}, ['udid', 'scheme', 'bundle_id', 'confirm_authorization'], false),
|
|
890
|
-
definition('runtime_cleanup_preview', 'Preview stale forge temp directories, terminal local jobs, and historical attention cleanup candidates.', {
|
|
891
|
-
repo_id: repoId,
|
|
892
|
-
min_age_minutes: { type: 'number' },
|
|
893
|
-
include_temp_dirs: { type: 'boolean' },
|
|
894
|
-
include_terminal_local_jobs: { type: 'boolean' },
|
|
895
|
-
include_legacy_runs: { type: 'boolean' },
|
|
896
|
-
include_historical_attention: { type: 'boolean' },
|
|
897
|
-
max_candidates: { type: 'number' },
|
|
898
|
-
}),
|
|
899
|
-
definition('runtime_cleanup_apply', 'Apply safe cleanup candidates. Requires confirm_cleanup=true and remains limited to explicit forge candidates.', {
|
|
900
|
-
repo_id: repoId,
|
|
901
|
-
min_age_minutes: { type: 'number' },
|
|
902
|
-
include_temp_dirs: { type: 'boolean' },
|
|
903
|
-
include_terminal_local_jobs: { type: 'boolean' },
|
|
904
|
-
include_legacy_runs: { type: 'boolean' },
|
|
905
|
-
include_historical_attention: { type: 'boolean' },
|
|
906
|
-
max_candidates: { type: 'number' },
|
|
907
|
-
confirm_cleanup: { type: 'boolean' },
|
|
908
|
-
}, [], false),
|
|
909
|
-
definition('create_schedule', 'Create a bounded repository Schedule. Shadow mode defaults to true.', {
|
|
910
|
-
repo_id: repoId,
|
|
911
|
-
request_id: { type: 'string' },
|
|
912
|
-
name: { type: 'string' },
|
|
913
|
-
trigger_type: { type: 'string', enum: ['interval', 'cron', 'calendar', 'condition', 'repository-event', 'dependency-checkpoint', 'manual'] },
|
|
914
|
-
every_minutes: { type: 'number' },
|
|
915
|
-
cron_expression: { type: 'string' },
|
|
916
|
-
calendar_at: { type: 'string' },
|
|
917
|
-
condition: { type: 'object' },
|
|
918
|
-
event_name: { type: 'string' },
|
|
919
|
-
dependency_job_ids: { type: 'array', items: { type: 'string' } },
|
|
920
|
-
operation: { type: 'string' },
|
|
921
|
-
arguments: { type: 'object' },
|
|
922
|
-
shadow_mode: { type: 'boolean' },
|
|
923
|
-
daily_budget_minutes: { type: 'number' },
|
|
924
|
-
cooldown_minutes: { type: 'number' },
|
|
925
|
-
backoff_base_minutes: { type: 'number' },
|
|
926
|
-
backoff_max_minutes: { type: 'number' },
|
|
927
|
-
max_failures: { type: 'number' },
|
|
928
|
-
stop_conditions: { type: 'array', items: { type: 'string' } },
|
|
929
|
-
}, ['name', 'operation'], false),
|
|
930
|
-
definition('list_schedules', 'List repository Schedules and recent bounded Occurrences.', { repo_id: repoId, include_occurrences: { type: 'boolean' } }),
|
|
931
|
-
definition('pause_schedule', 'Pause a repository Schedule.', { repo_id: repoId, schedule_id: { type: 'string' }, reason: { type: 'string' } }, ['schedule_id'], false),
|
|
932
|
-
definition('trigger_schedule', 'Create one idempotent bounded Occurrence for a Schedule or deliver a repository event.', {
|
|
933
|
-
repo_id: repoId,
|
|
934
|
-
schedule_id: { type: 'string' },
|
|
935
|
-
event_name: { type: 'string' },
|
|
936
|
-
event_id: { type: 'string' },
|
|
937
|
-
event_data: { type: 'object' },
|
|
938
|
-
}, ['schedule_id'], false),
|
|
939
|
-
definition('request_release_gate', 'Return an explicit external-Controller handoff for release evidence. Push, tag, and publish remain separately authorized.', { repo_id: repoId, request_id: { type: 'string' } }, [], false),
|
|
940
|
-
definition('create_portfolio_workflow', 'Create a cross-repository DAG with deterministic Saga stop/compensation semantics.', {
|
|
941
|
-
name: { type: 'string' }, request_id: { type: 'string' }, failure_policy: { type: 'string', enum: ['stop', 'compensate'] }, steps: { type: 'array', items: { type: 'object' } },
|
|
942
|
-
}, ['name', 'request_id', 'steps'], false),
|
|
943
|
-
definition('list_portfolio_workflows', 'List cross-repository Portfolio workflows.', { limit: { type: 'number' } }),
|
|
944
|
-
definition('get_portfolio_workflow', 'Read one Portfolio workflow and its repository DAG state.', { workflow_id: { type: 'string' } }, ['workflow_id']),
|
|
945
|
-
definition('record_candidate_finding', 'Record or refresh a deduplicated candidate requirement without creating an Issue automatically.', {
|
|
946
|
-
repo_id: repoId, request_id: { type: 'string' }, semantic_key: { type: 'string' }, title: { type: 'string' }, summary: { type: 'string' }, severity: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] }, reference: { type: 'string' }, evidence: { type: 'object' },
|
|
947
|
-
}, ['semantic_key', 'title'], false),
|
|
948
|
-
definition('list_candidate_findings', 'List bounded candidate findings awaiting human promotion or dismissal.', { repo_id: repoId, include_terminal: { type: 'boolean' }, limit: { type: 'number' } }),
|
|
949
|
-
definition('promote_candidate_finding', 'Return the candidate finding and an explicit external-Controller handoff for Issue creation.', {
|
|
950
|
-
repo_id: repoId, finding_id: { type: 'string' }, request_id: { type: 'string' }, kind: { type: 'string', enum: ['bug', 'feature', 'governance', 'investigation'] }, goals: { type: 'array', items: { type: 'string' } }, acceptance_criteria: { type: 'array', items: { type: 'string' } },
|
|
951
|
-
}, ['finding_id'], false),
|
|
952
|
-
];
|
|
953
|
-
|
|
954
|
-
export const connectorExposedTools = runtimeToolDefinitions.map((tool) => tool.name);
|
|
955
|
-
export const currentCallableTools = new Set(connectorExposedTools);
|
|
225
|
+
export {
|
|
226
|
+
connectorExposedTools,
|
|
227
|
+
currentCallableTools,
|
|
228
|
+
runtimeToolDefinitions,
|
|
229
|
+
} from './runtime-tool-definitions';
|
|
230
|
+
import {
|
|
231
|
+
currentCallableTools,
|
|
232
|
+
runtimeToolDefinitions,
|
|
233
|
+
} from './runtime-tool-definitions';
|
|
956
234
|
|
|
957
235
|
const RH_CONTEXT_CURRENT_WINDOW_MS = 24 * 60 * 60 * 1_000;
|
|
958
236
|
|
|
@@ -971,6 +249,233 @@ function isCurrentRhContextWork(
|
|
|
971
249
|
return timestampIsCurrent(contract.updatedAt, cutoffMs);
|
|
972
250
|
}
|
|
973
251
|
|
|
252
|
+
function rhContextReadSessionId(ctx: MultiRepositoryMcpToolContext): string | undefined {
|
|
253
|
+
const principal = ctx.principalId?.trim();
|
|
254
|
+
if (principal) {
|
|
255
|
+
const controllerInstance = ctx.controllerInstanceId?.trim() || currentControllerInstanceId();
|
|
256
|
+
return `controller:${principal}:${controllerInstance}`;
|
|
257
|
+
}
|
|
258
|
+
const transportSession = ctx.sessionId?.trim();
|
|
259
|
+
return transportSession ? `transport:${transportSession}` : undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const RH_CONTEXT_SEMANTIC_QUERY_LIMIT = 8;
|
|
263
|
+
const RH_CONTEXT_SEMANTIC_LOCATION_LIMIT = 200;
|
|
264
|
+
const RH_CONTEXT_LEGACY_SEMANTIC_SYNTAX = '@tsnav references <repo-path>:<line>:<column> | @swiftnav references <repo-path>:<line>:<column>';
|
|
265
|
+
|
|
266
|
+
type RhContextSemanticNavigationRequest = {
|
|
267
|
+
navigation: SemanticNavigationKind;
|
|
268
|
+
path: string;
|
|
269
|
+
line: number;
|
|
270
|
+
column: number;
|
|
271
|
+
tsconfig_path?: string;
|
|
272
|
+
language?: string;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
function rhContextLegacySemanticQuery(query: string): {
|
|
276
|
+
retrievalQuery: string;
|
|
277
|
+
requests: RhContextSemanticNavigationRequest[];
|
|
278
|
+
} {
|
|
279
|
+
const requests: RhContextSemanticNavigationRequest[] = [];
|
|
280
|
+
const directive = /(?:^|\s)@(tsnav|swiftnav)\s+(definition|references|implementations)\s+([^\s]+):(\d+):(\d+)(?:\s+tsconfig=([^\s]+))?/gi;
|
|
281
|
+
const retrievalQuery = query.replace(directive, (_match, directiveKind, navigation, path, line, column, tsconfigPath) => {
|
|
282
|
+
const language = String(directiveKind).toLowerCase() === 'swiftnav' ? 'swift' : 'typescript';
|
|
283
|
+
requests.push({
|
|
284
|
+
navigation: String(navigation).toLowerCase() as SemanticNavigationKind,
|
|
285
|
+
path: String(path),
|
|
286
|
+
line: Number(line),
|
|
287
|
+
column: Number(column),
|
|
288
|
+
language,
|
|
289
|
+
...(tsconfigPath ? { tsconfig_path: String(tsconfigPath) } : {}),
|
|
290
|
+
});
|
|
291
|
+
return ' ';
|
|
292
|
+
}).replace(/\s+/g, ' ').trim();
|
|
293
|
+
const fallback = requests.length > 0
|
|
294
|
+
? `${requests[0]!.language === 'swift' ? 'Swift' : 'TypeScript'} ${requests[0]!.navigation} ${requests[0]!.path}`
|
|
295
|
+
: query;
|
|
296
|
+
return { retrievalQuery: retrievalQuery || fallback, requests };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
interface RhContextSemanticNavigationProjection {
|
|
300
|
+
requested: number;
|
|
301
|
+
executed: number;
|
|
302
|
+
results: Record<string, unknown>[];
|
|
303
|
+
errors: Array<{ index: number; code: string; message: string }>;
|
|
304
|
+
providers: unknown[];
|
|
305
|
+
policyDeniedLocations: number;
|
|
306
|
+
policyDeniedReads: number;
|
|
307
|
+
policyDeniedReadSamples: string[];
|
|
308
|
+
requestTruncated: boolean;
|
|
309
|
+
freshness: 'not_requested' | 'changed_during_query' | 'current_at_query';
|
|
310
|
+
sourceIdentity?: Record<string, unknown>;
|
|
311
|
+
staticClosure: { scope: string; status: 'not_requested' | 'incomplete' | 'complete_for_requested_symbols'; limitations: string[] };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function rhContextSemanticNavigation(
|
|
315
|
+
repoRoot: string,
|
|
316
|
+
policy: MultiRepositoryMcpToolContext['policy'],
|
|
317
|
+
value: unknown,
|
|
318
|
+
repositoryIdentity: { repoId: string; checkoutId: string },
|
|
319
|
+
): Promise<RhContextSemanticNavigationProjection> {
|
|
320
|
+
const raw = Array.isArray(value) ? value : [];
|
|
321
|
+
const requests = raw.slice(0, RH_CONTEXT_SEMANTIC_QUERY_LIMIT);
|
|
322
|
+
const results: Record<string, unknown>[] = [];
|
|
323
|
+
const errors: Array<{ index: number; code: string; message: string }> = [];
|
|
324
|
+
let anyLocationTruncated = false;
|
|
325
|
+
let policyDeniedLocations = 0;
|
|
326
|
+
let policyDeniedReads = 0;
|
|
327
|
+
const policyDeniedReadSamples = new Set<string>();
|
|
328
|
+
const semanticAccessScope = createHash('sha256')
|
|
329
|
+
.update(JSON.stringify({ profile: policy.profile, readGlobs: policy.readGlobs, denyGlobs: policy.denyGlobs }))
|
|
330
|
+
.digest('hex')
|
|
331
|
+
.slice(0, 20);
|
|
332
|
+
|
|
333
|
+
const fingerprintOf = (identity: ReturnType<typeof freshGitIdentity> | undefined): string | undefined => identity
|
|
334
|
+
? identity.workingTreeFingerprint
|
|
335
|
+
?? createHash('sha256').update(`${identity.head ?? ''}\n${identity.branch ?? ''}`).digest('hex').slice(0, 24)
|
|
336
|
+
: undefined;
|
|
337
|
+
// Semantic providers are targeted/optional, so pay the stronger fresh identity
|
|
338
|
+
// sampling cost only when semantic evidence was explicitly requested.
|
|
339
|
+
const sourceBefore = requests.length > 0 ? freshGitIdentity(repoRoot) : undefined;
|
|
340
|
+
const sourceFingerprintBefore = fingerprintOf(sourceBefore);
|
|
341
|
+
const indexedRequests: Array<{ index: number; request: SemanticNavigationRequest }> = [];
|
|
342
|
+
|
|
343
|
+
requests.forEach((entry, index) => {
|
|
344
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
345
|
+
errors.push({ index, code: 'SEMANTIC_NAVIGATION_REQUEST_INVALID', message: 'semantic_navigation entries must be objects.' });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const item = entry as Record<string, unknown>;
|
|
349
|
+
const navigation = String(item.navigation ?? '') as SemanticNavigationKind;
|
|
350
|
+
const path = String(item.path ?? '').trim();
|
|
351
|
+
const line = Number(item.line);
|
|
352
|
+
const column = Number(item.column);
|
|
353
|
+
const tsconfigPath = typeof item.tsconfig_path === 'string' && item.tsconfig_path.trim() ? item.tsconfig_path.trim() : undefined;
|
|
354
|
+
const language = typeof item.language === 'string' && item.language.trim() ? item.language.trim().toLowerCase() : undefined;
|
|
355
|
+
if (!['definition', 'references', 'implementations'].includes(navigation) || !path || !Number.isInteger(line) || line < 1 || !Number.isInteger(column) || column < 1) {
|
|
356
|
+
errors.push({ index, code: 'SEMANTIC_NAVIGATION_REQUEST_INVALID', message: 'navigation, path, and positive 1-based line/column are required.' });
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const targetDecision = resolveMcpPath(repoRoot, path, policy, 'read');
|
|
360
|
+
if (!targetDecision.ok) {
|
|
361
|
+
errors.push({ index, code: 'SEMANTIC_NAVIGATION_TARGET_DENIED', message: targetDecision.reason ?? 'target path denied by MCP read policy.' });
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
indexedRequests.push({
|
|
365
|
+
index,
|
|
366
|
+
request: { navigation, path, line, column, ...(tsconfigPath ? { tsconfigPath } : {}), ...(language ? { language } : {}) },
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
const allowRepositoryPath = (relativePath: string): boolean => {
|
|
371
|
+
const decision = resolveMcpPath(repoRoot, relativePath, policy, 'read');
|
|
372
|
+
if (decision.ok) return true;
|
|
373
|
+
policyDeniedReads += 1;
|
|
374
|
+
if (policyDeniedReadSamples.size < 20) policyDeniedReadSamples.add(relativePath);
|
|
375
|
+
return false;
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const navigationOutcomes = await defaultSemanticProviderRegistry.navigate(repoRoot, indexedRequests, {
|
|
379
|
+
cacheScope: `mcp:${semanticAccessScope}`,
|
|
380
|
+
sourceIdentity: sourceFingerprintBefore,
|
|
381
|
+
profile: policy.profile,
|
|
382
|
+
allowRepositoryPath,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
for (const { index, outcome } of navigationOutcomes) {
|
|
386
|
+
if (!outcome.ok) {
|
|
387
|
+
errors.push({ index, code: outcome.code, message: outcome.message });
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const semantic = outcome.result;
|
|
391
|
+
const allowedLocations = semantic.locations.filter((location) => {
|
|
392
|
+
const decision = resolveMcpPath(repoRoot, location.path, policy, 'read');
|
|
393
|
+
if (decision.ok) return true;
|
|
394
|
+
policyDeniedLocations += 1;
|
|
395
|
+
return false;
|
|
396
|
+
});
|
|
397
|
+
const truncated = allowedLocations.length > RH_CONTEXT_SEMANTIC_LOCATION_LIMIT;
|
|
398
|
+
anyLocationTruncated ||= truncated;
|
|
399
|
+
results.push({
|
|
400
|
+
provider: semantic.providerId,
|
|
401
|
+
...(semantic.providerIdentity ? { providerIdentity: semantic.providerIdentity } : {}),
|
|
402
|
+
language: semantic.language,
|
|
403
|
+
navigation: semantic.navigation,
|
|
404
|
+
target: semantic.target,
|
|
405
|
+
locations: allowedLocations.slice(0, RH_CONTEXT_SEMANTIC_LOCATION_LIMIT),
|
|
406
|
+
totalLocations: allowedLocations.length,
|
|
407
|
+
returnedLocations: Math.min(allowedLocations.length, RH_CONTEXT_SEMANTIC_LOCATION_LIMIT),
|
|
408
|
+
truncated,
|
|
409
|
+
policyDeniedReads: semantic.policyDeniedReads ?? 0,
|
|
410
|
+
...(semantic.details ?? {}),
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
results.sort((left, right) => {
|
|
415
|
+
const leftTarget = left.target as { path?: string; line?: number; column?: number } | undefined;
|
|
416
|
+
const rightTarget = right.target as { path?: string; line?: number; column?: number } | undefined;
|
|
417
|
+
return String(leftTarget?.path ?? '').localeCompare(String(rightTarget?.path ?? ''))
|
|
418
|
+
|| Number(leftTarget?.line ?? 0) - Number(rightTarget?.line ?? 0)
|
|
419
|
+
|| Number(leftTarget?.column ?? 0) - Number(rightTarget?.column ?? 0);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const sourceAfter = requests.length > 0 ? freshGitIdentity(repoRoot) : undefined;
|
|
423
|
+
const sourceFingerprintAfter = fingerprintOf(sourceAfter);
|
|
424
|
+
const sourceChangedDuringQuery = Boolean(
|
|
425
|
+
sourceBefore
|
|
426
|
+
&& sourceAfter
|
|
427
|
+
&& (sourceBefore.head !== sourceAfter.head || sourceFingerprintBefore !== sourceFingerprintAfter),
|
|
428
|
+
);
|
|
429
|
+
if (sourceChangedDuringQuery) {
|
|
430
|
+
errors.push({
|
|
431
|
+
index: -1,
|
|
432
|
+
code: 'SEMANTIC_SOURCE_CHANGED_DURING_QUERY',
|
|
433
|
+
message: 'Repository source identity changed while semantic providers were running. Returned locations are retained only as hints for the sampled source and are not proof for the newer source state.',
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const requestTruncated = raw.length > RH_CONTEXT_SEMANTIC_QUERY_LIMIT;
|
|
438
|
+
const incomplete = requestTruncated || anyLocationTruncated || policyDeniedLocations > 0 || policyDeniedReads > 0 || errors.length > 0 || sourceChangedDuringQuery;
|
|
439
|
+
const languages = new Set(results.map((entry) => String(entry.language ?? '')).filter(Boolean));
|
|
440
|
+
const singleLanguage = languages.size === 1 ? [...languages][0] : undefined;
|
|
441
|
+
const scope = singleLanguage === 'typescript'
|
|
442
|
+
? 'requested_typescript_static_relationships'
|
|
443
|
+
: singleLanguage === 'swift'
|
|
444
|
+
? 'requested_swift_static_relationships'
|
|
445
|
+
: singleLanguage
|
|
446
|
+
? `requested_${singleLanguage.replace(/[^a-z0-9_-]+/gi, '_').toLowerCase()}_static_relationships`
|
|
447
|
+
: languages.size > 1
|
|
448
|
+
? 'requested_multilanguage_static_relationships'
|
|
449
|
+
: 'requested_semantic_static_relationships';
|
|
450
|
+
return {
|
|
451
|
+
requested: raw.length,
|
|
452
|
+
executed: requests.length,
|
|
453
|
+
results,
|
|
454
|
+
errors,
|
|
455
|
+
providers: defaultSemanticProviderRegistry.list(),
|
|
456
|
+
policyDeniedLocations,
|
|
457
|
+
policyDeniedReads,
|
|
458
|
+
policyDeniedReadSamples: Array.from(policyDeniedReadSamples),
|
|
459
|
+
requestTruncated,
|
|
460
|
+
freshness: raw.length === 0 ? 'not_requested' : sourceChangedDuringQuery ? 'changed_during_query' : 'current_at_query',
|
|
461
|
+
...(sourceBefore ? {
|
|
462
|
+
sourceIdentity: {
|
|
463
|
+
repoId: repositoryIdentity.repoId,
|
|
464
|
+
checkoutId: repositoryIdentity.checkoutId,
|
|
465
|
+
branch: sourceBefore.branch,
|
|
466
|
+
head: sourceBefore.head,
|
|
467
|
+
workingTreeFingerprint: sourceFingerprintBefore,
|
|
468
|
+
sampledAt: new Date(sourceBefore.sampledAt).toISOString(),
|
|
469
|
+
},
|
|
470
|
+
} : {}),
|
|
471
|
+
staticClosure: {
|
|
472
|
+
scope,
|
|
473
|
+
status: raw.length === 0 ? 'not_requested' : incomplete ? 'incomplete' : 'complete_for_requested_symbols',
|
|
474
|
+
limitations: ['dynamic_registration', 'string_or_config_edges', 'reflection', 'runtime_dispatch', 'stale_or_missing_language_index'],
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
974
479
|
export function summarizeControllerReadyPayload(fullPayload: Record<string, unknown>): Record<string, unknown> {
|
|
975
480
|
const health = (fullPayload.health ?? {}) as Record<string, unknown>;
|
|
976
481
|
const workerLoop = (fullPayload.workerLoop ?? {}) as Record<string, unknown>;
|
|
@@ -1110,6 +615,32 @@ async function callStandaloneRecoveryTool(
|
|
|
1110
615
|
}
|
|
1111
616
|
}
|
|
1112
617
|
|
|
618
|
+
export const RH_WORK_VERIFY_LEASE_WAIT_MS = DEFAULT_WORK_CHECK_LEASE_WAIT_MS;
|
|
619
|
+
|
|
620
|
+
type TerminalCheckEvidenceState = 'matched' | 'process_runtime_failed_before_result' | 'missing' | 'mismatch';
|
|
621
|
+
|
|
622
|
+
export function classifyTerminalCheckEvidence(input: {
|
|
623
|
+
processError?: { code: string; message: string };
|
|
624
|
+
structuredPresent: boolean;
|
|
625
|
+
structuredMatches: boolean;
|
|
626
|
+
legacyPresent: boolean;
|
|
627
|
+
legacyMatches: boolean;
|
|
628
|
+
}): { state: TerminalCheckEvidenceState; warning?: string; infrastructureReason?: string } {
|
|
629
|
+
if (input.structuredMatches || input.legacyMatches) return { state: 'matched' };
|
|
630
|
+
if (!input.structuredPresent && !input.legacyPresent && input.processError?.message?.trim()) {
|
|
631
|
+
const reason = input.processError.message.trim().slice(0, 512);
|
|
632
|
+
return {
|
|
633
|
+
state: 'process_runtime_failed_before_result',
|
|
634
|
+
warning: `check process failed before structured result receipt: ${reason}`,
|
|
635
|
+
infrastructureReason: reason,
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
if (input.structuredPresent || input.legacyPresent) {
|
|
639
|
+
return { state: 'mismatch', warning: 'check result receipt did not match the terminal Process semantic identity' };
|
|
640
|
+
}
|
|
641
|
+
return { state: 'missing', warning: 'check result receipt is missing for the terminal Check Process' };
|
|
642
|
+
}
|
|
643
|
+
|
|
1113
644
|
function result(value: Record<string, unknown>, isError = false): CallToolResult {
|
|
1114
645
|
// Compact text channel by default (no pretty-print bloat).
|
|
1115
646
|
return { content: [{ type: 'text', text: JSON.stringify(value) }], structuredContent: value, ...(isError ? { isError: true } : {}) };
|
|
@@ -1293,6 +824,17 @@ function summarizePluginActionReceipt(manifest: ReturnType<typeof getAssistantPl
|
|
|
1293
824
|
};
|
|
1294
825
|
}
|
|
1295
826
|
|
|
827
|
+
function compactSubmittedPluginActionResult(value: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
|
|
828
|
+
if (!value) return undefined;
|
|
829
|
+
const nested = value.result;
|
|
830
|
+
if (!nested || typeof nested !== 'object' || Array.isArray(nested)) return value;
|
|
831
|
+
const work = value.work;
|
|
832
|
+
return {
|
|
833
|
+
...(nested as Record<string, unknown>),
|
|
834
|
+
...(work && typeof work === 'object' && !Array.isArray(work) ? { work } : {}),
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
|
|
1296
838
|
|
|
1297
839
|
function stringList(value: unknown): string[] {
|
|
1298
840
|
return Array.isArray(value) ? value.map(String).map((entry) => entry.trim()).filter(Boolean) : [];
|
|
@@ -1636,6 +1178,7 @@ function ensureFacadeWorkHandle(
|
|
|
1636
1178
|
managedWorktree,
|
|
1637
1179
|
workContractId: contract.workId,
|
|
1638
1180
|
baseCommit: contract.baseRevision ?? status.head ?? undefined,
|
|
1181
|
+
deliveryBaseCommit: contract.baseRevision ?? status.head ?? undefined,
|
|
1639
1182
|
expectedHead: status.head ?? contract.baseRevision,
|
|
1640
1183
|
permissionSnapshotVersion: currentPermissionSnapshotVersion(ctx.controllerHome, repository.repoId),
|
|
1641
1184
|
state: 'prepared',
|
|
@@ -1646,6 +1189,206 @@ function ensureFacadeWorkHandle(
|
|
|
1646
1189
|
});
|
|
1647
1190
|
}
|
|
1648
1191
|
|
|
1192
|
+
function materializeFacadeWorkPlacement(
|
|
1193
|
+
ctx: MultiRepositoryMcpToolContext,
|
|
1194
|
+
repository: ReturnType<typeof selected>,
|
|
1195
|
+
workId: string,
|
|
1196
|
+
args: Record<string, unknown>,
|
|
1197
|
+
) {
|
|
1198
|
+
const store = { controllerHome: ctx.controllerHome, repoId: repository.repoId };
|
|
1199
|
+
const contract = getWorkContract(store, workId);
|
|
1200
|
+
if (!contract || contract.worktreePolicy.required !== true || contract.worktreeRef) return contract;
|
|
1201
|
+
const workspace = ensureManagedWorkspace(ctx.controllerHome, repository, {
|
|
1202
|
+
requestId: workId,
|
|
1203
|
+
title: contract.objective,
|
|
1204
|
+
baseRef: contract.baseRevision,
|
|
1205
|
+
prepareDependencies: args.needs_dependencies === true,
|
|
1206
|
+
});
|
|
1207
|
+
if (!workspace.managed || !workspace.checkoutId || !workspace.root || workspace.checkoutId === repository.activeCheckoutId) {
|
|
1208
|
+
throw new Error('MANAGED_WORKSPACE_NOT_MATERIALIZED');
|
|
1209
|
+
}
|
|
1210
|
+
return updateWorkContract(store, workId, {
|
|
1211
|
+
checkoutId: workspace.checkoutId,
|
|
1212
|
+
baseRevision: workspace.baseRevision ?? contract.baseRevision,
|
|
1213
|
+
worktreeRef: workspace.root,
|
|
1214
|
+
driver: { ...contract.driver, preferred: 'isolated_worktree', allowDirectEdit: false },
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function reauthorizeCancelledFacadeWork(
|
|
1219
|
+
ctx: MultiRepositoryMcpToolContext,
|
|
1220
|
+
repository: ReturnType<typeof selected>,
|
|
1221
|
+
workId: string,
|
|
1222
|
+
args: Record<string, unknown>,
|
|
1223
|
+
): { reconstructedCheckout: boolean } {
|
|
1224
|
+
const store = { controllerHome: ctx.controllerHome, repoId: repository.repoId };
|
|
1225
|
+
let work = getWorkContract(store, workId);
|
|
1226
|
+
if (!work || work.status !== 'cancelled') return { reconstructedCheckout: false };
|
|
1227
|
+
if (args.approval_confirmed !== true || args.requested_by !== 'user') {
|
|
1228
|
+
throw new Error(`WORK_CANCELLED_RESUME_REAUTHORIZATION_REQUIRED: ${workId}; use exact work_id with requested_by=user and approval_confirmed=true`);
|
|
1229
|
+
}
|
|
1230
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
1231
|
+
if (!work.principalId?.trim() || work.principalId.trim() !== identity.principalId) {
|
|
1232
|
+
throw new Error(`WORK_CANCELLED_RESUME_PRINCIPAL_MISMATCH: ${workId}`);
|
|
1233
|
+
}
|
|
1234
|
+
const owner = getControllerSession(store, workId);
|
|
1235
|
+
if (owner && (owner.principalId?.trim() || owner.controllerId) !== identity.principalId) {
|
|
1236
|
+
throw new Error(`WORK_CANCELLED_RESUME_CONTROLLER_OWNERSHIP_MISMATCH: ${workId}`);
|
|
1237
|
+
}
|
|
1238
|
+
if (work.completionReceipt || work.completionOutcome) throw new Error(`WORK_CANCELLED_RESUME_COMPLETION_CONFLICT: ${workId}`);
|
|
1239
|
+
if (work.reconciliations.length > 0) throw new Error(`WORK_CANCELLED_RESUME_DELIVERY_HISTORY_AMBIGUOUS: ${workId}`);
|
|
1240
|
+
if (work.phase !== 'cleanup' || work.phaseEvidence.cleanup.state !== 'skipped' || work.phaseEvidence.cleanup.source !== 'recorded') {
|
|
1241
|
+
throw new Error(`WORK_CANCELLED_RESUME_HISTORY_AMBIGUOUS: ${workId}`);
|
|
1242
|
+
}
|
|
1243
|
+
if (work.workKind !== 'repository_change' || work.worktreePolicy.required !== true) {
|
|
1244
|
+
throw new Error(`WORK_CANCELLED_RESUME_ISOLATED_REPOSITORY_WORK_REQUIRED: ${workId}`);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
let handle = readWorkHandle(ctx.controllerHome, repository.repoId, workId);
|
|
1248
|
+
if (handle) {
|
|
1249
|
+
if (handle.repositoryId !== repository.repoId || (handle.workContractId && handle.workContractId !== workId)) {
|
|
1250
|
+
throw new Error(`WORK_CANCELLED_RESUME_HANDLE_IDENTITY_MISMATCH: ${workId}`);
|
|
1251
|
+
}
|
|
1252
|
+
if (handle.principalId !== identity.principalId) throw new Error(`WORK_CANCELLED_RESUME_HANDLE_PRINCIPAL_MISMATCH: ${workId}`);
|
|
1253
|
+
if (['committed', 'merged', 'cleaned', 'failed_terminal_cleanup'].includes(handle.state)
|
|
1254
|
+
|| handle.finalization.commit === 'done'
|
|
1255
|
+
|| handle.finalization.merge === 'done'
|
|
1256
|
+
|| handle.cleanupReceipt?.complete === true) {
|
|
1257
|
+
throw new Error(`WORK_CANCELLED_RESUME_DELIVERY_OR_CLEANUP_CONFLICT: ${workId}`);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
const registryRepository = listRepositories(ctx.controllerHome, { includeRemoved: true })
|
|
1262
|
+
.find((candidate) => candidate.repoId === repository.repoId);
|
|
1263
|
+
if (!registryRepository) throw new Error(`WORK_CANCELLED_RESUME_REPOSITORY_MISSING: ${workId}`);
|
|
1264
|
+
const recordedCheckoutId = work.checkoutId?.trim();
|
|
1265
|
+
const recordedWorktree = work.worktreeRef?.trim();
|
|
1266
|
+
const checkoutRecord = recordedCheckoutId
|
|
1267
|
+
? registryRepository.checkouts.find((candidate) => candidate.checkoutId === recordedCheckoutId)
|
|
1268
|
+
: undefined;
|
|
1269
|
+
const checkoutActive = checkoutRecord?.lifecycle === 'active';
|
|
1270
|
+
const worktreePresent = Boolean(recordedWorktree && existsSync(recordedWorktree));
|
|
1271
|
+
|
|
1272
|
+
if (checkoutActive && worktreePresent && recordedCheckoutId && recordedWorktree) {
|
|
1273
|
+
const retained = selectRepositoryCheckout(registryRepository, recordedCheckoutId);
|
|
1274
|
+
if (resolve(retained.canonicalRoot) !== resolve(recordedWorktree)) {
|
|
1275
|
+
throw new Error(`WORK_CANCELLED_RESUME_CHECKOUT_REUSED: ${workId}`);
|
|
1276
|
+
}
|
|
1277
|
+
if (!handle) throw new Error(`WORK_CANCELLED_RESUME_HANDLE_REQUIRED: ${workId}`);
|
|
1278
|
+
if (handle.checkoutId !== recordedCheckoutId || resolve(handle.worktreePath) !== resolve(recordedWorktree)) {
|
|
1279
|
+
throw new Error(`WORK_CANCELLED_RESUME_CHECKOUT_OWNERSHIP_MISMATCH: ${workId}`);
|
|
1280
|
+
}
|
|
1281
|
+
const status = repositoryGitStatus(retained);
|
|
1282
|
+
if (!status.branch || status.branch !== handle.branch) throw new Error(`WORK_CANCELLED_RESUME_BRANCH_OWNERSHIP_MISMATCH: ${workId}`);
|
|
1283
|
+
writeWorkHandle(ctx.controllerHome, {
|
|
1284
|
+
...handle,
|
|
1285
|
+
principalId: identity.principalId,
|
|
1286
|
+
sessionId: identity.sessionId,
|
|
1287
|
+
updatedAt: new Date().toISOString(),
|
|
1288
|
+
});
|
|
1289
|
+
resumeRetainedCancelledWorkContract(store, workId, {
|
|
1290
|
+
principalId: identity.principalId,
|
|
1291
|
+
controllerInstanceId: identity.controllerInstanceId,
|
|
1292
|
+
summary: 'Explicit current user re-authorized the same retained cancelled Work; exact managed checkout ownership was revalidated.',
|
|
1293
|
+
});
|
|
1294
|
+
return { reconstructedCheckout: false };
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
if (checkoutActive) throw new Error(`WORK_CANCELLED_RESUME_PRESERVATION_AMBIGUOUS: ${workId}`);
|
|
1298
|
+
if (!recordedCheckoutId || !recordedWorktree || !handle) throw new Error(`WORK_CANCELLED_RESUME_HANDLE_REQUIRED: ${workId}`);
|
|
1299
|
+
|
|
1300
|
+
const actualChangedPaths = work.scopeEvidence?.actualChangedPaths;
|
|
1301
|
+
if (!actualChangedPaths || actualChangedPaths.length !== 0
|
|
1302
|
+
|| work.checks.length !== 0
|
|
1303
|
+
|| work.checkRefs.length !== 0
|
|
1304
|
+
|| work.evidenceState !== 'none') {
|
|
1305
|
+
throw new Error(`WORK_CANCELLED_RESUME_ZERO_DELTA_PROOF_REQUIRED: ${workId}`);
|
|
1306
|
+
}
|
|
1307
|
+
const recordedBase = work.baseRevision?.trim();
|
|
1308
|
+
if (!recordedBase) throw new Error(`WORK_CANCELLED_RESUME_BASE_REVISION_REQUIRED: ${workId}`);
|
|
1309
|
+
const baseRevision = gitCommitAtRef(repository.canonicalRoot, recordedBase);
|
|
1310
|
+
if (!baseRevision) throw new Error(`WORK_CANCELLED_RESUME_BASE_REVISION_MISSING: ${workId}`);
|
|
1311
|
+
const handleBase = handle.baseCommit ? gitCommitAtRef(repository.canonicalRoot, handle.baseCommit) : undefined;
|
|
1312
|
+
const handleHead = handle.expectedHead ? gitCommitAtRef(repository.canonicalRoot, handle.expectedHead) : undefined;
|
|
1313
|
+
if ((handle.baseCommit && handleBase !== baseRevision) || (handle.expectedHead && handleHead !== baseRevision)) {
|
|
1314
|
+
throw new Error(`WORK_CANCELLED_RESUME_REVISION_AMBIGUOUS: ${workId}`);
|
|
1315
|
+
}
|
|
1316
|
+
if (handle.checkoutId !== recordedCheckoutId || resolve(handle.worktreePath) !== resolve(recordedWorktree)) {
|
|
1317
|
+
throw new Error(`WORK_CANCELLED_RESUME_CHECKOUT_OWNERSHIP_MISMATCH: ${workId}`);
|
|
1318
|
+
}
|
|
1319
|
+
if (handle.branch) {
|
|
1320
|
+
const historicalBranchHead = gitCommitAtRef(repository.canonicalRoot, `refs/heads/${handle.branch}`);
|
|
1321
|
+
if (historicalBranchHead && historicalBranchHead !== baseRevision) {
|
|
1322
|
+
throw new Error(`WORK_CANCELLED_RESUME_UNIQUE_COMMIT_CONFLICT: ${workId}`);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
if (worktreePresent) {
|
|
1326
|
+
const retainedSnapshot = gitWorktreeSnapshot(recordedWorktree);
|
|
1327
|
+
if (!retainedSnapshot || !retainedSnapshot.clean || retainedSnapshot.head !== baseRevision || (handle.branch && retainedSnapshot.branch !== handle.branch)) {
|
|
1328
|
+
throw new Error(`WORK_CANCELLED_RESUME_ZERO_DELTA_PHYSICAL_CONFLICT: ${workId}`);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
const branchName = `work/resume-${workId.replace(/[^A-Za-z0-9._-]+/g, '-').slice(0, 42)}-${baseRevision.slice(0, 12)}`;
|
|
1333
|
+
const existingResumeBranch = gitCommitAtRef(repository.canonicalRoot, `refs/heads/${branchName}`);
|
|
1334
|
+
if (existingResumeBranch && existingResumeBranch !== baseRevision) {
|
|
1335
|
+
throw new Error(`WORK_CANCELLED_RESUME_RECONSTRUCTION_BRANCH_CONFLICT: ${workId}`);
|
|
1336
|
+
}
|
|
1337
|
+
const workspace = ensureManagedWorkspace(ctx.controllerHome, repository, {
|
|
1338
|
+
requestId: `${workId}:explicit-user-resume:${baseRevision}`,
|
|
1339
|
+
title: `${work.objective} explicit user resume`,
|
|
1340
|
+
branchName,
|
|
1341
|
+
baseRef: baseRevision,
|
|
1342
|
+
prepareDependencies: args.needs_dependencies === true,
|
|
1343
|
+
});
|
|
1344
|
+
if (!workspace.managed || !workspace.checkoutId || !workspace.root || !workspace.branch) {
|
|
1345
|
+
throw new Error(`WORK_CANCELLED_RESUME_RECONSTRUCTION_FAILED: ${workId}`);
|
|
1346
|
+
}
|
|
1347
|
+
const refreshedRepository = listRepositories(ctx.controllerHome, { includeRemoved: true })
|
|
1348
|
+
.find((candidate) => candidate.repoId === repository.repoId);
|
|
1349
|
+
if (!refreshedRepository) throw new Error(`WORK_CANCELLED_RESUME_REPOSITORY_MISSING: ${workId}`);
|
|
1350
|
+
const reconstructed = selectRepositoryCheckout(refreshedRepository, workspace.checkoutId);
|
|
1351
|
+
const reconstructedStatus = repositoryGitStatus(reconstructed);
|
|
1352
|
+
if (!reconstructedStatus.clean || reconstructedStatus.head !== baseRevision || reconstructedStatus.branch !== workspace.branch) {
|
|
1353
|
+
throw new Error(`WORK_CANCELLED_RESUME_RECONSTRUCTION_REVISION_MISMATCH: ${workId}`);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
work = updateWorkContract(store, workId, {
|
|
1357
|
+
checkoutId: workspace.checkoutId,
|
|
1358
|
+
worktreeRef: workspace.root,
|
|
1359
|
+
baseRevision,
|
|
1360
|
+
controllerInstanceId: identity.controllerInstanceId,
|
|
1361
|
+
});
|
|
1362
|
+
const now = new Date().toISOString();
|
|
1363
|
+
handle = writeWorkHandle(ctx.controllerHome, {
|
|
1364
|
+
...handle,
|
|
1365
|
+
principalId: identity.principalId,
|
|
1366
|
+
sessionId: identity.sessionId,
|
|
1367
|
+
checkoutId: workspace.checkoutId,
|
|
1368
|
+
worktreePath: workspace.root,
|
|
1369
|
+
branch: workspace.branch,
|
|
1370
|
+
sourceCheckoutId: repository.activeCheckoutId,
|
|
1371
|
+
managedWorktree: true,
|
|
1372
|
+
baseCommit: baseRevision,
|
|
1373
|
+
deliveryBaseCommit: baseRevision,
|
|
1374
|
+
expectedHead: baseRevision,
|
|
1375
|
+
state: 'prepared',
|
|
1376
|
+
validatedInputFingerprint: undefined,
|
|
1377
|
+
failureReason: undefined,
|
|
1378
|
+
cleanupReceipt: undefined,
|
|
1379
|
+
finalization: { validation: 'pending', commit: 'pending', merge: 'pending', branchCleanup: 'pending', worktreeCleanup: 'pending' },
|
|
1380
|
+
updatedAt: now,
|
|
1381
|
+
});
|
|
1382
|
+
resumeRetainedCancelledWorkContract(store, workId, {
|
|
1383
|
+
principalId: identity.principalId,
|
|
1384
|
+
controllerInstanceId: identity.controllerInstanceId,
|
|
1385
|
+
summary: 'Explicit current user re-authorized the same cancelled Work; prior managed checkout was absent and a fresh isolated checkout was reconstructed at the exact zero-delta base revision.',
|
|
1386
|
+
checkoutId: workspace.checkoutId,
|
|
1387
|
+
worktreeRef: workspace.root,
|
|
1388
|
+
});
|
|
1389
|
+
return { reconstructedCheckout: true };
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1649
1392
|
function claimNewFacadeWork(
|
|
1650
1393
|
ctx: MultiRepositoryMcpToolContext,
|
|
1651
1394
|
repository: ReturnType<typeof selected>,
|
|
@@ -1698,7 +1441,8 @@ async function finalizeFacadeWorkHandle(
|
|
|
1698
1441
|
): Promise<CallToolResult | undefined> {
|
|
1699
1442
|
const workId = String(args.work_id ?? '').trim();
|
|
1700
1443
|
if (!workId) return undefined;
|
|
1701
|
-
|
|
1444
|
+
let handle = readWorkHandle(ctx.controllerHome, repository.repoId, workId)
|
|
1445
|
+
?? recoverTerminalWorkHandle(ctx.controllerHome, repository.repoId, workId);
|
|
1702
1446
|
if (!handle) return undefined;
|
|
1703
1447
|
const session = bindFacadeExecutionSession(ctx, repository, handle, args);
|
|
1704
1448
|
const targetBranch = typeof args.target_branch === 'string' && args.target_branch.trim()
|
|
@@ -1726,11 +1470,45 @@ async function finalizeFacadeWorkHandle(
|
|
|
1726
1470
|
const head = status.head ?? handle.expectedHead ?? handle.baseCommit;
|
|
1727
1471
|
const committedDelta = Boolean(head && handle.baseCommit && head !== handle.baseCommit);
|
|
1728
1472
|
const commit = typeof args.commit === 'boolean' ? args.commit : !status.clean;
|
|
1729
|
-
const
|
|
1473
|
+
const workContract = getWorkContract({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, workId);
|
|
1474
|
+
if (
|
|
1475
|
+
status.clean
|
|
1476
|
+
&& committedDelta
|
|
1477
|
+
&& commit === false
|
|
1478
|
+
&& (workContract?.allowedPaths.length ?? 0) > 0
|
|
1479
|
+
&& head
|
|
1480
|
+
&& handle.expectedHead
|
|
1481
|
+
&& head !== handle.expectedHead
|
|
1482
|
+
&& (handle.state === 'prepared' || handle.state === 'editing')
|
|
1483
|
+
) {
|
|
1484
|
+
const contract = getWorkContract({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, workId);
|
|
1485
|
+
if ((contract?.allowedPaths.length ?? 0) > 0 || (contract?.forbiddenPaths.length ?? 0) > 0) {
|
|
1486
|
+
const adopted = await callExecutionTool(ctx, 'work_prepare', {
|
|
1487
|
+
session_id: session.sessionId,
|
|
1488
|
+
repo_id: repository.repoId,
|
|
1489
|
+
checkout_id: handle.checkoutId,
|
|
1490
|
+
work_id: workId,
|
|
1491
|
+
expected_previous_head: handle.expectedHead,
|
|
1492
|
+
adopt_candidate_head: head,
|
|
1493
|
+
});
|
|
1494
|
+
if (!adopted || adopted.isError === true) return adopted;
|
|
1495
|
+
handle = readWorkHandle(ctx.controllerHome, repository.repoId, workId) ?? handle;
|
|
1496
|
+
} else if (handle.state === 'prepared') {
|
|
1497
|
+
// A Work without explicit path allow/deny constraints historically permits
|
|
1498
|
+
// repository-scoped committed progress. Preserve that contract while moving
|
|
1499
|
+
// the handle onto a legal lifecycle edge before the physical finalizer
|
|
1500
|
+
// validates and adopts the exact delivery HEAD.
|
|
1501
|
+
handle = transitionWorkHandle(ctx.controllerHome, handle, 'editing', { failureReason: undefined });
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1730
1504
|
const requestedOutcome = args.completion_outcome === 'completed_no_change' || args.completion_outcome === 'completed_changed'
|
|
1731
1505
|
? args.completion_outcome
|
|
1732
1506
|
: undefined;
|
|
1733
1507
|
const completionOutcome = requestedOutcome ?? (!commit && !committedDelta && status.clean ? 'completed_no_change' : 'completed_changed');
|
|
1508
|
+
// A proven no-change completion has nothing to merge. Defaulting merge=true
|
|
1509
|
+
// forced the generic Git delivery path to do unnecessary work and conflicted
|
|
1510
|
+
// with work_finalize's explicit no-change contract.
|
|
1511
|
+
const merge = typeof args.merge === 'boolean' ? args.merge : completionOutcome !== 'completed_no_change';
|
|
1734
1512
|
const explicitNoChangeEvidence = typeof args.no_change_evidence === 'string' ? args.no_change_evidence.trim() : '';
|
|
1735
1513
|
const noChangeEvidence = completionOutcome === 'completed_no_change'
|
|
1736
1514
|
? explicitNoChangeEvidence || `Validated Work ${workId} has no repository delta from base ${handle.baseCommit ?? 'unknown'} at clean HEAD ${head ?? 'unknown'}.`
|
|
@@ -1740,6 +1518,7 @@ async function finalizeFacadeWorkHandle(
|
|
|
1740
1518
|
commit,
|
|
1741
1519
|
merge,
|
|
1742
1520
|
no_ff: args.no_ff === true,
|
|
1521
|
+
remote_write: args.remote_write === true,
|
|
1743
1522
|
completion_outcome: completionOutcome,
|
|
1744
1523
|
...(noChangeEvidence ? { no_change_evidence: noChangeEvidence } : {}),
|
|
1745
1524
|
};
|
|
@@ -1796,27 +1575,6 @@ function selected(ctx: MultiRepositoryMcpToolContext, args: Record<string, unkno
|
|
|
1796
1575
|
});
|
|
1797
1576
|
}
|
|
1798
1577
|
|
|
1799
|
-
function schemaAwareWorkSubmitArguments(
|
|
1800
|
-
name: string,
|
|
1801
|
-
explicitArguments: Record<string, unknown>,
|
|
1802
|
-
repository: ReturnType<typeof selected>,
|
|
1803
|
-
definition: McpToolDefinition,
|
|
1804
|
-
timeoutMs: unknown,
|
|
1805
|
-
): Record<string, unknown> {
|
|
1806
|
-
const schema = definition.inputSchema as { properties?: Record<string, unknown> };
|
|
1807
|
-
const declared = new Set(Object.keys(schema.properties ?? {}));
|
|
1808
|
-
const candidates = repositoryScopedToolArgs(name, {
|
|
1809
|
-
...explicitArguments,
|
|
1810
|
-
...(declared.has('repo_id') ? { repo_id: repository.repoId } : {}),
|
|
1811
|
-
...(declared.has('timeout_ms') && typeof timeoutMs === 'number' ? { timeout_ms: timeoutMs } : {}),
|
|
1812
|
-
}, repository);
|
|
1813
|
-
const scoped: Record<string, unknown> = { ...explicitArguments };
|
|
1814
|
-
for (const [key, value] of Object.entries(candidates)) {
|
|
1815
|
-
if (Object.hasOwn(explicitArguments, key) || declared.has(key)) scoped[key] = value;
|
|
1816
|
-
}
|
|
1817
|
-
return scoped;
|
|
1818
|
-
}
|
|
1819
|
-
|
|
1820
1578
|
function pluginRepository(
|
|
1821
1579
|
ctx: MultiRepositoryMcpToolContext,
|
|
1822
1580
|
args: Record<string, unknown>,
|
|
@@ -1827,6 +1585,32 @@ function pluginRepository(
|
|
|
1827
1585
|
: selected(ctx, args);
|
|
1828
1586
|
}
|
|
1829
1587
|
|
|
1588
|
+
function definedArguments(value: Record<string, unknown>): Record<string, unknown> {
|
|
1589
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
function legacyIosPluginAction(
|
|
1593
|
+
ctx: MultiRepositoryMcpToolContext,
|
|
1594
|
+
legacyTool: string,
|
|
1595
|
+
args: Record<string, unknown>,
|
|
1596
|
+
actionId: string,
|
|
1597
|
+
actionArgs: Record<string, unknown>,
|
|
1598
|
+
): Promise<CallToolResult | undefined> {
|
|
1599
|
+
const repository = selected(ctx, args);
|
|
1600
|
+
const requestId = typeof args.request_id === 'string' && args.request_id.trim()
|
|
1601
|
+
? args.request_id.trim()
|
|
1602
|
+
: `legacy-${legacyTool}-${Date.now()}`;
|
|
1603
|
+
return callRuntimeTool(ctx, 'plugin_action_execute', {
|
|
1604
|
+
repo_id: repository.repoId,
|
|
1605
|
+
checkout_id: repository.activeCheckoutId,
|
|
1606
|
+
plugin_id: 'ios',
|
|
1607
|
+
action_id: actionId,
|
|
1608
|
+
request_id: requestId,
|
|
1609
|
+
arguments: definedArguments(actionArgs),
|
|
1610
|
+
...(args.confirm_authorization === true ? { confirm_authorization: true } : {}),
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1830
1614
|
function expectedRevision(args: Record<string, unknown>, key = 'expected_revision'): number | undefined {
|
|
1831
1615
|
return typeof args[key] === 'number' ? Math.trunc(args[key] as number) : undefined;
|
|
1832
1616
|
}
|
|
@@ -2447,14 +2231,10 @@ function managedProcessOperationDigest(
|
|
|
2447
2231
|
summary: terminal
|
|
2448
2232
|
? `Managed process ${handle.processId} completed with status ${handle.status}.`
|
|
2449
2233
|
: `Managed process ${handle.processId} is still ${handle.status}.`,
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
payload: { work_ref: handle.processId },
|
|
2455
|
-
risk: 'readonly',
|
|
2456
|
-
confidence: 'high',
|
|
2457
|
-
}],
|
|
2234
|
+
// A running Process is not itself a request to poll. The controller should
|
|
2235
|
+
// keep making independent progress, then join only at its real dependency
|
|
2236
|
+
// boundary through the Process lifecycle surface.
|
|
2237
|
+
suggestedNextActions: [],
|
|
2458
2238
|
};
|
|
2459
2239
|
}
|
|
2460
2240
|
|
|
@@ -2525,6 +2305,90 @@ async function runFacadeRepair(
|
|
|
2525
2305
|
: 'diagnose';
|
|
2526
2306
|
const dryRun = args.dry_run === undefined ? true : args.dry_run === true;
|
|
2527
2307
|
const elevatedRepair = args.destructive === true || args.remote_write === true || args.remote_effect === true;
|
|
2308
|
+
const planId = typeof args.plan_id === 'string' ? args.plan_id.trim() : '';
|
|
2309
|
+
const planStepId = typeof args.plan_step_id === 'string' ? args.plan_step_id.trim() : '';
|
|
2310
|
+
|
|
2311
|
+
if (planId || planStepId) {
|
|
2312
|
+
if (!planId || !planStepId) {
|
|
2313
|
+
const facade = buildFacadeResult({ status: 'blocked', summary: 'PLAN_STEP_REPAIR_CONTEXT_REQUIRED: plan_id and plan_step_id are both required.', data: { operation: repairOperation, dryRun, repaired: false } });
|
|
2314
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
2315
|
+
}
|
|
2316
|
+
const plan = getPlanContract(store, planId);
|
|
2317
|
+
const step = plan?.steps.find((candidate) => candidate.id === planStepId);
|
|
2318
|
+
if (!plan || !step) {
|
|
2319
|
+
const facade = buildFacadeResult({ status: 'not_found', summary: !plan ? `PlanContract ${planId} not found.` : `PLAN_STEP_NOT_FOUND: ${planStepId}`, data: { operation: repairOperation, dryRun, planId, planStepId, repaired: false } });
|
|
2320
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
2321
|
+
}
|
|
2322
|
+
if (!step.workId) {
|
|
2323
|
+
const facade = buildFacadeResult({ summary: `Plan step ${planId}/${planStepId} has no Work binding to repair.`, data: { operation: repairOperation, dryRun, planId, planStepId, repaired: false, repairRequired: false } });
|
|
2324
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
2325
|
+
}
|
|
2326
|
+
const boundWork = getWorkContract(store, step.workId);
|
|
2327
|
+
if (boundWork) {
|
|
2328
|
+
if (['completed', 'failed', 'cancelled'].includes(boundWork.status)) {
|
|
2329
|
+
if (repairOperation !== 'repair' || dryRun) {
|
|
2330
|
+
const facade = buildFacadeResult({
|
|
2331
|
+
summary: `PLAN_STEP_TERMINAL_WORK_RECONCILIABLE: ${planId}/${planStepId} is bound to existing terminal Work ${boundWork.workId}. Explicit repair can project that exact Work without creating a replacement.`,
|
|
2332
|
+
data: { operation: repairOperation, dryRun, planId, planStepId, boundWorkId: boundWork.workId, repaired: false, repairRequired: true, reusedExistingWork: true },
|
|
2333
|
+
suggestedNextActions: [{ label: 'Project existing terminal Work', tool: 'rh_work', operation: 'repair', payload: { plan_id: planId, plan_step_id: planStepId, repair_operation: 'repair', dry_run: false }, risk: 'workspace_write', confidence: 'high' }],
|
|
2334
|
+
});
|
|
2335
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
2336
|
+
}
|
|
2337
|
+
try {
|
|
2338
|
+
const reconciledPlan = completePlanStepForWork(store, { planId, stepId: planStepId, work: boundWork });
|
|
2339
|
+
const facade = buildFacadeResult({
|
|
2340
|
+
summary: `Reconciled Plan step ${planId}/${planStepId} from its existing terminal Work ${boundWork.workId}; no replacement Work was created.`,
|
|
2341
|
+
data: { operation: repairOperation, dryRun: false, plan: summarizePlanContract(reconciledPlan), boundWorkId: boundWork.workId, repaired: true, reusedExistingWork: true },
|
|
2342
|
+
});
|
|
2343
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
2344
|
+
} catch (error) {
|
|
2345
|
+
const facade = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'PLAN_STEP_TERMINAL_WORK_RECONCILIATION_FAILED', data: { operation: repairOperation, dryRun: false, planId, planStepId, boundWorkId: boundWork.workId, repaired: false } });
|
|
2346
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
const facade = buildFacadeResult({
|
|
2350
|
+
status: 'blocked',
|
|
2351
|
+
summary: `PLAN_STEP_BOUND_WORK_STILL_EXISTS: ${planId}/${planStepId} is bound to active Work ${boundWork.workId}; continue that exact Work instead of repairing or replacing it.`,
|
|
2352
|
+
data: { operation: repairOperation, dryRun, planId, planStepId, boundWorkId: boundWork.workId, repaired: false, repairRequired: false },
|
|
2353
|
+
suggestedNextActions: [{ label: 'Continue existing Work', tool: 'rh_work', operation: 'continue', payload: { work_id: boundWork.workId }, risk: 'readonly', confidence: 'high' }],
|
|
2354
|
+
});
|
|
2355
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
2356
|
+
}
|
|
2357
|
+
const conflicting = listWorkContracts({ ...store, status: 'active', limit: 200 })
|
|
2358
|
+
.filter((candidate) => candidate.planId === planId && candidate.planStepId === planStepId && candidate.workId !== step.workId);
|
|
2359
|
+
if (conflicting.length > 0) {
|
|
2360
|
+
const facade = buildFacadeResult({
|
|
2361
|
+
status: 'blocked',
|
|
2362
|
+
summary: `PLAN_STEP_REPAIR_CONFLICT: ${planId}/${planStepId} is bound to missing Work ${step.workId}, but ${conflicting.length} other active Work record(s) claim the same step. Resolve the conflicting authority before changing the Plan binding.`,
|
|
2363
|
+
data: { operation: repairOperation, dryRun, planId, planStepId, boundWorkId: step.workId, conflictingWorkIds: conflicting.map((candidate) => candidate.workId), repaired: false },
|
|
2364
|
+
});
|
|
2365
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
2366
|
+
}
|
|
2367
|
+
if (repairOperation !== 'repair' || dryRun) {
|
|
2368
|
+
const facade = buildFacadeResult({
|
|
2369
|
+
summary: `PLAN_STEP_DANGLING_WORK_BINDING: ${planId}/${planStepId} points to missing Work ${step.workId}. Exact repair is available and will clear only this unchanged ghost binding.`,
|
|
2370
|
+
data: { operation: repairOperation, dryRun, planId, planStepId, boundWorkId: step.workId, repaired: false, repairRequired: true },
|
|
2371
|
+
suggestedNextActions: [{ label: 'Repair exact dangling binding', tool: 'rh_work', operation: 'repair', payload: { plan_id: planId, plan_step_id: planStepId, repair_operation: 'repair', dry_run: false }, risk: 'workspace_write', confidence: 'high' }],
|
|
2372
|
+
});
|
|
2373
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
2374
|
+
}
|
|
2375
|
+
try {
|
|
2376
|
+
const repairedPlan = repairDanglingPlanStepWorkBinding(store, {
|
|
2377
|
+
planId,
|
|
2378
|
+
stepId: planStepId,
|
|
2379
|
+
expectedWorkId: step.workId,
|
|
2380
|
+
reason: 'Explicit Controller repair confirmed that the exact bound Work record is absent and no other active primary Work claims this Plan step.',
|
|
2381
|
+
});
|
|
2382
|
+
const facade = buildFacadeResult({
|
|
2383
|
+
summary: `Repaired dangling Plan step binding ${planId}/${planStepId}; ${step.workId} was cleared without creating a replacement Work.`,
|
|
2384
|
+
data: { operation: repairOperation, dryRun: false, plan: summarizePlanContract(repairedPlan), boundWorkId: step.workId, repaired: true, replacementWorkCreated: false },
|
|
2385
|
+
});
|
|
2386
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
2387
|
+
} catch (error) {
|
|
2388
|
+
const facade = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'PLAN_STEP_DANGLING_WORK_REPAIR_FAILED', data: { operation: repairOperation, dryRun: false, planId, planStepId, boundWorkId: step.workId, repaired: false } });
|
|
2389
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2528
2392
|
|
|
2529
2393
|
// The self-healing facade is a policy/planning surface; the authoritative
|
|
2530
2394
|
// maintenance executor owns mutations. Execute it here only for an explicit,
|
|
@@ -2632,6 +2496,144 @@ async function runFacadeRepair(
|
|
|
2632
2496
|
return result(facade as unknown as Record<string, unknown>, facade.status === 'blocked' || facade.status === 'approval_required' || facade.status === 'failed');
|
|
2633
2497
|
}
|
|
2634
2498
|
|
|
2499
|
+
function reconcileTerminalFacadeWorkVerifications(
|
|
2500
|
+
ctx: MultiRepositoryMcpToolContext,
|
|
2501
|
+
repository: ReturnType<typeof selected>,
|
|
2502
|
+
workId: string,
|
|
2503
|
+
): { sourceRevision?: string; workspaceFingerprint?: string; workspaceChangedPaths?: string[]; reconciledProcessIds: string[] } {
|
|
2504
|
+
const store = { controllerHome: ctx.controllerHome, repoId: repository.repoId };
|
|
2505
|
+
const workContract = getWorkContract(store, workId);
|
|
2506
|
+
if (!workContract || workContract.completionReceipt) return { reconciledProcessIds: [] };
|
|
2507
|
+
|
|
2508
|
+
let verificationRepository: ReturnType<typeof selectRepositoryCheckout>;
|
|
2509
|
+
try {
|
|
2510
|
+
verificationRepository = workContract.checkoutId
|
|
2511
|
+
? selectRepositoryCheckout(repository, workContract.checkoutId, { allowArchived: true })
|
|
2512
|
+
: repository;
|
|
2513
|
+
} catch {
|
|
2514
|
+
return { reconciledProcessIds: [] };
|
|
2515
|
+
}
|
|
2516
|
+
const verificationStatus = repositoryGitStatus(verificationRepository);
|
|
2517
|
+
const sourceRevision = verificationStatus.head ?? undefined;
|
|
2518
|
+
if (!sourceRevision) return { reconciledProcessIds: [] };
|
|
2519
|
+
const verificationHandle = readWorkHandle(ctx.controllerHome, repository.repoId, workId);
|
|
2520
|
+
const deliveryBaseRevision = verificationHandle
|
|
2521
|
+
? workDeliveryBaseRevision(verificationHandle)
|
|
2522
|
+
: workContract.baseRevision;
|
|
2523
|
+
const committedPaths = deliveryBaseRevision
|
|
2524
|
+
? workChangedPaths(verificationRepository.canonicalRoot, deliveryBaseRevision, sourceRevision)
|
|
2525
|
+
: [];
|
|
2526
|
+
const workspaceChangedPaths = [...new Set([
|
|
2527
|
+
...committedPaths,
|
|
2528
|
+
...verificationStatus.staged,
|
|
2529
|
+
...verificationStatus.unstaged,
|
|
2530
|
+
...verificationStatus.untracked,
|
|
2531
|
+
])].sort();
|
|
2532
|
+
const workspaceFingerprint = workspaceValidationFingerprint(verificationRepository.canonicalRoot, verificationStatus);
|
|
2533
|
+
const availableChecks = listControllerChecks(repository.canonicalRoot);
|
|
2534
|
+
const workloopCtx = {
|
|
2535
|
+
workStore: store,
|
|
2536
|
+
handoffStore: store,
|
|
2537
|
+
repoId: repository.repoId,
|
|
2538
|
+
availableChecks,
|
|
2539
|
+
};
|
|
2540
|
+
const seenChecks = new Set<string>();
|
|
2541
|
+
const reconciledProcessIds: string[] = [];
|
|
2542
|
+
const candidates = listProcessRecords(ctx.controllerHome, repository.repoId, 500)
|
|
2543
|
+
.filter((record) => (
|
|
2544
|
+
record.workId === workId
|
|
2545
|
+
&& record.checkoutId === verificationRepository.activeCheckoutId
|
|
2546
|
+
&& !isManagedProcessActive(record)
|
|
2547
|
+
&& record.origin?.workVerificationSnapshot === true
|
|
2548
|
+
&& typeof record.origin?.checkId === 'string'
|
|
2549
|
+
&& typeof record.origin?.requestSemanticFingerprint === 'string'
|
|
2550
|
+
));
|
|
2551
|
+
|
|
2552
|
+
for (const record of candidates) {
|
|
2553
|
+
const checkId = record.origin?.checkId?.trim() ?? '';
|
|
2554
|
+
if (!checkId || seenChecks.has(checkId)) continue;
|
|
2555
|
+
seenChecks.add(checkId);
|
|
2556
|
+
const classified = classifyVerificationOutcome({ checkId, available: availableChecks });
|
|
2557
|
+
if (classified.outcome === 'invalid_check_id' || !classified.normalizedCheckId) continue;
|
|
2558
|
+
const normalizedCheckId = classified.normalizedCheckId;
|
|
2559
|
+
const requestedChecks = workContract.checks.length ? workContract.checks : [normalizedCheckId];
|
|
2560
|
+
const currentFingerprint = verificationInputFingerprint({
|
|
2561
|
+
sourceRevision,
|
|
2562
|
+
workspaceFingerprint,
|
|
2563
|
+
checkId: normalizedCheckId,
|
|
2564
|
+
requestedChecks,
|
|
2565
|
+
});
|
|
2566
|
+
if (record.origin?.requestSemanticFingerprint !== currentFingerprint || !record.checkExecution) continue;
|
|
2567
|
+
|
|
2568
|
+
try {
|
|
2569
|
+
const receipt = processCheckCompletionReceipt(record, {
|
|
2570
|
+
repoId: verificationRepository.repoId,
|
|
2571
|
+
checkoutId: verificationRepository.activeCheckoutId,
|
|
2572
|
+
workId,
|
|
2573
|
+
checkId: normalizedCheckId,
|
|
2574
|
+
processId: record.processId,
|
|
2575
|
+
requestId: record.origin?.requestId,
|
|
2576
|
+
checkExecution: {
|
|
2577
|
+
cacheKey: record.checkExecution.cacheKey,
|
|
2578
|
+
revision: record.checkExecution.revision,
|
|
2579
|
+
definitionDigest: record.checkExecution.definitionDigest,
|
|
2580
|
+
environmentFingerprint: record.checkExecution.environmentFingerprint,
|
|
2581
|
+
timeoutMs: record.checkExecution.timeoutMs,
|
|
2582
|
+
scopeKey: record.checkExecution.scopeKey,
|
|
2583
|
+
},
|
|
2584
|
+
});
|
|
2585
|
+
const latestContract = getWorkContract(store, workId);
|
|
2586
|
+
if (latestContract?.checkRefs.some((entry) => entry.receipt?.receiptId === receipt.receiptId)) continue;
|
|
2587
|
+
|
|
2588
|
+
const structuredCheckResult = readPersistedCheckResultReceipt(record.origin?.checkResultReceiptPath);
|
|
2589
|
+
const structuredResultMatchesProcess = Boolean(
|
|
2590
|
+
structuredCheckResult
|
|
2591
|
+
&& structuredCheckResult.checkId === normalizedCheckId
|
|
2592
|
+
&& structuredCheckResult.cacheKey === record.checkExecution.cacheKey,
|
|
2593
|
+
);
|
|
2594
|
+
const legacyEvidence = record.origin?.checkResultReceiptPath
|
|
2595
|
+
? undefined
|
|
2596
|
+
: readLatestControllerCheckEvidence(verificationRepository.canonicalRoot, normalizedCheckId);
|
|
2597
|
+
const legacyEvidenceMatchesProcess = Boolean(
|
|
2598
|
+
legacyEvidence?.cacheKey
|
|
2599
|
+
&& legacyEvidence.cacheKey === record.checkExecution.cacheKey,
|
|
2600
|
+
);
|
|
2601
|
+
const evidenceState = classifyTerminalCheckEvidence({
|
|
2602
|
+
processError: record.error,
|
|
2603
|
+
structuredPresent: Boolean(structuredCheckResult),
|
|
2604
|
+
structuredMatches: structuredResultMatchesProcess,
|
|
2605
|
+
legacyPresent: Boolean(legacyEvidence),
|
|
2606
|
+
legacyMatches: legacyEvidenceMatchesProcess,
|
|
2607
|
+
});
|
|
2608
|
+
const failureClass = structuredResultMatchesProcess
|
|
2609
|
+
? structuredCheckResult?.failureClass
|
|
2610
|
+
: legacyEvidenceMatchesProcess ? legacyEvidence?.failureClass : undefined;
|
|
2611
|
+
const infrastructureFailed = receipt.timedOut
|
|
2612
|
+
|| receipt.cancelled
|
|
2613
|
+
|| evidenceState.state !== 'matched'
|
|
2614
|
+
|| (!receipt.ok && failureClass !== 'acceptance_failure');
|
|
2615
|
+
const checkFailed = !receipt.ok && !infrastructureFailed;
|
|
2616
|
+
verifyGoalWorkloop(workloopCtx, {
|
|
2617
|
+
workId,
|
|
2618
|
+
checkId: normalizedCheckId,
|
|
2619
|
+
sourceRevision,
|
|
2620
|
+
workspaceFingerprint,
|
|
2621
|
+
verificationInputFingerprint: currentFingerprint,
|
|
2622
|
+
commandFingerprint: commandFingerprint(normalizedCheckId, receipt.commandId),
|
|
2623
|
+
receipt,
|
|
2624
|
+
infrastructureFailed,
|
|
2625
|
+
checkFailed,
|
|
2626
|
+
});
|
|
2627
|
+
reconciledProcessIds.push(record.processId);
|
|
2628
|
+
} catch {
|
|
2629
|
+
// Exact receipt/process identity is mandatory. Any malformed, stale, or
|
|
2630
|
+
// mismatched terminal Process remains non-authoritative and is ignored.
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
return { sourceRevision, workspaceFingerprint, workspaceChangedPaths, reconciledProcessIds };
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2635
2637
|
async function runFacadeVerify(
|
|
2636
2638
|
ctx: MultiRepositoryMcpToolContext,
|
|
2637
2639
|
repository: ReturnType<typeof selected>,
|
|
@@ -2646,7 +2648,30 @@ async function runFacadeVerify(
|
|
|
2646
2648
|
availableChecks: checks,
|
|
2647
2649
|
};
|
|
2648
2650
|
const workId = typeof args.work_id === 'string' ? args.work_id : '';
|
|
2649
|
-
const checkId = String(args.check_id ?? args.checkId ?? '');
|
|
2651
|
+
const checkId = String(args.check_id ?? args.checkId ?? '').trim();
|
|
2652
|
+
if (!checkId) {
|
|
2653
|
+
return result(buildFacadeResult({
|
|
2654
|
+
status: 'blocked',
|
|
2655
|
+
summary: 'rh_work verify requires a registered check_id.',
|
|
2656
|
+
data: {
|
|
2657
|
+
verification: {
|
|
2658
|
+
outcome: 'check_id_required',
|
|
2659
|
+
isAcceptanceFailure: false,
|
|
2660
|
+
isInfrastructureIssue: true,
|
|
2661
|
+
doesNotRequestTaskChanges: true,
|
|
2662
|
+
},
|
|
2663
|
+
registeredCheckCount: checks.length,
|
|
2664
|
+
},
|
|
2665
|
+
warnings: ['CHECK_ID_REQUIRED: pass check_id for one registered repository check.'],
|
|
2666
|
+
suggestedNextActions: normalizeCheckIds(checks.slice(0, 3).map((check) => check.id), checks).suggestedNextActions,
|
|
2667
|
+
}) as unknown as Record<string, unknown>, true);
|
|
2668
|
+
}
|
|
2669
|
+
const workContract = workId ? getWorkContract(store, workId) : undefined;
|
|
2670
|
+
if (workId && (!workContract || workContract.status === 'completed' || workContract.status === 'cancelled' || workContract.status === 'failed')) {
|
|
2671
|
+
const facade = verifyGoalWorkloop(workloopCtx, { workId, checkId });
|
|
2672
|
+
return result(facade as unknown as Record<string, unknown>, facade.status === 'failed');
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2650
2675
|
const classified = classifyVerificationOutcome({
|
|
2651
2676
|
checkId,
|
|
2652
2677
|
available: checks,
|
|
@@ -2708,12 +2733,39 @@ async function runFacadeVerify(
|
|
|
2708
2733
|
const requestId = typeof args.request_id === 'string' && args.request_id.trim()
|
|
2709
2734
|
? args.request_id.trim()
|
|
2710
2735
|
: undefined;
|
|
2711
|
-
const workContract = workId ? getWorkContract(store, workId) : undefined;
|
|
2712
|
-
if (workId && !workContract) throw new Error(`WORK_NOT_FOUND: ${workId}`);
|
|
2713
2736
|
const verificationRepository = workContract?.checkoutId
|
|
2714
2737
|
? selectRepositoryCheckout(repository, workContract.checkoutId, { allowArchived: true })
|
|
2715
2738
|
: repository;
|
|
2716
|
-
const
|
|
2739
|
+
const verificationStatus = repositoryGitStatus(verificationRepository);
|
|
2740
|
+
const observedGitHead = verificationStatus.head;
|
|
2741
|
+
const workspaceFingerprint = workspaceValidationFingerprint(verificationRepository.canonicalRoot, verificationStatus);
|
|
2742
|
+
const requestedChecks = workContract?.checks.length ? workContract.checks : [normalizedCheckId];
|
|
2743
|
+
const verificationRequestFingerprint = observedGitHead ? verificationInputFingerprint({
|
|
2744
|
+
sourceRevision: observedGitHead,
|
|
2745
|
+
workspaceFingerprint,
|
|
2746
|
+
checkId: normalizedCheckId,
|
|
2747
|
+
requestedChecks,
|
|
2748
|
+
}) : undefined;
|
|
2749
|
+
const registeredCheck = checks.find((entry) => entry.id === normalizedCheckId);
|
|
2750
|
+
const durableClassCheck = checkRequiresDurableWorkflow(normalizedCheckId, registeredCheck);
|
|
2751
|
+
let allowDurableCheckExecution = false;
|
|
2752
|
+
if (durableClassCheck && workContract?.checks.includes(normalizedCheckId) && !workContract.completionReceipt && workContract.status === 'running') {
|
|
2753
|
+
try {
|
|
2754
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
2755
|
+
const owner = getControllerSession(store, workId);
|
|
2756
|
+
const ownerPrincipal = owner?.principalId?.trim() || owner?.controllerId;
|
|
2757
|
+
allowDurableCheckExecution = Boolean(
|
|
2758
|
+
owner
|
|
2759
|
+
&& owner.controllerId === identity.controllerId
|
|
2760
|
+
&& ownerPrincipal === identity.principalId
|
|
2761
|
+
&& owner.controllerInstanceId === identity.controllerInstanceId,
|
|
2762
|
+
);
|
|
2763
|
+
} catch {
|
|
2764
|
+
// Durable-class checks remain deferred when the authenticated claim cannot
|
|
2765
|
+
// be proven exactly. The lower Process Runtime never infers authority.
|
|
2766
|
+
allowDurableCheckExecution = false;
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2717
2769
|
const executed = await runPersistedCheckViaProcessRuntime({
|
|
2718
2770
|
controllerHome: ctx.controllerHome,
|
|
2719
2771
|
repoId: verificationRepository.repoId,
|
|
@@ -2724,8 +2776,13 @@ async function runFacadeVerify(
|
|
|
2724
2776
|
timeoutMs: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
|
|
2725
2777
|
// rh_work.verify is a control-plane continuation primitive: callers should
|
|
2726
2778
|
// be able to keep working and later reattach to the exact same Process.
|
|
2779
|
+
// Absorb brief shared build-cache/resource contention during admission so
|
|
2780
|
+
// a lifecycle verification does not turn a few seconds of contention into
|
|
2781
|
+
// a terminal failed Check that the controller must manually retry.
|
|
2727
2782
|
interactiveWaitMs: 0,
|
|
2783
|
+
leaseWaitMs: RH_WORK_VERIFY_LEASE_WAIT_MS,
|
|
2728
2784
|
requestId,
|
|
2785
|
+
requestSemanticFingerprint: verificationRequestFingerprint,
|
|
2729
2786
|
workId: workId || undefined,
|
|
2730
2787
|
commandId: requestId,
|
|
2731
2788
|
verificationSnapshot: workContract ? {
|
|
@@ -2733,6 +2790,7 @@ async function runFacadeVerify(
|
|
|
2733
2790
|
allowedPaths: workContract.allowedPaths,
|
|
2734
2791
|
forbiddenPaths: workContract.forbiddenPaths,
|
|
2735
2792
|
} : undefined,
|
|
2793
|
+
allowDurableCheckExecution,
|
|
2736
2794
|
});
|
|
2737
2795
|
|
|
2738
2796
|
if (executed.mode === 'durable') {
|
|
@@ -2825,7 +2883,14 @@ async function runFacadeVerify(
|
|
|
2825
2883
|
&& record.checkExecution?.cacheKey
|
|
2826
2884
|
&& legacyEvidence.cacheKey === record.checkExecution.cacheKey,
|
|
2827
2885
|
);
|
|
2828
|
-
const
|
|
2886
|
+
const evidenceState = classifyTerminalCheckEvidence({
|
|
2887
|
+
processError: record.error,
|
|
2888
|
+
structuredPresent: Boolean(structuredCheckResult),
|
|
2889
|
+
structuredMatches: structuredResultMatchesProcess,
|
|
2890
|
+
legacyPresent: Boolean(legacyEvidence),
|
|
2891
|
+
legacyMatches: legacyEvidenceMatchesProcess,
|
|
2892
|
+
});
|
|
2893
|
+
const resultMatchesProcess = evidenceState.state === 'matched';
|
|
2829
2894
|
const failureClass = structuredResultMatchesProcess
|
|
2830
2895
|
? structuredCheckResult?.failureClass
|
|
2831
2896
|
: legacyEvidenceMatchesProcess ? legacyEvidence?.failureClass : undefined;
|
|
@@ -2858,12 +2923,21 @@ async function runFacadeVerify(
|
|
|
2858
2923
|
checkResultReceiptId: structuredCheckResult?.receiptId,
|
|
2859
2924
|
verificationIsolation: record.origin?.workVerificationSnapshot ? 'work_snapshot' : 'shared_checkout',
|
|
2860
2925
|
boundedStatus,
|
|
2926
|
+
evidenceState: evidenceState.state,
|
|
2927
|
+
...(evidenceState.infrastructureReason ? { infrastructureReason: evidenceState.infrastructureReason } : {}),
|
|
2928
|
+
...(record.error?.code ? { processErrorCode: record.error.code } : {}),
|
|
2861
2929
|
};
|
|
2862
2930
|
|
|
2863
2931
|
if (workId) {
|
|
2932
|
+
const sourceRevision = observedGitHead ?? undefined;
|
|
2864
2933
|
const facade = verifyGoalWorkloop(workloopCtx, {
|
|
2865
2934
|
workId,
|
|
2866
2935
|
checkId: normalizedCheckId,
|
|
2936
|
+
sourceRevision,
|
|
2937
|
+
workspaceFingerprint,
|
|
2938
|
+
verificationInputFingerprint: sourceRevision ? verificationRequestFingerprint : undefined,
|
|
2939
|
+
commandFingerprint: commandFingerprint(normalizedCheckId, receipt.commandId),
|
|
2940
|
+
receipt,
|
|
2867
2941
|
infrastructureFailed,
|
|
2868
2942
|
checkFailed,
|
|
2869
2943
|
});
|
|
@@ -2878,7 +2952,7 @@ async function runFacadeVerify(
|
|
|
2878
2952
|
},
|
|
2879
2953
|
},
|
|
2880
2954
|
warnings: infrastructureFailed
|
|
2881
|
-
? [...facade.warnings,
|
|
2955
|
+
? [...facade.warnings, evidenceState.warning ?? 'infrastructure_failure is distinct from acceptance failure']
|
|
2882
2956
|
: facade.warnings,
|
|
2883
2957
|
} as unknown as Record<string, unknown>, facade.status === 'failed');
|
|
2884
2958
|
}
|
|
@@ -2892,7 +2966,7 @@ async function runFacadeVerify(
|
|
|
2892
2966
|
: `Check ${normalizedCheckId} failed acceptance.`,
|
|
2893
2967
|
data: { verification: commonVerification },
|
|
2894
2968
|
warnings: infrastructureFailed
|
|
2895
|
-
? [
|
|
2969
|
+
? [evidenceState.warning ?? 'infrastructure_failure is distinct from acceptance failure']
|
|
2896
2970
|
: [],
|
|
2897
2971
|
rawAvailable: false,
|
|
2898
2972
|
}) as unknown as Record<string, unknown>, checkFailed);
|
|
@@ -2957,6 +3031,90 @@ export function runtimeSourceSnapshotStatus(
|
|
|
2957
3031
|
};
|
|
2958
3032
|
}
|
|
2959
3033
|
|
|
3034
|
+
export function repositoryExecutionReadiness(
|
|
3035
|
+
repoRoot: string,
|
|
3036
|
+
availableChecks: ReturnType<typeof listControllerChecks>,
|
|
3037
|
+
requestedCheckIds: string[] = [],
|
|
3038
|
+
schedulingScope: { repoId?: string; checkoutId?: string } = {},
|
|
3039
|
+
): Record<string, unknown> {
|
|
3040
|
+
const git = gitSnapshot(repoRoot);
|
|
3041
|
+
const registeredCheckIds = availableChecks.map((check) => check.id);
|
|
3042
|
+
const normalizedChecks = normalizeCheckIds(requestedCheckIds, availableChecks);
|
|
3043
|
+
const hasPackageJson = existsSync(join(repoRoot, 'package.json'));
|
|
3044
|
+
const nodeModulesReady = !hasPackageJson || existsSync(join(repoRoot, 'node_modules'));
|
|
3045
|
+
const lockCandidates = [
|
|
3046
|
+
['bun', 'bun.lock'],
|
|
3047
|
+
['bun', 'bun.lockb'],
|
|
3048
|
+
['pnpm', 'pnpm-lock.yaml'],
|
|
3049
|
+
['npm', 'package-lock.json'],
|
|
3050
|
+
['yarn', 'yarn.lock'],
|
|
3051
|
+
] as const;
|
|
3052
|
+
const detectedLock = lockCandidates.find(([, path]) => existsSync(join(repoRoot, path)));
|
|
3053
|
+
const packageManager = detectedLock?.[0];
|
|
3054
|
+
const bootstrapCommand = hasPackageJson && !nodeModulesReady
|
|
3055
|
+
? packageManager === 'bun' ? ['bun', 'install', '--frozen-lockfile']
|
|
3056
|
+
: packageManager === 'pnpm' ? ['pnpm', 'install', '--frozen-lockfile']
|
|
3057
|
+
: packageManager === 'npm' ? ['npm', 'ci']
|
|
3058
|
+
: packageManager === 'yarn' ? ['yarn', 'install', '--frozen-lockfile']
|
|
3059
|
+
: undefined
|
|
3060
|
+
: undefined;
|
|
3061
|
+
const hasPythonManifest = existsSync(join(repoRoot, 'pyproject.toml'))
|
|
3062
|
+
|| existsSync(join(repoRoot, 'requirements.txt'))
|
|
3063
|
+
|| existsSync(join(repoRoot, 'requirements-dev.txt'));
|
|
3064
|
+
const localPythonReady = !hasPythonManifest
|
|
3065
|
+
|| existsSync(join(repoRoot, '.venv', 'bin', 'python'))
|
|
3066
|
+
|| existsSync(join(repoRoot, '.venv', 'Scripts', 'python.exe'));
|
|
3067
|
+
const checkScheduling = buildCheckExecutionSchedule({
|
|
3068
|
+
checks: availableChecks,
|
|
3069
|
+
requestedCheckIds,
|
|
3070
|
+
repoId: schedulingScope.repoId?.trim() || 'selected-repository',
|
|
3071
|
+
checkoutId: schedulingScope.checkoutId?.trim() || 'active',
|
|
3072
|
+
});
|
|
3073
|
+
const blockers = [
|
|
3074
|
+
...(!nodeModulesReady ? [{ code: 'NODE_DEPENDENCIES_MISSING', message: 'package.json is present but node_modules is not materialized in this checkout.' }] : []),
|
|
3075
|
+
...normalizedChecks.invalidCheckIds.map((checkId) => ({ code: 'CHECK_NOT_REGISTERED', message: `Requested check is not registered: ${checkId}`, checkId })),
|
|
3076
|
+
];
|
|
3077
|
+
return {
|
|
3078
|
+
readyForFocusedExecution: blockers.length === 0,
|
|
3079
|
+
git: { head: git.head, branch: git.branch, dirty: git.dirty },
|
|
3080
|
+
checks: {
|
|
3081
|
+
registeredCount: registeredCheckIds.length,
|
|
3082
|
+
registeredCheckIds: registeredCheckIds.slice(0, 80),
|
|
3083
|
+
requestedCheckIds: requestedCheckIds.slice(0, 40),
|
|
3084
|
+
normalized: normalizedChecks,
|
|
3085
|
+
},
|
|
3086
|
+
checkScheduling: {
|
|
3087
|
+
waveCount: checkScheduling.waves.length,
|
|
3088
|
+
maxParallel: checkScheduling.maxParallel,
|
|
3089
|
+
waveSummaries: checkScheduling.waves.map((wave) => `wave ${wave.wave}: ${wave.checkIds.join(', ')}`),
|
|
3090
|
+
conflictSummaries: checkScheduling.conflicts.map((conflict) => {
|
|
3091
|
+
const resources = [...new Set(conflict.resources.flatMap(({ left, right }) => [left.resourceKey, right.resourceKey]))];
|
|
3092
|
+
return `${conflict.leftCheckId} <> ${conflict.rightCheckId}: ${resources.join(', ')}`;
|
|
3093
|
+
}),
|
|
3094
|
+
invalidCheckIds: checkScheduling.invalidCheckIds,
|
|
3095
|
+
guidance: checkScheduling.guidance,
|
|
3096
|
+
},
|
|
3097
|
+
dependencies: {
|
|
3098
|
+
node: {
|
|
3099
|
+
applicable: hasPackageJson,
|
|
3100
|
+
ready: nodeModulesReady,
|
|
3101
|
+
packageManager: packageManager ?? null,
|
|
3102
|
+
lockfile: detectedLock?.[1] ?? null,
|
|
3103
|
+
...(bootstrapCommand ? { bootstrapCommand } : {}),
|
|
3104
|
+
},
|
|
3105
|
+
python: {
|
|
3106
|
+
applicable: hasPythonManifest,
|
|
3107
|
+
localVirtualEnvReady: localPythonReady,
|
|
3108
|
+
advisoryOnly: true,
|
|
3109
|
+
},
|
|
3110
|
+
},
|
|
3111
|
+
blockers,
|
|
3112
|
+
guidance: bootstrapCommand
|
|
3113
|
+
? [`Materialize checkout dependencies before tests/builds: ${bootstrapCommand.join(' ')}`]
|
|
3114
|
+
: [],
|
|
3115
|
+
};
|
|
3116
|
+
}
|
|
3117
|
+
|
|
2960
3118
|
export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name: string, args: Record<string, unknown>): Promise<CallToolResult | undefined> {
|
|
2961
3119
|
try {
|
|
2962
3120
|
switch (name) {
|
|
@@ -2970,16 +3128,167 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
2970
3128
|
if (operation === 'repair') {
|
|
2971
3129
|
return await runFacadeRepair(ctx, repository, args);
|
|
2972
3130
|
}
|
|
2973
|
-
const
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
3131
|
+
const detailLevel = args.detail_level === 'detail' ? 'detail' : 'summary';
|
|
3132
|
+
if (detailLevel === 'summary') {
|
|
3133
|
+
const startedAt = performance.now();
|
|
3134
|
+
const observation = observeRuntimeStatus(ctx.controllerHome);
|
|
3135
|
+
// Summary answers only whether this repository can work now. Reuse one
|
|
3136
|
+
// porcelain-v2 sample for branch/HEAD/dirty and avoid the full Git
|
|
3137
|
+
// status/diff-stat, access-policy, and inventory construction paths.
|
|
3138
|
+
const repositoryIdentity = cachedGitIdentity(repository.canonicalRoot);
|
|
3139
|
+
const runtimeGeneration = readRuntimeGeneration(ctx.controllerHome);
|
|
3140
|
+
const runtimeSource = runtimeSourceSnapshotStatus(
|
|
3141
|
+
runtimeGeneration?.source,
|
|
3142
|
+
ctx.runtimeSourceRoot,
|
|
3143
|
+
);
|
|
3144
|
+
const sourceSnapshotStale = runtimeSource.restartRequired;
|
|
3145
|
+
const toolset = await import('../../../cli/mcp/toolset');
|
|
3146
|
+
const exposure = toolset.controllerToolSurfaceStatus(ctx);
|
|
3147
|
+
const toolSurfaceReady = exposure.ready && exposure.missingToolNames.length === 0;
|
|
3148
|
+
const ready = observation.ready && toolSurfaceReady && !sourceSnapshotStale;
|
|
3149
|
+
const reasonCodes = [...observation.reasonCodes];
|
|
3150
|
+
if (!toolSurfaceReady) reasonCodes.push('MCP_TOOL_SURFACE_INCOMPLETE');
|
|
3151
|
+
if (sourceSnapshotStale) reasonCodes.push(runtimeSource.code === 'RUNTIME_SOURCE_SNAPSHOT_MISSING'
|
|
3152
|
+
? 'RUNTIME_SOURCE_SNAPSHOT_MISSING'
|
|
3153
|
+
: runtimeSource.code === 'RUNTIME_SOURCE_CURRENT_UNAVAILABLE'
|
|
3154
|
+
? 'RUNTIME_SOURCE_CURRENT_UNAVAILABLE'
|
|
3155
|
+
: 'RUNTIME_SOURCE_SNAPSHOT_STALE');
|
|
3156
|
+
const runtimeReadiness = observation.snapshot?.readiness;
|
|
3157
|
+
const releaseDiagnostic = runtimeReadiness?.diagnostics.releaseCoherence;
|
|
3158
|
+
const activeWorkSnapshot = listWorkContracts({ ...store, status: 'active', limit: 3 }).map((entry) => ({
|
|
3159
|
+
workId: entry.workId,
|
|
3160
|
+
status: entry.status,
|
|
3161
|
+
mode: entry.mode,
|
|
3162
|
+
objective: entry.objective.slice(0, 160),
|
|
3163
|
+
semantics: buildWorkContinuationSnapshot(entry).semantics,
|
|
3164
|
+
nextSafeAction: buildWorkContinuationSnapshot(entry).nextSafeAction,
|
|
3165
|
+
}));
|
|
3166
|
+
const activePlanSnapshot = listPlanContracts({ ...store, status: 'active', limit: 3 }).map(summarizePlanContract);
|
|
3167
|
+
const pendingHandoffSnapshot = listHandoffItems({ ...store, status: 'pending', limit: 4 });
|
|
3168
|
+
const preferredFacadeTools = ['rh_access', 'rh_status', 'rh_inbox', 'rh_context', 'rh_work'] as const;
|
|
3169
|
+
const facade = buildFacadeResult({
|
|
3170
|
+
status: ready ? 'ok' : 'blocked',
|
|
3171
|
+
summary: ready ? 'Controller and MCP tool surface are ready for bounded work.' : 'Controller or MCP tool surface needs attention before work.',
|
|
3172
|
+
data: {
|
|
3173
|
+
operation,
|
|
3174
|
+
repoId: repository.repoId,
|
|
3175
|
+
readiness: {
|
|
3176
|
+
ready,
|
|
3177
|
+
reasonCodes: [...new Set(reasonCodes)],
|
|
3178
|
+
diagnostics: {
|
|
3179
|
+
runtime: { ready: observation.ready },
|
|
3180
|
+
runtimeReadiness: runtimeReadiness ? {
|
|
3181
|
+
observedAt: runtimeReadiness.observedAt,
|
|
3182
|
+
database: runtimeReadiness.diagnostics.database,
|
|
3183
|
+
scheduler: runtimeReadiness.diagnostics.scheduler,
|
|
3184
|
+
releaseCoherence: runtimeReadiness.diagnostics.releaseCoherence,
|
|
3185
|
+
mcpEndToEnd: runtimeReadiness.diagnostics.mcpEndToEnd,
|
|
3186
|
+
} : undefined,
|
|
3187
|
+
toolSurface: {
|
|
3188
|
+
ready: toolSurfaceReady,
|
|
3189
|
+
expectedToolCount: exposure.expectedToolNames.length,
|
|
3190
|
+
actualToolCount: exposure.actualToolNames.length,
|
|
3191
|
+
observation: 'computed',
|
|
3192
|
+
missingTools: exposure.missingToolNames,
|
|
3193
|
+
unexpectedTools: exposure.unexpectedToolNames,
|
|
3194
|
+
duplicateTools: exposure.duplicateToolNames,
|
|
3195
|
+
fingerprint: exposure.fingerprint,
|
|
3196
|
+
schemaStableAcrossAccessModes: exposure.schemaStableAcrossAccessModes,
|
|
3197
|
+
},
|
|
3198
|
+
semantics: {
|
|
3199
|
+
executionReady: observation.ready,
|
|
3200
|
+
maintenanceHealthy: null,
|
|
3201
|
+
maintenanceCandidateCount: 0,
|
|
3202
|
+
releaseReady: releaseDiagnostic?.outcome === 'pass',
|
|
3203
|
+
executionBlockers: observation.ready ? [] : observation.reasonCodes,
|
|
3204
|
+
releaseBlockers: releaseDiagnostic?.outcome === 'fail' && releaseDiagnostic.reasonCode ? [releaseDiagnostic.reasonCode] : [],
|
|
3205
|
+
},
|
|
3206
|
+
sourceCoherence: { ready: !sourceSnapshotStale, reasons: runtimeSource.reasons },
|
|
3207
|
+
},
|
|
3208
|
+
observedAt: observation.observedAt,
|
|
3209
|
+
},
|
|
3210
|
+
repositoryState: {
|
|
3211
|
+
branch: repositoryIdentity.branch,
|
|
3212
|
+
head: repositoryIdentity.head,
|
|
3213
|
+
dirty: repositoryIdentity.dirty,
|
|
3214
|
+
observedAt: new Date().toISOString(),
|
|
3215
|
+
sourceSnapshotAgeMs: runtimeGeneration?.source.observedAt
|
|
3216
|
+
? Math.max(0, Date.now() - Date.parse(runtimeGeneration.source.observedAt))
|
|
3217
|
+
: undefined,
|
|
3218
|
+
sourceSnapshotStale,
|
|
3219
|
+
sourceSnapshotReasons: runtimeSource.reasons,
|
|
3220
|
+
runtimeSourceDirty: runtimeSource.current?.dirty === true,
|
|
3221
|
+
},
|
|
3222
|
+
toolArchitecture: {
|
|
3223
|
+
facadeTools: [...preferredFacadeTools],
|
|
3224
|
+
domainSchemaLoading: 'status_summary_runtime_snapshot',
|
|
3225
|
+
},
|
|
3226
|
+
toolSurface: preferredFacadeTools.filter((tool) => exposure.actualToolNames.includes(tool)),
|
|
3227
|
+
toolSurfaceStatus: {
|
|
3228
|
+
ready: toolSurfaceReady,
|
|
3229
|
+
expectedToolCount: exposure.expectedToolNames.length,
|
|
3230
|
+
actualToolCount: exposure.actualToolNames.length,
|
|
3231
|
+
observation: 'computed',
|
|
3232
|
+
missingTools: exposure.missingToolNames,
|
|
3233
|
+
unexpectedTools: exposure.unexpectedToolNames,
|
|
3234
|
+
duplicateTools: exposure.duplicateToolNames,
|
|
3235
|
+
fingerprint: exposure.fingerprint,
|
|
3236
|
+
schemaStableAcrossAccessModes: exposure.schemaStableAcrossAccessModes,
|
|
3237
|
+
},
|
|
3238
|
+
controllerSnapshot: {
|
|
3239
|
+
activeWork: activeWorkSnapshot,
|
|
3240
|
+
activePlans: activePlanSnapshot,
|
|
3241
|
+
pendingHandoffCount: pendingHandoffSnapshot.length,
|
|
3242
|
+
pendingHandoffs: pendingHandoffSnapshot.slice(0, 3).map((item) => ({
|
|
3243
|
+
id: item.id,
|
|
3244
|
+
workId: item.workId,
|
|
3245
|
+
title: item.title.slice(0, 96),
|
|
3246
|
+
severity: item.severity,
|
|
3247
|
+
updatedAt: item.updatedAt,
|
|
3248
|
+
})),
|
|
3249
|
+
bounded: true,
|
|
3250
|
+
nextDetail: 'Use rh_context(work_id=...) or rh_work(plan_get) only when the next decision requires more detail.',
|
|
3251
|
+
},
|
|
3252
|
+
},
|
|
3253
|
+
suggestedNextActions: [{
|
|
3254
|
+
label: 'Read repository context',
|
|
3255
|
+
tool: 'rh_context',
|
|
3256
|
+
operation: 'get',
|
|
3257
|
+
risk: 'readonly',
|
|
3258
|
+
confidence: 'medium',
|
|
3259
|
+
}],
|
|
3260
|
+
rawAvailable: false,
|
|
3261
|
+
detailLevel,
|
|
3262
|
+
});
|
|
3263
|
+
const payload = facade as unknown as Record<string, unknown>;
|
|
3264
|
+
payload.responseMeta = {
|
|
3265
|
+
serverDurationMs: Number((performance.now() - startedAt).toFixed(2)),
|
|
3266
|
+
structuredPayloadBytes: 0,
|
|
3267
|
+
};
|
|
3268
|
+
(payload.responseMeta as { structuredPayloadBytes: number }).structuredPayloadBytes = Buffer.byteLength(JSON.stringify(payload), 'utf8');
|
|
3269
|
+
return result(payload, facade.status !== 'ok');
|
|
3270
|
+
}
|
|
3271
|
+
const detailTimingStartedAt = performance.now();
|
|
3272
|
+
let detailTimingMark = detailTimingStartedAt;
|
|
3273
|
+
const detailPhaseTimingsMs: Record<string, number> = {};
|
|
3274
|
+
const markDetailPhase = (phase: string): void => {
|
|
3275
|
+
const now = performance.now();
|
|
3276
|
+
detailPhaseTimingsMs[phase] = Number((now - detailTimingMark).toFixed(2));
|
|
3277
|
+
detailTimingMark = now;
|
|
3278
|
+
};
|
|
3279
|
+
const readiness = await controllerReadinessEvidence(ctx, repository);
|
|
3280
|
+
markDetailPhase('readiness');
|
|
3281
|
+
const liveGit = gitSnapshot(repository.canonicalRoot);
|
|
3282
|
+
markDetailPhase('git');
|
|
3283
|
+
// Compare startup Runtime Source against the Controller package authority —
|
|
3284
|
+
// never against the selected execution repository.
|
|
2977
3285
|
const runtimeSource = runtimeSourceSnapshotStatus(readiness.daemon.source, ctx.runtimeSourceRoot);
|
|
2978
3286
|
const sourceSnapshotStale = runtimeSource.restartRequired;
|
|
2979
3287
|
// Dynamic import avoids a static cycle: toolset.ts composes runtimeToolDefinitions.
|
|
2980
3288
|
const toolset = await import('../../../cli/mcp/toolset');
|
|
2981
3289
|
const exposure = toolset.controllerExposureSnapshot(ctx);
|
|
2982
3290
|
const localRegisteredToolNames = toolset.allControllerToolDefinitions(ctx).map((tool) => tool.name).sort();
|
|
3291
|
+
markDetailPhase('tool_surface');
|
|
2983
3292
|
const toolSurfaceReady = exposure.ready && exposure.missingToolNames.length === 0;
|
|
2984
3293
|
// Ordinary interactive execution depends on the execution axis only.
|
|
2985
3294
|
// Maintenance debt / durable queue debt are reported as separate
|
|
@@ -2999,6 +3308,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
2999
3308
|
maintenanceHealthy = null;
|
|
3000
3309
|
}
|
|
3001
3310
|
}
|
|
3311
|
+
markDetailPhase('maintenance');
|
|
3002
3312
|
const effectiveReady = executionReady && toolSurfaceReady && !sourceSnapshotStale;
|
|
3003
3313
|
const readinessReasons = [...readiness.reasons];
|
|
3004
3314
|
if (!toolSurfaceReady) {
|
|
@@ -3056,22 +3366,29 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3056
3366
|
},
|
|
3057
3367
|
observedAt: new Date().toISOString(),
|
|
3058
3368
|
};
|
|
3059
|
-
|
|
3060
|
-
// Always prefer stored plugin manifests on rh_status get. Live host probes
|
|
3369
|
+
// Always prefer stored plugin manifests on rh_status detail. Live host probes
|
|
3061
3370
|
// (Xcode/simctl, etc.) must not stall Managed MCP gateways on reconnect/status.
|
|
3062
3371
|
const manifests = listAssistantPluginManifests(ctx.controllerHome, repository, {
|
|
3063
3372
|
preferStored: true,
|
|
3064
3373
|
});
|
|
3065
3374
|
const capabilities = listCapabilityDescriptors(manifests);
|
|
3375
|
+
markDetailPhase('plugins');
|
|
3066
3376
|
const pendingHandoffs = listHandoffItems({ ...store, status: 'pending', limit: 20 });
|
|
3067
3377
|
const activeContracts = listWorkContracts({ ...store, status: 'active', limit: 200 });
|
|
3378
|
+
markDetailPhase('work_state');
|
|
3068
3379
|
const activePrimaryWork = activeContracts.filter((contract) => (contract.lifecycleRole ?? 'primary') === 'primary');
|
|
3069
3380
|
const activeExecutionChildren = activeContracts.filter((contract) => contract.lifecycleRole === 'execution_child');
|
|
3070
|
-
|
|
3071
|
-
|
|
3381
|
+
// Current Runtime activity is owned by the in-memory Process monitor set.
|
|
3382
|
+
// Do not route an interactive status read through the retired cross-restart
|
|
3383
|
+
// recovery index, which may contain historical starting/running records.
|
|
3384
|
+
const activeProcessRecords = processRuntimeResourceDiagnostics().activeProcessIds
|
|
3385
|
+
.map((processId) => getProcessRecord(ctx.controllerHome, repository.repoId, processId))
|
|
3386
|
+
.filter((process): process is NonNullable<typeof process> => Boolean(process && isManagedProcessActive(process)));
|
|
3387
|
+
markDetailPhase('process_state');
|
|
3072
3388
|
const activeProcessWorkIds = new Set(activeProcessRecords.map((process) => process.workId).filter((workId): workId is string => Boolean(workId)));
|
|
3073
3389
|
const activeControllerWorkIds = new Set(activePrimaryWork.filter((contract) => Boolean(getControllerSession({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, contract.workId))).map((contract) => contract.workId));
|
|
3074
3390
|
const executingPrimaryWorkIds = new Set([...activeProcessWorkIds, ...activeControllerWorkIds].filter((workId) => activePrimaryWork.some((contract) => contract.workId === workId)));
|
|
3391
|
+
markDetailPhase('controller_sessions');
|
|
3075
3392
|
const preferredFacadeTools = ['rh_access', 'rh_status', 'rh_inbox', 'rh_context', 'rh_work'] as const;
|
|
3076
3393
|
const facade = buildFacadeResult({
|
|
3077
3394
|
status: effectiveReady ? 'ok' : 'blocked',
|
|
@@ -3132,7 +3449,13 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3132
3449
|
rawAvailable: detailLevel === 'detail',
|
|
3133
3450
|
detailLevel,
|
|
3134
3451
|
});
|
|
3135
|
-
|
|
3452
|
+
markDetailPhase('response_build');
|
|
3453
|
+
const payload = facade as unknown as Record<string, unknown>;
|
|
3454
|
+
payload.responseMeta = {
|
|
3455
|
+
serverDurationMs: Number((performance.now() - detailTimingStartedAt).toFixed(2)),
|
|
3456
|
+
phaseTimingsMs: detailPhaseTimingsMs,
|
|
3457
|
+
};
|
|
3458
|
+
return result(payload, facade.status !== 'ok');
|
|
3136
3459
|
}
|
|
3137
3460
|
case 'rh_inbox': {
|
|
3138
3461
|
const repository = selected(ctx, args);
|
|
@@ -3234,6 +3557,8 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3234
3557
|
});
|
|
3235
3558
|
return result(facade as unknown as Record<string, unknown>, true);
|
|
3236
3559
|
}
|
|
3560
|
+
const legacySemanticQuery = rhContextLegacySemanticQuery(query);
|
|
3561
|
+
const retrievalQuery = legacySemanticQuery.retrievalQuery;
|
|
3237
3562
|
const list = (value: unknown): string[] => Array.isArray(value)
|
|
3238
3563
|
? value.map(String).map((entry) => entry.trim()).filter(Boolean)
|
|
3239
3564
|
: [];
|
|
@@ -3247,9 +3572,27 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3247
3572
|
: retrievalMode === 'plan' || retrievalMode === 'debug'
|
|
3248
3573
|
? 'required'
|
|
3249
3574
|
: 'auto';
|
|
3250
|
-
const
|
|
3251
|
-
|
|
3252
|
-
|
|
3575
|
+
const requestedCheckIds = list(args.requested_check_ids);
|
|
3576
|
+
// Source retrieval is the default Context responsibility. Check
|
|
3577
|
+
// readiness is only meaningful when the controller has named the
|
|
3578
|
+
// checks it is about to use, so do not pay its Git/dependency/schedule
|
|
3579
|
+
// preflight cost for every broad search.
|
|
3580
|
+
const checks = requestedCheckIds.length > 0
|
|
3581
|
+
? listControllerChecks(repository.canonicalRoot)
|
|
3582
|
+
: [];
|
|
3583
|
+
const executionReadiness = requestedCheckIds.length > 0
|
|
3584
|
+
? repositoryExecutionReadiness(repository.canonicalRoot, checks, requestedCheckIds, {
|
|
3585
|
+
repoId: repository.repoId,
|
|
3586
|
+
checkoutId: repository.activeCheckoutId,
|
|
3587
|
+
})
|
|
3588
|
+
: undefined;
|
|
3589
|
+
const pack = await buildControllerContextPackAsync(repository.canonicalRoot, ctx.policy, {
|
|
3590
|
+
description: retrievalQuery,
|
|
3591
|
+
// Short code-like queries remain useful exact lexical needles. Long
|
|
3592
|
+
// semantic prompts are already tokenized from description; adding the
|
|
3593
|
+
// whole prompt as an exact needle prevents batch-search early exit and
|
|
3594
|
+
// forces a full bounded repository scan for a phrase that will not match.
|
|
3595
|
+
searchTerms: retrievalQuery.length <= 160 ? [retrievalQuery] : undefined,
|
|
3253
3596
|
knownPaths: list(args.known_paths),
|
|
3254
3597
|
includeGlobs: list(args.include_globs),
|
|
3255
3598
|
excludeGlobs: list(args.exclude_globs),
|
|
@@ -3259,7 +3602,55 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3259
3602
|
structuralIndexRoot: structuralContext === 'off' ? undefined : structuralIndexRoot(repository),
|
|
3260
3603
|
retrievalMode,
|
|
3261
3604
|
impactDomains,
|
|
3605
|
+
session: rhContextReadSessionId(ctx)
|
|
3606
|
+
? { sessionId: rhContextReadSessionId(ctx)!, repoId: repository.repoId, checkoutId: repository.activeCheckoutId }
|
|
3607
|
+
: undefined,
|
|
3262
3608
|
});
|
|
3609
|
+
const explicitSemanticNavigation = Array.isArray(args.semantic_navigation) ? args.semantic_navigation : [];
|
|
3610
|
+
const semanticRequests = [...explicitSemanticNavigation, ...legacySemanticQuery.requests];
|
|
3611
|
+
const semanticNavigation = {
|
|
3612
|
+
...await rhContextSemanticNavigation(repository.canonicalRoot, ctx.policy, semanticRequests, { repoId: repository.repoId, checkoutId: repository.activeCheckoutId }),
|
|
3613
|
+
requestSource: explicitSemanticNavigation.length > 0 && legacySemanticQuery.requests.length > 0
|
|
3614
|
+
? 'schema_and_query'
|
|
3615
|
+
: explicitSemanticNavigation.length > 0
|
|
3616
|
+
? 'semantic_navigation'
|
|
3617
|
+
: legacySemanticQuery.requests.length > 0
|
|
3618
|
+
? 'query_compatibility'
|
|
3619
|
+
: 'none',
|
|
3620
|
+
compatibilityQuerySyntax: RH_CONTEXT_LEGACY_SEMANTIC_SYNTAX,
|
|
3621
|
+
};
|
|
3622
|
+
const semanticReasonCodes = Array.from(new Set([
|
|
3623
|
+
...semanticNavigation.errors.map((entry) => `semantic.${String(entry.code ?? 'provider_error').toLowerCase()}`),
|
|
3624
|
+
...(semanticNavigation.requestTruncated ? ['semantic.request_truncated'] : []),
|
|
3625
|
+
...(semanticNavigation.policyDeniedLocations > 0 || semanticNavigation.policyDeniedReads > 0 ? ['semantic.policy_denied'] : []),
|
|
3626
|
+
...(semanticNavigation.freshness === 'changed_during_query' ? ['semantic.source_changed_during_query'] : []),
|
|
3627
|
+
...(semanticNavigation.requested > 0 && semanticNavigation.staticClosure.status !== 'complete_for_requested_symbols' ? ['semantic.static_closure_incomplete'] : []),
|
|
3628
|
+
]));
|
|
3629
|
+
const semanticUnavailable = semanticNavigation.requested > 0
|
|
3630
|
+
&& semanticNavigation.results.length === 0
|
|
3631
|
+
&& semanticNavigation.errors.length > 0
|
|
3632
|
+
&& semanticNavigation.errors.every((entry) => /UNAVAILABLE|NOT_AVAILABLE|NOT_READY|MISSING|NOT_FOUND|BUILD_SETTINGS|BUILD_SERVER/i.test(String(entry.code ?? '')));
|
|
3633
|
+
const semanticReadinessStatus = semanticNavigation.requested === 0
|
|
3634
|
+
? 'not_requested' as const
|
|
3635
|
+
: semanticNavigation.results.length === 0 && semanticNavigation.errors.length > 0
|
|
3636
|
+
? semanticUnavailable ? 'unavailable' as const : 'error' as const
|
|
3637
|
+
: semanticNavigation.staticClosure.status === 'complete_for_requested_symbols' && semanticReasonCodes.length === 0
|
|
3638
|
+
? 'ready' as const
|
|
3639
|
+
: 'partial' as const;
|
|
3640
|
+
const readinessStatus = pack.readiness.status === 'insufficient'
|
|
3641
|
+
? 'insufficient' as const
|
|
3642
|
+
: semanticReadinessStatus === 'unavailable' || semanticReadinessStatus === 'error'
|
|
3643
|
+
? 'insufficient' as const
|
|
3644
|
+
: pack.readiness.status === 'degraded' || semanticReadinessStatus === 'partial'
|
|
3645
|
+
? 'degraded' as const
|
|
3646
|
+
: 'ready' as const;
|
|
3647
|
+
const readiness = {
|
|
3648
|
+
...pack.readiness,
|
|
3649
|
+
status: readinessStatus,
|
|
3650
|
+
semantic: { status: semanticReadinessStatus, reasonCodes: semanticReasonCodes },
|
|
3651
|
+
unresolvedReasonCodes: Array.from(new Set([...pack.readiness.unresolvedReasonCodes, ...semanticReasonCodes])).slice(0, 80),
|
|
3652
|
+
readyForHighConfidenceMutation: readinessStatus === 'ready',
|
|
3653
|
+
};
|
|
3263
3654
|
const warnings = structuralContext === 'required' && !pack.structuralContext.requiredSatisfied
|
|
3264
3655
|
? [pack.structuralContext.fallbackReason ?? 'Required structural context is not ready; lexical retrieval results are returned as degraded evidence.']
|
|
3265
3656
|
: [];
|
|
@@ -3275,16 +3666,27 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3275
3666
|
search: pack.search,
|
|
3276
3667
|
structuralContext: pack.structuralContext,
|
|
3277
3668
|
impactContext: pack.impactContext,
|
|
3669
|
+
semanticNavigation,
|
|
3670
|
+
readiness,
|
|
3671
|
+
expansion: pack.expansion,
|
|
3278
3672
|
files: pack.files,
|
|
3673
|
+
coverage: pack.coverage,
|
|
3674
|
+
cache: pack.cache,
|
|
3675
|
+
timingsMs: pack.timingsMs,
|
|
3279
3676
|
deniedPaths: pack.deniedPaths,
|
|
3280
3677
|
omitted: pack.omitted,
|
|
3281
3678
|
limits: pack.limits,
|
|
3282
3679
|
contextContract: pack.contextContract,
|
|
3680
|
+
...(executionReadiness ? {
|
|
3681
|
+
executionReadiness,
|
|
3682
|
+
registeredChecks: checks.slice(0, 80).map((check) => ({ id: check.id, description: check.description, source: check.source, effects: check.effects })),
|
|
3683
|
+
} : {}),
|
|
3283
3684
|
retrievalPolicy: {
|
|
3284
3685
|
defaultBackend: 'bounded_lexical',
|
|
3285
3686
|
structuralBackend: 'codegraph',
|
|
3286
3687
|
rawReadTool: 'read_repository_file',
|
|
3287
3688
|
shellSearchFallbackOnly: true,
|
|
3689
|
+
executionReadiness: 'requested_check_ids_only',
|
|
3288
3690
|
},
|
|
3289
3691
|
},
|
|
3290
3692
|
warnings,
|
|
@@ -3294,8 +3696,116 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3294
3696
|
});
|
|
3295
3697
|
return result(facade as unknown as Record<string, unknown>);
|
|
3296
3698
|
}
|
|
3297
|
-
const
|
|
3699
|
+
const startedAt = performance.now();
|
|
3700
|
+
const requestedCapabilityId = typeof args.capability_id === 'string' ? args.capability_id.trim() : '';
|
|
3701
|
+
if (requestedCapabilityId) {
|
|
3702
|
+
const pluginMatch = /^plugin\.([^.]+)\.(.+)$/.exec(requestedCapabilityId);
|
|
3703
|
+
const manifests = pluginMatch ? (() => {
|
|
3704
|
+
const pluginId = pluginMatch[1];
|
|
3705
|
+
const targets = [repository];
|
|
3706
|
+
const controllerRepository = controllerPluginRepository(ctx.controllerHome);
|
|
3707
|
+
if (controllerRepository.repoId !== repository.repoId) targets.push(controllerRepository);
|
|
3708
|
+
for (const target of targets) {
|
|
3709
|
+
try {
|
|
3710
|
+
return [getAssistantPluginManifest(ctx.controllerHome, target, pluginId)];
|
|
3711
|
+
} catch (error) {
|
|
3712
|
+
if (error instanceof Error && error.message.startsWith('PLUGIN_NOT_FOUND:')) continue;
|
|
3713
|
+
throw error;
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
return [];
|
|
3717
|
+
})() : [];
|
|
3718
|
+
const descriptor = getCapabilityDescriptor(requestedCapabilityId, manifests);
|
|
3719
|
+
const pluginAction = getPluginActionCapabilitySchema(requestedCapabilityId, manifests);
|
|
3720
|
+
const facade = buildFacadeResult({
|
|
3721
|
+
status: 'ok',
|
|
3722
|
+
summary: descriptor
|
|
3723
|
+
? `Exact capability ${requestedCapabilityId} is available.`
|
|
3724
|
+
: `Capability ${requestedCapabilityId} was not found.`,
|
|
3725
|
+
data: {
|
|
3726
|
+
operation,
|
|
3727
|
+
repoId: repository.repoId,
|
|
3728
|
+
repository: {
|
|
3729
|
+
repoId: repository.repoId,
|
|
3730
|
+
displayName: repository.displayName,
|
|
3731
|
+
defaultBranch: repository.defaultBranch,
|
|
3732
|
+
repositoryType: repository.repositoryType,
|
|
3733
|
+
},
|
|
3734
|
+
capabilityLookup: {
|
|
3735
|
+
requestedCapabilityId,
|
|
3736
|
+
found: Boolean(descriptor),
|
|
3737
|
+
descriptor,
|
|
3738
|
+
pluginAction,
|
|
3739
|
+
},
|
|
3740
|
+
toolArchitecture: {
|
|
3741
|
+
facadeTools: ['rh_access', 'rh_status', 'rh_inbox', 'rh_context', 'rh_work'],
|
|
3742
|
+
domainSchemaLoading: 'exact_capability_fast_path',
|
|
3743
|
+
},
|
|
3744
|
+
bounded: true,
|
|
3745
|
+
},
|
|
3746
|
+
warnings: [],
|
|
3747
|
+
suggestedNextActions: [],
|
|
3748
|
+
detailLevel: args.detail_level === 'detail' || args.detail_level === 'raw' ? args.detail_level : 'summary',
|
|
3749
|
+
rawAvailable: false,
|
|
3750
|
+
});
|
|
3751
|
+
const payload = facade as unknown as Record<string, unknown>;
|
|
3752
|
+
payload.responseMeta = {
|
|
3753
|
+
serverDurationMs: Number((performance.now() - startedAt).toFixed(2)),
|
|
3754
|
+
structuredPayloadBytes: 0,
|
|
3755
|
+
};
|
|
3756
|
+
(payload.responseMeta as { structuredPayloadBytes: number }).structuredPayloadBytes = Buffer.byteLength(JSON.stringify(payload), 'utf8');
|
|
3757
|
+
return result(payload);
|
|
3758
|
+
}
|
|
3759
|
+
const capabilityIntentQuery = operation === 'list' && typeof args.query === 'string'
|
|
3760
|
+
? args.query.trim()
|
|
3761
|
+
: '';
|
|
3762
|
+
if (capabilityIntentQuery) {
|
|
3763
|
+
const manifestOptions = { preferStored: true };
|
|
3764
|
+
const repositoryManifests = listAssistantPluginManifests(ctx.controllerHome, repository, manifestOptions);
|
|
3765
|
+
const controllerRepository = controllerPluginRepository(ctx.controllerHome);
|
|
3766
|
+
const controllerManifests = repository.repoId === controllerRepository.repoId
|
|
3767
|
+
? []
|
|
3768
|
+
: listAssistantPluginManifests(ctx.controllerHome, controllerRepository, manifestOptions);
|
|
3769
|
+
const manifests = [...new Map(
|
|
3770
|
+
[...repositoryManifests, ...controllerManifests].map((manifest) => [manifest.pluginId, manifest] as const),
|
|
3771
|
+
).values()];
|
|
3772
|
+
const matches = searchCapabilityDescriptors(capabilityIntentQuery, manifests, 12)
|
|
3773
|
+
.map((match) => ({
|
|
3774
|
+
...match,
|
|
3775
|
+
pluginAction: getPluginActionCapabilitySchema(match.capabilityId, manifests),
|
|
3776
|
+
}));
|
|
3777
|
+
const facade = buildFacadeResult({
|
|
3778
|
+
status: 'ok',
|
|
3779
|
+
summary: matches.length > 0
|
|
3780
|
+
? `Found ${matches.length} capability candidate(s) for intent: ${capabilityIntentQuery}`
|
|
3781
|
+
: `No capability candidates matched intent: ${capabilityIntentQuery}`,
|
|
3782
|
+
data: {
|
|
3783
|
+
operation,
|
|
3784
|
+
repoId: repository.repoId,
|
|
3785
|
+
capabilitySearch: {
|
|
3786
|
+
query: capabilityIntentQuery,
|
|
3787
|
+
matches,
|
|
3788
|
+
readOnlyDiscovery: true,
|
|
3789
|
+
executeWith: 'plugin_action_execute',
|
|
3790
|
+
},
|
|
3791
|
+
toolArchitecture: {
|
|
3792
|
+
facadeTools: ['rh_access', 'rh_status', 'rh_inbox', 'rh_context', 'rh_work'],
|
|
3793
|
+
domainSchemaLoading: 'intent_ranked_capability_search',
|
|
3794
|
+
exactCapabilityLookupStillAvailable: true,
|
|
3795
|
+
},
|
|
3796
|
+
bounded: true,
|
|
3797
|
+
},
|
|
3798
|
+
warnings: [],
|
|
3799
|
+
suggestedNextActions: [],
|
|
3800
|
+
detailLevel: args.detail_level === 'detail' || args.detail_level === 'raw' ? args.detail_level : 'summary',
|
|
3801
|
+
rawAvailable: false,
|
|
3802
|
+
});
|
|
3803
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
3804
|
+
}
|
|
3298
3805
|
const requested = Array.isArray(args.requested_check_ids) ? args.requested_check_ids.map(String) : [];
|
|
3806
|
+
const detailLevel = args.detail_level === 'detail' || args.detail_level === 'raw' ? args.detail_level : 'summary';
|
|
3807
|
+
const isSummary = detailLevel === 'summary';
|
|
3808
|
+
const checks = requested.length > 0 || !isSummary ? listControllerChecks(repository.canonicalRoot) : [];
|
|
3299
3809
|
const normalizedChecks = normalizeCheckIds(requested, checks);
|
|
3300
3810
|
const store = { controllerHome: ctx.controllerHome, repoId: repository.repoId };
|
|
3301
3811
|
const workId = typeof args.work_id === 'string' ? args.work_id : undefined;
|
|
@@ -3310,9 +3820,6 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3310
3820
|
});
|
|
3311
3821
|
return result(facade as unknown as Record<string, unknown>, true);
|
|
3312
3822
|
}
|
|
3313
|
-
const startedAt = performance.now();
|
|
3314
|
-
const detailLevel = args.detail_level === 'detail' || args.detail_level === 'raw' ? args.detail_level : 'summary';
|
|
3315
|
-
const isSummary = detailLevel === 'summary';
|
|
3316
3823
|
const activeContractScan = operation === 'list' || !workId
|
|
3317
3824
|
? listWorkContracts({ ...store, status: 'active', limit: 20 })
|
|
3318
3825
|
: [];
|
|
@@ -3326,36 +3833,29 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3326
3833
|
.filter((job) => timestampIsCurrent(job.updatedAt, currentCutoffMs))
|
|
3327
3834
|
.slice(0, 5)
|
|
3328
3835
|
: [];
|
|
3329
|
-
const processScan =
|
|
3836
|
+
const processScan = isSummary
|
|
3837
|
+
? listRecoverableProcessRecords(ctx.controllerHome, repository.repoId)
|
|
3838
|
+
: listProcessRecords(ctx.controllerHome, repository.repoId, workId ? 100 : 50);
|
|
3330
3839
|
const relevantProcesses = processScan.filter((process) => workId ? process.workId === workId : timestampIsCurrent(process.updatedAt, currentCutoffMs));
|
|
3331
|
-
const
|
|
3332
|
-
const activeProcesses = relevantProcesses.filter((process) =>
|
|
3840
|
+
const liveProcessIds = new Set(processRuntimeResourceDiagnostics().activeProcessIds);
|
|
3841
|
+
const activeProcesses = relevantProcesses.filter((process) => liveProcessIds.has(process.processId) && isManagedProcessActive(process));
|
|
3333
3842
|
const workController = work ? getControllerSession(store, work.workId) : undefined;
|
|
3334
|
-
const
|
|
3335
|
-
|
|
3336
|
-
});
|
|
3337
|
-
const controllerRepository = controllerPluginRepository(ctx.controllerHome);
|
|
3338
|
-
const controllerManifests = repository.repoId === controllerRepository.repoId
|
|
3843
|
+
const manifestOptions = { preferStored: true };
|
|
3844
|
+
const manifests = isSummary
|
|
3339
3845
|
? []
|
|
3340
|
-
:
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
const
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
const capabilityLookup = requestedCapabilityId ? {
|
|
3354
|
-
requestedCapabilityId,
|
|
3355
|
-
found: Boolean(selectedCapability),
|
|
3356
|
-
descriptor: selectedCapability,
|
|
3357
|
-
pluginAction: pluginActionSchema,
|
|
3358
|
-
} : undefined;
|
|
3846
|
+
: (() => {
|
|
3847
|
+
const repositoryManifests = listAssistantPluginManifests(ctx.controllerHome, repository, manifestOptions);
|
|
3848
|
+
const controllerRepository = controllerPluginRepository(ctx.controllerHome);
|
|
3849
|
+
const controllerManifests = repository.repoId === controllerRepository.repoId
|
|
3850
|
+
? []
|
|
3851
|
+
: listAssistantPluginManifests(ctx.controllerHome, controllerRepository, manifestOptions);
|
|
3852
|
+
return [...new Map(
|
|
3853
|
+
[...repositoryManifests, ...controllerManifests].map((manifest) => [manifest.pluginId, manifest] as const),
|
|
3854
|
+
).values()];
|
|
3855
|
+
})();
|
|
3856
|
+
const capabilities = isSummary ? [] : listCapabilityDescriptors(manifests);
|
|
3857
|
+
const capabilityGroups = isSummary ? [] : summarizeCapabilityGroups(manifests);
|
|
3858
|
+
const capabilityLookup = undefined;
|
|
3359
3859
|
const selectedChecks = normalizedChecks.validCheckIds
|
|
3360
3860
|
.map((id) => checks.find((check) => check.id === id))
|
|
3361
3861
|
.filter((check): check is (typeof checks)[number] => Boolean(check))
|
|
@@ -3407,8 +3907,11 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3407
3907
|
requestedCheckIds: requested,
|
|
3408
3908
|
normalizedChecks,
|
|
3409
3909
|
invalidCheckIdsAreNotFailures: true,
|
|
3410
|
-
|
|
3411
|
-
|
|
3910
|
+
capabilityInventory: {
|
|
3911
|
+
mode: 'detail_only',
|
|
3912
|
+
deferred: true,
|
|
3913
|
+
reason: 'Work/repository summary does not hydrate plugin manifests; request capability_id or detail/raw only when schema/policy detail is needed.',
|
|
3914
|
+
},
|
|
3412
3915
|
capabilityLookup,
|
|
3413
3916
|
toolArchitecture: {
|
|
3414
3917
|
facadeTools: ['rh_access', 'rh_status', 'rh_inbox', 'rh_context', 'rh_work'],
|
|
@@ -3441,8 +3944,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3441
3944
|
counts: {
|
|
3442
3945
|
availableChecks: checks.length,
|
|
3443
3946
|
selectedChecks: selectedChecks.length,
|
|
3444
|
-
|
|
3445
|
-
capabilityGroups: capabilityGroups.length,
|
|
3947
|
+
capabilityInventoryDeferred: true,
|
|
3446
3948
|
activeWork: currentContractScan.length,
|
|
3447
3949
|
activeWorkShown: activeContracts.length,
|
|
3448
3950
|
storedNonTerminalWork: activeContractScan.length,
|
|
@@ -3452,6 +3954,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3452
3954
|
currentAttentionShown: attention.length,
|
|
3453
3955
|
activeProcesses: activeProcesses.length,
|
|
3454
3956
|
recentProcesses: relevantProcesses.length,
|
|
3957
|
+
historicalProcessScanDeferred: true,
|
|
3455
3958
|
pendingAttentionScanned: pendingAttention.length,
|
|
3456
3959
|
historicalPendingAttention: Math.max(0, pendingAttention.length - currentAttentionScan.length),
|
|
3457
3960
|
omittedCurrentAttention: Math.max(0, currentAttentionScan.length - attention.length),
|
|
@@ -3547,14 +4050,30 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3547
4050
|
case 'rh_work': {
|
|
3548
4051
|
const repository = selected(ctx, args);
|
|
3549
4052
|
const store = { controllerHome: ctx.controllerHome, repoId: repository.repoId };
|
|
3550
|
-
const
|
|
4053
|
+
const requestedOperation = String(args.operation ?? 'start');
|
|
4054
|
+
const frozenScheduleDeleteId = requestedOperation === 'repair' && typeof args.capability_id === 'string' && args.capability_id.startsWith('schedule.delete:')
|
|
4055
|
+
? args.capability_id.slice('schedule.delete:'.length).trim()
|
|
4056
|
+
: '';
|
|
4057
|
+
let frozenControllerDisposition: ReturnType<typeof parseControllerDispositionCompatibilityCapability>;
|
|
4058
|
+
try {
|
|
4059
|
+
frozenControllerDisposition = parseControllerDispositionCompatibilityCapability(requestedOperation, args.capability_id);
|
|
4060
|
+
} catch (error) {
|
|
4061
|
+
return result(buildFacadeResult({
|
|
4062
|
+
status: 'blocked',
|
|
4063
|
+
summary: error instanceof Error ? error.message : 'Controller disposition compatibility input is invalid.',
|
|
4064
|
+
data: {},
|
|
4065
|
+
}) as unknown as Record<string, unknown>, true);
|
|
4066
|
+
}
|
|
4067
|
+
const operation = frozenControllerDisposition
|
|
4068
|
+
? 'controller_disposition'
|
|
4069
|
+
: frozenScheduleDeleteId ? 'schedule_delete' : requestedOperation;
|
|
3551
4070
|
if (!allowedFacadeOperations('rh_work').includes(operation)) {
|
|
3552
4071
|
return invalidFacadeOperation('rh_work', operation);
|
|
3553
4072
|
}
|
|
3554
4073
|
if (operation.startsWith('schedule_')) {
|
|
3555
4074
|
try {
|
|
3556
4075
|
const workId = String(args.work_id ?? '').trim();
|
|
3557
|
-
const scheduleId = String(args.schedule_id ?? '').trim();
|
|
4076
|
+
const scheduleId = frozenScheduleDeleteId || String(args.schedule_id ?? '').trim();
|
|
3558
4077
|
if (operation === 'schedule_create') {
|
|
3559
4078
|
const controllerType = String(args.controller_type ?? 'chatgpt').trim();
|
|
3560
4079
|
if (!['chatgpt', 'codex', 'claude', 'grok'].includes(controllerType)) throw new Error('CONTROLLER_TYPE_INVALID');
|
|
@@ -3562,56 +4081,8 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3562
4081
|
const triggerType = ['interval', 'cron', 'calendar', 'condition', 'repository-event', 'dependency-checkpoint', 'manual'].includes(triggerTypeRaw)
|
|
3563
4082
|
? triggerTypeRaw as 'interval' | 'cron' | 'calendar' | 'condition' | 'repository-event' | 'dependency-checkpoint' | 'manual'
|
|
3564
4083
|
: undefined;
|
|
3565
|
-
const scheduleModeRaw = String(args.schedule_mode ?? (workId ? 'continuation' : '
|
|
3566
|
-
if (!['
|
|
3567
|
-
if (scheduleModeRaw === 'workflow') {
|
|
3568
|
-
if (controllerType !== 'chatgpt') throw new Error('WORKFLOW_SCHEDULE_CONTROLLER_TYPE_UNSUPPORTED');
|
|
3569
|
-
const prompt = (typeof args.objective === 'string' ? args.objective : typeof args.continuation_prompt === 'string' ? args.continuation_prompt : '').trim();
|
|
3570
|
-
if (!prompt) throw new Error('WORKFLOW_SCHEDULE_PROMPT_REQUIRED');
|
|
3571
|
-
const trigger = {
|
|
3572
|
-
type: triggerType ?? (typeof args.every_minutes === 'number' ? 'interval' : 'manual'),
|
|
3573
|
-
everyMinutes: typeof args.every_minutes === 'number' ? Math.max(1, Math.trunc(args.every_minutes)) : undefined,
|
|
3574
|
-
cronExpression: typeof args.cron_expression === 'string' ? args.cron_expression : undefined,
|
|
3575
|
-
timezone: typeof args.schedule_timezone === 'string' ? args.schedule_timezone : undefined,
|
|
3576
|
-
catchUpMinutes: typeof args.catch_up_minutes === 'number' ? Math.max(0, Math.trunc(args.catch_up_minutes)) : undefined,
|
|
3577
|
-
calendarAt: typeof args.calendar_at === 'string' ? args.calendar_at : undefined,
|
|
3578
|
-
condition: args.condition && typeof args.condition === 'object' && !Array.isArray(args.condition) ? args.condition as never : undefined,
|
|
3579
|
-
eventName: typeof args.event_name === 'string' ? args.event_name : undefined,
|
|
3580
|
-
dependencyJobIds: Array.isArray(args.dependency_job_ids) ? args.dependency_job_ids.map(String) : undefined,
|
|
3581
|
-
};
|
|
3582
|
-
const policy = {
|
|
3583
|
-
maxActiveOccurrences: 1,
|
|
3584
|
-
maxFailures: typeof args.max_failures === 'number' ? Math.max(1, Math.trunc(args.max_failures)) : 3,
|
|
3585
|
-
cooldownMinutes: typeof args.cooldown_minutes === 'number' ? Math.max(0, Math.trunc(args.cooldown_minutes)) : 120,
|
|
3586
|
-
dailyBudgetMinutes: typeof args.daily_budget_minutes === 'number' ? Math.max(1, Math.trunc(args.daily_budget_minutes)) : 180,
|
|
3587
|
-
shadowMode: args.shadow_mode !== false,
|
|
3588
|
-
backoffBaseMinutes: typeof args.backoff_base_minutes === 'number' ? Math.max(1, Math.trunc(args.backoff_base_minutes)) : 5,
|
|
3589
|
-
backoffMaxMinutes: typeof args.backoff_max_minutes === 'number' ? Math.max(1, Math.trunc(args.backoff_max_minutes)) : 24 * 60,
|
|
3590
|
-
};
|
|
3591
|
-
const actionArguments = {
|
|
3592
|
-
prompt,
|
|
3593
|
-
...(typeof args.browser_session_id === 'string' && args.browser_session_id.trim() ? { browser_session_id: args.browser_session_id.trim() } : {}),
|
|
3594
|
-
...(typeof args.conversation_url === 'string' && args.conversation_url.trim() ? { conversation_url: args.conversation_url.trim() } : {}),
|
|
3595
|
-
};
|
|
3596
|
-
assertAutomatedOperationAllowed('chatgpt_browser_prompt', actionArguments);
|
|
3597
|
-
const name = typeof args.schedule_name === 'string' && args.schedule_name.trim() ? args.schedule_name.trim() : `Workflow: ${prompt.slice(0, 80)}`;
|
|
3598
|
-
const stopConditions = Array.isArray(args.stop_conditions) ? args.stop_conditions.map(String) : [];
|
|
3599
|
-
const semantic = JSON.stringify({ repoId: repository.repoId, name, trigger, policy, actionArguments, stopConditions });
|
|
3600
|
-
const requestId = typeof args.schedule_request_id === 'string' && args.schedule_request_id.trim()
|
|
3601
|
-
? args.schedule_request_id.trim()
|
|
3602
|
-
: `workflow-schedule:${repository.repoId}:${createHash('sha256').update(semantic).digest('hex').slice(0, 20)}`;
|
|
3603
|
-
const schedule = createSchedule(ctx.controllerHome, {
|
|
3604
|
-
requestId,
|
|
3605
|
-
repoId: repository.repoId,
|
|
3606
|
-
name,
|
|
3607
|
-
enabled: true,
|
|
3608
|
-
trigger,
|
|
3609
|
-
policy,
|
|
3610
|
-
action: { operation: 'chatgpt_browser_prompt', target: 'runtime', arguments: actionArguments, resourceClaims: [] },
|
|
3611
|
-
stopConditions,
|
|
3612
|
-
});
|
|
3613
|
-
return result(buildFacadeResult({ summary: `Workflow schedule ${schedule.scheduleId} is configured without a durable Work.`, data: { schedule } }) as unknown as Record<string, unknown>);
|
|
3614
|
-
}
|
|
4084
|
+
const scheduleModeRaw = String(args.schedule_mode ?? (workId ? 'continuation' : '')).trim();
|
|
4085
|
+
if (!['continuation', 'browser_watch', 'browser_keepalive'].includes(scheduleModeRaw)) throw new Error('SCHEDULE_MODE_REQUIRES_WORK_OR_EXPLICIT_BROWSER_KEEPALIVE');
|
|
3615
4086
|
const created = createWorkContinuationSchedule(ctx.controllerHome, repository.repoId, {
|
|
3616
4087
|
workId,
|
|
3617
4088
|
scheduleMode: scheduleModeRaw as 'continuation' | 'browser_watch' | 'browser_keepalive',
|
|
@@ -3654,38 +4125,53 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3654
4125
|
backoffMaxMinutes: typeof args.backoff_max_minutes === 'number' ? args.backoff_max_minutes : undefined,
|
|
3655
4126
|
stopConditions: Array.isArray(args.stop_conditions) ? args.stop_conditions.map(String) : undefined,
|
|
3656
4127
|
});
|
|
3657
|
-
return result(buildFacadeResult({
|
|
4128
|
+
return result(buildFacadeResult({
|
|
4129
|
+
summary: created.work
|
|
4130
|
+
? `Work schedule ${created.schedule.scheduleId} is configured for Work ${created.work.workId}.`
|
|
4131
|
+
: `Browser keepalive schedule ${created.schedule.scheduleId} is configured without a durable Work.`,
|
|
4132
|
+
data: {
|
|
4133
|
+
schedule: created.schedule,
|
|
4134
|
+
...(created.work ? { work: buildWorkContinuationSnapshot(created.work) } : {}),
|
|
4135
|
+
},
|
|
4136
|
+
}) as unknown as Record<string, unknown>);
|
|
3658
4137
|
}
|
|
3659
4138
|
if (operation === 'schedule_list') {
|
|
3660
|
-
const schedules =
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
return result(buildFacadeResult({ summary: `Found ${schedules.length} schedule(s).`, data }) as unknown as Record<string, unknown>);
|
|
4139
|
+
const schedules = listWorkContinuationSchedules(ctx.controllerHome, repository.repoId, {
|
|
4140
|
+
workId: workId || undefined,
|
|
4141
|
+
includeOccurrences: args.include_occurrences === true,
|
|
4142
|
+
});
|
|
4143
|
+
const data = args.include_occurrences === true ? schedules : { schedules: schedules.schedules };
|
|
4144
|
+
return result(buildFacadeResult({ summary: `Found ${schedules.schedules.length} schedule(s).`, data }) as unknown as Record<string, unknown>);
|
|
3666
4145
|
}
|
|
3667
4146
|
if (!scheduleId) throw new Error('SCHEDULE_ID_REQUIRED');
|
|
3668
4147
|
if (operation === 'schedule_get') {
|
|
3669
|
-
const
|
|
3670
|
-
const data = args.include_occurrences === true ? { schedule, occurrences: listOccurrences(ctx.controllerHome, repository.repoId, scheduleId, 50) } : { schedule };
|
|
4148
|
+
const data = getWorkContinuationSchedule(ctx.controllerHome, repository.repoId, scheduleId, args.include_occurrences === true);
|
|
3671
4149
|
return result(buildFacadeResult({ summary: `Schedule ${scheduleId}.`, data }) as unknown as Record<string, unknown>);
|
|
3672
4150
|
}
|
|
3673
4151
|
if (operation === 'schedule_pause') {
|
|
3674
|
-
const
|
|
3675
|
-
const saved = saveSchedule(ctx.controllerHome, { ...schedule, enabled: false, pausedReason: typeof args.reason === 'string' && args.reason.trim() ? args.reason.trim() : 'Paused by ChatGPT.' });
|
|
4152
|
+
const saved = pauseWorkContinuationSchedule(ctx.controllerHome, repository.repoId, scheduleId, typeof args.reason === 'string' ? args.reason : undefined);
|
|
3676
4153
|
return result(buildFacadeResult({ summary: `Schedule ${scheduleId} is paused.`, data: { schedule: saved } }) as unknown as Record<string, unknown>);
|
|
3677
4154
|
}
|
|
3678
4155
|
if (operation === 'schedule_resume') {
|
|
3679
|
-
const
|
|
3680
|
-
const saved = saveSchedule(ctx.controllerHome, { ...schedule, enabled: true, pausedReason: undefined, consecutiveFailures: 0, nextEligibleAt: undefined });
|
|
4156
|
+
const saved = resumeWorkContinuationSchedule(ctx.controllerHome, repository.repoId, scheduleId);
|
|
3681
4157
|
return result(buildFacadeResult({ summary: `Schedule ${scheduleId} is resumed.`, data: { schedule: saved } }) as unknown as Record<string, unknown>);
|
|
3682
4158
|
}
|
|
4159
|
+
if (operation === 'schedule_delete') {
|
|
4160
|
+
const schedule = getWorkContinuationSchedule(ctx.controllerHome, repository.repoId, scheduleId).schedule;
|
|
4161
|
+
deleteSchedule(ctx.controllerHome, repository.repoId, scheduleId);
|
|
4162
|
+
return result(buildFacadeResult({
|
|
4163
|
+
summary: `Schedule ${scheduleId} is deleted; historical occurrences and evidence are retained.`,
|
|
4164
|
+
data: { scheduleId, deleted: true, requestId: schedule.requestId },
|
|
4165
|
+
}) as unknown as Record<string, unknown>);
|
|
4166
|
+
}
|
|
3683
4167
|
if (operation === 'schedule_trigger') {
|
|
3684
|
-
const
|
|
3685
|
-
const
|
|
3686
|
-
|
|
4168
|
+
const repositoryEvent = typeof args.event_name === 'string' && args.event_name.trim().length > 0;
|
|
4169
|
+
const explicitEventId = typeof args.event_id === 'string' ? args.event_id.trim() : '';
|
|
4170
|
+
const manualRequestId = !repositoryEvent && typeof args.request_id === 'string' ? args.request_id.trim() : '';
|
|
4171
|
+
const occurrence = await triggerWorkContinuationSchedule(ctx.controllerHome, repository.repoId, scheduleId, {
|
|
4172
|
+
source: repositoryEvent ? 'repository-event' : 'manual',
|
|
3687
4173
|
eventName: typeof args.event_name === 'string' ? args.event_name : undefined,
|
|
3688
|
-
eventId:
|
|
4174
|
+
eventId: explicitEventId || manualRequestId || undefined,
|
|
3689
4175
|
data: args.event_data && typeof args.event_data === 'object' && !Array.isArray(args.event_data) ? args.event_data as Record<string, unknown> : undefined,
|
|
3690
4176
|
});
|
|
3691
4177
|
return result(buildFacadeResult({ summary: occurrence ? `Schedule ${scheduleId} produced ${occurrence.decision}.` : `Schedule ${scheduleId} produced no occurrence.`, data: { occurrence } }) as unknown as Record<string, unknown>);
|
|
@@ -3731,25 +4217,210 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3731
4217
|
permissionSnapshotVersion,
|
|
3732
4218
|
lastValidatedAt: new Date().toISOString(),
|
|
3733
4219
|
});
|
|
3734
|
-
|
|
4220
|
+
const relay = acknowledgeControllerRoundClaim(
|
|
4221
|
+
{ controllerHome: ctx.controllerHome, repoId: repository.repoId },
|
|
4222
|
+
{ workId, session },
|
|
4223
|
+
);
|
|
4224
|
+
return result(buildFacadeResult({
|
|
4225
|
+
summary: relay?.status === 'claimed'
|
|
4226
|
+
? `Controller ${session.controllerId} claimed ${session.workId}; the dispatched ChatGPT round is mechanically acknowledged and still requires an explicit semantic disposition.`
|
|
4227
|
+
: `Controller ${session.controllerId} claimed ${session.workId}.`,
|
|
4228
|
+
data: { session, relay },
|
|
4229
|
+
}) as unknown as Record<string, unknown>);
|
|
3735
4230
|
} catch (error) {
|
|
3736
4231
|
return result(buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Controller claim failed.', data: {} }) as unknown as Record<string, unknown>, true);
|
|
3737
4232
|
}
|
|
3738
4233
|
}
|
|
4234
|
+
if (operation === 'controller_disposition') {
|
|
4235
|
+
try {
|
|
4236
|
+
const workId = String(args.work_id ?? '').trim();
|
|
4237
|
+
const disposition = frozenControllerDisposition?.disposition ?? String(args.disposition ?? '').trim();
|
|
4238
|
+
if (!['continue_immediately', 'wait', 'wait_for_user', 'goal_complete'].includes(disposition)) {
|
|
4239
|
+
throw new Error('CONTROLLER_RELAY_DISPOSITION_INVALID');
|
|
4240
|
+
}
|
|
4241
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
4242
|
+
const work = getWorkContract(store, workId);
|
|
4243
|
+
if (!work) throw new Error(`WORK_NOT_FOUND: ${workId}`);
|
|
4244
|
+
const terminalGoalComplete = work.status === 'completed' && disposition === 'goal_complete';
|
|
4245
|
+
const currentOwner = getControllerSession(store, workId);
|
|
4246
|
+
if (!currentOwner && !terminalGoalComplete) {
|
|
4247
|
+
if (work.status === 'failed' || work.status === 'cancelled' || work.status === 'completed') {
|
|
4248
|
+
throw new Error(`CONTROLLER_RELAY_WORK_TERMINAL: ${work.status}`);
|
|
4249
|
+
}
|
|
4250
|
+
throw new Error(`CONTROLLER_RELAY_ACTIVE_CLAIM_REQUIRED: ${workId}`);
|
|
4251
|
+
}
|
|
4252
|
+
if (currentOwner) {
|
|
4253
|
+
if (currentOwner.controllerType !== 'chatgpt') throw new Error(`CONTROLLER_RELAY_CHATGPT_ONLY: ${workId}`);
|
|
4254
|
+
if (currentOwner.controllerId !== identity.controllerId) throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workId}`);
|
|
4255
|
+
if ((currentOwner.principalId?.trim() || currentOwner.controllerId) !== identity.principalId) {
|
|
4256
|
+
throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${workId}`);
|
|
4257
|
+
}
|
|
4258
|
+
// Finalization may terminalize the Work before the prompt-required
|
|
4259
|
+
// goal_complete disposition is submitted. A frozen MCP client may also
|
|
4260
|
+
// rotate transport sessions between those two calls. Never resume or
|
|
4261
|
+
// rewrite a terminal Work lease here; terminal authorization is fenced
|
|
4262
|
+
// by the already-claimed relay lineage in submitControllerRoundDisposition.
|
|
4263
|
+
if (!terminalGoalComplete) {
|
|
4264
|
+
if (!currentOwner.controllerInstanceId?.trim() || currentOwner.controllerInstanceId.trim() !== identity.controllerInstanceId) {
|
|
4265
|
+
throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${workId}`);
|
|
4266
|
+
}
|
|
4267
|
+
if (currentOwner.sessionId !== identity.sessionId) {
|
|
4268
|
+
resumeControllerSession(store, {
|
|
4269
|
+
workId,
|
|
4270
|
+
controllerId: identity.controllerId,
|
|
4271
|
+
controllerType: 'chatgpt',
|
|
4272
|
+
sessionId: identity.sessionId,
|
|
4273
|
+
principalId: identity.principalId,
|
|
4274
|
+
controllerInstanceId: identity.controllerInstanceId,
|
|
4275
|
+
expectedClaimGeneration: currentOwner.claimGeneration,
|
|
4276
|
+
leaseMs: 3_600_000,
|
|
4277
|
+
});
|
|
4278
|
+
}
|
|
4279
|
+
}
|
|
4280
|
+
}
|
|
4281
|
+
const chatgptBinding = getChatgptWorkConversationBinding(store, workId);
|
|
4282
|
+
const relay = submitControllerRoundDisposition({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, {
|
|
4283
|
+
workId,
|
|
4284
|
+
identity,
|
|
4285
|
+
disposition: disposition as ControllerRoundDisposition,
|
|
4286
|
+
relayScopeId: frozenControllerDisposition?.relayScopeId ?? (typeof args.relay_scope_id === 'string' ? args.relay_scope_id : undefined),
|
|
4287
|
+
requirementId: typeof args.requirement_id === 'string' ? args.requirement_id : undefined,
|
|
4288
|
+
handoffId: typeof args.handoff_id === 'string' ? args.handoff_id : undefined,
|
|
4289
|
+
stateFingerprint: typeof args.state_fingerprint === 'string' ? args.state_fingerprint : undefined,
|
|
4290
|
+
reason: typeof args.reason === 'string' ? args.reason : undefined,
|
|
4291
|
+
browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : chatgptBinding?.latestBrowserSessionId,
|
|
4292
|
+
conversationUrl: typeof args.conversation_url === 'string' ? args.conversation_url : chatgptBinding?.conversationUrl,
|
|
4293
|
+
maxRounds: typeof args.max_rounds === 'number' ? args.max_rounds : undefined,
|
|
4294
|
+
maxRepeatedState: typeof args.max_repeated_state === 'number' ? args.max_repeated_state : undefined,
|
|
4295
|
+
maxFailures: typeof args.max_failures === 'number' ? args.max_failures : undefined,
|
|
4296
|
+
});
|
|
4297
|
+
return result(buildFacadeResult({
|
|
4298
|
+
status: relay.status === 'blocked' ? 'blocked' : 'ok',
|
|
4299
|
+
summary: relay.status === 'pending_release'
|
|
4300
|
+
? `Controller disposition ${relay.disposition} recorded; relay will dispatch only after the current lease is released.`
|
|
4301
|
+
: `Controller disposition ${relay.disposition} recorded with status ${relay.status}.`,
|
|
4302
|
+
data: { relay },
|
|
4303
|
+
}) as unknown as Record<string, unknown>, relay.status === 'blocked');
|
|
4304
|
+
} catch (error) {
|
|
4305
|
+
return result(buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Controller disposition failed.', data: {} }) as unknown as Record<string, unknown>, true);
|
|
4306
|
+
}
|
|
4307
|
+
}
|
|
3739
4308
|
if (operation === 'controller_release') {
|
|
3740
4309
|
try {
|
|
3741
4310
|
const workId = String(args.work_id ?? '').trim();
|
|
3742
4311
|
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
3743
4312
|
const owner = getControllerSession(store, workId);
|
|
3744
|
-
if (owner
|
|
3745
|
-
|
|
4313
|
+
if (owner) {
|
|
4314
|
+
const ownerPrincipal = owner.principalId?.trim() || owner.controllerId;
|
|
4315
|
+
const ownerInstanceId = owner.controllerInstanceId?.trim() || '';
|
|
4316
|
+
if (owner.controllerId !== identity.controllerId) {
|
|
4317
|
+
throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workId} is owned by ${owner.controllerId}`);
|
|
4318
|
+
}
|
|
4319
|
+
if (ownerPrincipal !== identity.principalId) {
|
|
4320
|
+
throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${workId}`);
|
|
4321
|
+
}
|
|
4322
|
+
if (!ownerInstanceId || ownerInstanceId !== identity.controllerInstanceId) {
|
|
4323
|
+
throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${workId}`);
|
|
4324
|
+
}
|
|
4325
|
+
if (typeof owner.claimGeneration !== 'number' || owner.claimGeneration < 1) {
|
|
4326
|
+
throw new Error(`WORK_CONTROLLER_CLAIM_GENERATION_REQUIRED: ${workId}`);
|
|
4327
|
+
}
|
|
4328
|
+
const released = releaseControllerSessionWithAuthority(store, {
|
|
4329
|
+
workId,
|
|
4330
|
+
actor: `controller-release:${identity.controllerId}:${identity.controllerInstanceId}`,
|
|
4331
|
+
authority: {
|
|
4332
|
+
controllerId: owner.controllerId,
|
|
4333
|
+
controllerType: owner.controllerType,
|
|
4334
|
+
principalId: ownerPrincipal,
|
|
4335
|
+
controllerInstanceId: ownerInstanceId,
|
|
4336
|
+
claimGeneration: owner.claimGeneration,
|
|
4337
|
+
},
|
|
4338
|
+
});
|
|
4339
|
+
if (!released.allowed) {
|
|
4340
|
+
throw new Error(`WORK_CONTROLLER_RELEASE_FENCED: ${workId}:${released.reason}`);
|
|
4341
|
+
}
|
|
3746
4342
|
}
|
|
3747
|
-
releaseControllerSession(store, workId, identity.controllerId);
|
|
3748
4343
|
const executionSession = readExecutionSession(ctx.controllerHome, identity);
|
|
3749
4344
|
if (executionSession?.activeWorkId === workId) {
|
|
3750
4345
|
updateExecutionSession(ctx.controllerHome, identity, { activeWorkId: undefined, lastValidatedAt: new Date().toISOString() });
|
|
3751
4346
|
}
|
|
3752
|
-
|
|
4347
|
+
const relayStore = { controllerHome: ctx.controllerHome, repoId: repository.repoId };
|
|
4348
|
+
const relay = owner ? beginControllerRoundRelayAfterRelease(
|
|
4349
|
+
relayStore,
|
|
4350
|
+
{ workId, releasedSession: owner },
|
|
4351
|
+
) : undefined;
|
|
4352
|
+
if (relay?.status === 'dispatching') {
|
|
4353
|
+
try {
|
|
4354
|
+
assertAutomatedOperationAllowed('external_controller_wake', {
|
|
4355
|
+
controller_type: 'chatgpt',
|
|
4356
|
+
relay_scope_id: relay.relayScopeId,
|
|
4357
|
+
requirement_id: relay.requirementId,
|
|
4358
|
+
});
|
|
4359
|
+
const prompt = buildControllerRoundRelayPrompt(relayStore, relay);
|
|
4360
|
+
const dispatched = await runWorkChatgptContinuation({
|
|
4361
|
+
controllerHome: ctx.controllerHome,
|
|
4362
|
+
repoId: repository.repoId,
|
|
4363
|
+
repoRoot: repository.canonicalRoot,
|
|
4364
|
+
workId,
|
|
4365
|
+
prompt,
|
|
4366
|
+
browserSessionId: relay.browserSessionId,
|
|
4367
|
+
conversationUrl: relay.conversationUrl,
|
|
4368
|
+
tabPolicy: 'reuse',
|
|
4369
|
+
});
|
|
4370
|
+
if (dispatched.status === 'failed') throw new Error(dispatched.error?.message ?? 'CONTROLLER_RELAY_DISPATCH_FAILED');
|
|
4371
|
+
// The next prompt is externally committed before this transition is
|
|
4372
|
+
// recorded. Contiguous immediate rounds intentionally retain one
|
|
4373
|
+
// Forge-owned tab, avoiding a close/reopen race and duplicate prompt.
|
|
4374
|
+
recordControllerRoundTabSettlement(relayStore, {
|
|
4375
|
+
workId,
|
|
4376
|
+
status: 'retained_for_immediate_continuation',
|
|
4377
|
+
});
|
|
4378
|
+
const completed = finishControllerRoundRelayDispatch(
|
|
4379
|
+
relayStore,
|
|
4380
|
+
{ workId, ok: true, browserSessionId: dispatched.browserSessionId, conversationUrl: dispatched.conversationUrl },
|
|
4381
|
+
);
|
|
4382
|
+
return result(buildFacadeResult({
|
|
4383
|
+
summary: `Controller lease released and immediate relay ${relay.relayScopeId} dispatched through the canonical ChatGPT launcher.`,
|
|
4384
|
+
data: { relay: completed, dispatch: dispatched },
|
|
4385
|
+
}) as unknown as Record<string, unknown>);
|
|
4386
|
+
} catch (relayError) {
|
|
4387
|
+
const failed = finishControllerRoundRelayDispatch(
|
|
4388
|
+
relayStore,
|
|
4389
|
+
{ workId, ok: false, error: relayError instanceof Error ? relayError.message : String(relayError) },
|
|
4390
|
+
);
|
|
4391
|
+
return result(buildFacadeResult({
|
|
4392
|
+
status: 'blocked',
|
|
4393
|
+
summary: `Controller lease released, but immediate relay dispatch failed: ${relayError instanceof Error ? relayError.message : String(relayError)}`,
|
|
4394
|
+
data: { relay: failed },
|
|
4395
|
+
}) as unknown as Record<string, unknown>, true);
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
const settledRelay = getControllerRoundRelay(relayStore, workId);
|
|
4399
|
+
let tabSettlement;
|
|
4400
|
+
if (
|
|
4401
|
+
owner?.controllerType === 'chatgpt'
|
|
4402
|
+
&& settledRelay
|
|
4403
|
+
&& ['waiting', 'waiting_for_user', 'goal_complete', 'blocked', 'failed'].includes(settledRelay.status)
|
|
4404
|
+
) {
|
|
4405
|
+
const binding = getChatgptWorkConversationBinding(store, workId);
|
|
4406
|
+
const browserSessionId = settledRelay.browserSessionId ?? binding?.latestBrowserSessionId;
|
|
4407
|
+
if (browserSessionId) {
|
|
4408
|
+
tabSettlement = await settleWorkChatgptAutomationTab({
|
|
4409
|
+
controllerHome: ctx.controllerHome,
|
|
4410
|
+
workId,
|
|
4411
|
+
browserSessionId,
|
|
4412
|
+
});
|
|
4413
|
+
recordControllerRoundTabSettlement(relayStore, {
|
|
4414
|
+
workId,
|
|
4415
|
+
status: tabSettlement.status,
|
|
4416
|
+
error: tabSettlement.error?.message,
|
|
4417
|
+
});
|
|
4418
|
+
}
|
|
4419
|
+
}
|
|
4420
|
+
return result(buildFacadeResult({
|
|
4421
|
+
summary: 'Controller lease released.',
|
|
4422
|
+
data: { relay: getControllerRoundRelay(relayStore, workId) ?? relay, tabSettlement },
|
|
4423
|
+
}) as unknown as Record<string, unknown>);
|
|
3753
4424
|
} catch (error) {
|
|
3754
4425
|
return result(buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Controller release failed.', data: {} }) as unknown as Record<string, unknown>, true);
|
|
3755
4426
|
}
|
|
@@ -3758,11 +4429,101 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3758
4429
|
try {
|
|
3759
4430
|
const controllerType = String(args.controller_type ?? 'codex');
|
|
3760
4431
|
if (!['chatgpt', 'codex', 'grok', 'claude'].includes(controllerType)) throw new Error('CONTROLLER_TYPE_INVALID');
|
|
3761
|
-
const
|
|
3762
|
-
|
|
4432
|
+
const workId = String(args.work_id ?? '').trim();
|
|
4433
|
+
const launchArgs = Array.isArray(args.launch_args) ? args.launch_args.map(String) : [];
|
|
4434
|
+
if (controllerType === 'chatgpt') {
|
|
4435
|
+
const work = getWorkContract(store, workId);
|
|
4436
|
+
if (!work) throw new Error(`WORK_NOT_FOUND: ${workId}`);
|
|
4437
|
+
const handoffId = typeof args.handoff_id === 'string' ? args.handoff_id.trim() : '';
|
|
4438
|
+
const handoff = handoffId ? getHandoffItem(store, handoffId) : undefined;
|
|
4439
|
+
const valueForFlag = (flag: string): string | undefined => {
|
|
4440
|
+
const index = launchArgs.indexOf(flag);
|
|
4441
|
+
if (index < 0) return undefined;
|
|
4442
|
+
const value = launchArgs[index + 1];
|
|
4443
|
+
if (!value || value.startsWith('--')) throw new Error(`CHATGPT_LAUNCH_ARG_VALUE_REQUIRED: ${flag}`);
|
|
4444
|
+
return value;
|
|
4445
|
+
};
|
|
4446
|
+
const supportedFlags = new Set(['--model', '--reasoning', '--tab-policy', '--timeout-ms']);
|
|
4447
|
+
for (let index = 0; index < launchArgs.length; index += 2) {
|
|
4448
|
+
const flag = launchArgs[index];
|
|
4449
|
+
if (!flag || !supportedFlags.has(flag)) throw new Error(`CHATGPT_LAUNCH_ARG_UNSUPPORTED: ${flag ?? ''}`);
|
|
4450
|
+
if (!launchArgs[index + 1] || launchArgs[index + 1]!.startsWith('--')) throw new Error(`CHATGPT_LAUNCH_ARG_VALUE_REQUIRED: ${flag}`);
|
|
4451
|
+
}
|
|
4452
|
+
const reasoning = valueForFlag('--reasoning') ?? 'high';
|
|
4453
|
+
if (!['medium', 'high', 'xhigh'].includes(reasoning)) throw new Error(`CHATGPT_LAUNCH_REASONING_INVALID: ${reasoning}`);
|
|
4454
|
+
const tabPolicy = valueForFlag('--tab-policy') ?? 'auto';
|
|
4455
|
+
if (!['auto', 'reuse', 'new'].includes(tabPolicy)) throw new Error(`CHATGPT_LAUNCH_TAB_POLICY_INVALID: ${tabPolicy}`);
|
|
4456
|
+
const timeoutValue = valueForFlag('--timeout-ms');
|
|
4457
|
+
const timeoutMs = timeoutValue === undefined ? undefined : Number(timeoutValue);
|
|
4458
|
+
if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs <= 0)) throw new Error(`CHATGPT_LAUNCH_TIMEOUT_INVALID: ${timeoutValue}`);
|
|
4459
|
+
const continuationPrompt = typeof args.continuation_prompt === 'string' ? args.continuation_prompt.trim() : '';
|
|
4460
|
+
const relay = beginInitialControllerRoundDispatch(
|
|
4461
|
+
{ controllerHome: ctx.controllerHome, repoId: repository.repoId },
|
|
4462
|
+
{
|
|
4463
|
+
workId,
|
|
4464
|
+
identity: authenticatedFacadeControllerIdentity(ctx, args),
|
|
4465
|
+
requirementId: work.requirementId,
|
|
4466
|
+
browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : undefined,
|
|
4467
|
+
conversationUrl: typeof args.conversation_url === 'string' ? args.conversation_url : undefined,
|
|
4468
|
+
},
|
|
4469
|
+
);
|
|
4470
|
+
if (relay.status === 'blocked') {
|
|
4471
|
+
throw new Error(`CONTROLLER_RELAY_LAUNCH_BLOCKED: ${relay.blockedReason ?? relay.relayScopeId}`);
|
|
4472
|
+
}
|
|
4473
|
+
const prompt = [
|
|
4474
|
+
`Continue Forge Work ${work.workId} in repo ${work.repoId}.`,
|
|
4475
|
+
`Objective: ${work.objective}`,
|
|
4476
|
+
`Acceptance: ${work.acceptanceCriteria.join('; ') || 'none declared'}`,
|
|
4477
|
+
`Current status: ${work.status}`,
|
|
4478
|
+
`Controller round: ${relay.relayScopeId}. launcher_start opened this durable round; dispatch success is not semantic completion.`,
|
|
4479
|
+
handoff ? `Handoff: ${handoff.summary}\nNext: ${handoff.recommendedContinuationPrompt ?? handoff.recommendedPrompt}` : '',
|
|
4480
|
+
continuationPrompt ? `Continuation: ${continuationPrompt}` : '',
|
|
4481
|
+
'Treat Forge Work/Plan/evidence as source of truth. Claim the exact Work before any mutation; if ownership cannot be established, do not mutate.',
|
|
4482
|
+
`Before this ChatGPT round ends, submit exactly one rh_work controller_disposition for Work ${work.workId} and relay_scope_id=${relay.relayScopeId}: continue_immediately, wait, wait_for_user (with an active Handoff), or goal_complete. If this client schema is frozen and omits controller_disposition, use rh_work operation=repair with capability_id=controller.disposition:<disposition>:${relay.relayScopeId}; it is mapped to the same canonical disposition path. Ending the response without a disposition does not close the round and may trigger bounded mechanical recovery.`,
|
|
4483
|
+
].filter(Boolean).join('\n');
|
|
4484
|
+
let dispatched: Awaited<ReturnType<typeof runWorkChatgptContinuation>>;
|
|
4485
|
+
try {
|
|
4486
|
+
dispatched = await runWorkChatgptContinuation({
|
|
4487
|
+
controllerHome: ctx.controllerHome,
|
|
4488
|
+
repoId: repository.repoId,
|
|
4489
|
+
repoRoot: repository.canonicalRoot,
|
|
4490
|
+
workId,
|
|
4491
|
+
prompt,
|
|
4492
|
+
browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : undefined,
|
|
4493
|
+
conversationUrl: typeof args.conversation_url === 'string' ? args.conversation_url : undefined,
|
|
4494
|
+
model: valueForFlag('--model') ?? 'gpt-5.6',
|
|
4495
|
+
reasoning: reasoning as 'medium' | 'high' | 'xhigh',
|
|
4496
|
+
tabPolicy: tabPolicy as 'auto' | 'reuse' | 'new',
|
|
4497
|
+
timeoutMs,
|
|
4498
|
+
});
|
|
4499
|
+
if (dispatched.status === 'failed') throw new Error(dispatched.error?.message ?? 'CHATGPT_WORK_CONTINUATION_FAILED');
|
|
4500
|
+
} catch (launchError) {
|
|
4501
|
+
finishControllerRoundRelayDispatch(
|
|
4502
|
+
{ controllerHome: ctx.controllerHome, repoId: repository.repoId },
|
|
4503
|
+
{ workId, ok: false, error: launchError instanceof Error ? launchError.message : String(launchError) },
|
|
4504
|
+
);
|
|
4505
|
+
throw launchError;
|
|
4506
|
+
}
|
|
4507
|
+
const completedRelay = finishControllerRoundRelayDispatch(
|
|
4508
|
+
{ controllerHome: ctx.controllerHome, repoId: repository.repoId },
|
|
4509
|
+
{ workId, ok: true, browserSessionId: dispatched.browserSessionId, conversationUrl: dispatched.conversationUrl },
|
|
4510
|
+
);
|
|
4511
|
+
return result(buildFacadeResult({
|
|
4512
|
+
summary: 'ChatGPT continuation dispatched; wake completion remains pending until the new ChatGPT Controller claims the Work, and semantic closure still requires an explicit disposition.',
|
|
4513
|
+
data: {
|
|
4514
|
+
workId,
|
|
4515
|
+
relay: completedRelay,
|
|
4516
|
+
browserSessionId: dispatched.browserSessionId,
|
|
4517
|
+
conversationUrl: dispatched.conversationUrl,
|
|
4518
|
+
executionPreferenceVerified: dispatched.executionPreferenceVerified,
|
|
4519
|
+
},
|
|
4520
|
+
}) as unknown as Record<string, unknown>);
|
|
4521
|
+
}
|
|
4522
|
+
const launched = await launchSuperController({ work: store, handoff: store }, {
|
|
4523
|
+
controllerType: controllerType as 'codex' | 'grok' | 'claude',
|
|
3763
4524
|
executable: typeof args.executable === 'string' && args.executable.trim() ? args.executable.trim() : undefined,
|
|
3764
|
-
args:
|
|
3765
|
-
workId
|
|
4525
|
+
args: launchArgs,
|
|
4526
|
+
workId,
|
|
3766
4527
|
launchReservationMs: typeof args.launch_reservation_ms === 'number' ? args.launch_reservation_ms : typeof args.lease_ms === 'number' ? args.lease_ms : undefined,
|
|
3767
4528
|
handoffId: typeof args.handoff_id === 'string' ? args.handoff_id : undefined,
|
|
3768
4529
|
browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : undefined,
|
|
@@ -3770,11 +4531,27 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3770
4531
|
continuationPrompt: typeof args.continuation_prompt === 'string' ? args.continuation_prompt : undefined,
|
|
3771
4532
|
cwd: repository.canonicalRoot,
|
|
3772
4533
|
});
|
|
3773
|
-
return result(buildFacadeResult({ summary: `Thin Launcher started ${launched.controllerType}.`, data: { pid: launched.pid, executable: launched.executable, workId
|
|
4534
|
+
return result(buildFacadeResult({ summary: `Thin Launcher started ${launched.controllerType}.`, data: { pid: launched.pid, executable: launched.executable, workId, reservationId: launched.reservationId } }) as unknown as Record<string, unknown>);
|
|
3774
4535
|
} catch (error) {
|
|
3775
4536
|
return result(buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Launcher failed.', data: {} }) as unknown as Record<string, unknown>, true);
|
|
3776
4537
|
}
|
|
3777
4538
|
}
|
|
4539
|
+
if (operation === 'plan_list') {
|
|
4540
|
+
const plans = listPlanContracts({ ...store, status: 'active', limit: typeof args.limit === 'number' ? args.limit : 20 });
|
|
4541
|
+
const facade = buildFacadeResult({
|
|
4542
|
+
summary: `${plans.length} active PlanContract(s) in this repository.`,
|
|
4543
|
+
data: { plans: plans.map(summarizePlanContract), bounded: true },
|
|
4544
|
+
});
|
|
4545
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
4546
|
+
}
|
|
4547
|
+
if (operation === 'plan_get') {
|
|
4548
|
+
const plan = getPlanContract(store, String(args.plan_id ?? ''));
|
|
4549
|
+
const facade = plan
|
|
4550
|
+
? buildFacadeResult({ summary: `PlanContract ${plan.planId} retrieved.`, data: { plan: args.detail_level === 'detail' ? plan : summarizePlanContract(plan) }, detailLevel: args.detail_level === 'detail' ? 'detail' : 'summary' })
|
|
4551
|
+
: buildFacadeResult({ status: 'not_found', summary: `PlanContract ${String(args.plan_id ?? '')} not found.`, data: { planId: String(args.plan_id ?? '') } });
|
|
4552
|
+
return result(facade as unknown as Record<string, unknown>, !plan);
|
|
4553
|
+
}
|
|
4554
|
+
|
|
3778
4555
|
const checks = listControllerChecks(repository.canonicalRoot);
|
|
3779
4556
|
const workloopCtx = {
|
|
3780
4557
|
workStore: store,
|
|
@@ -3786,82 +4563,173 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3786
4563
|
controllerInstanceId: ctx.controllerInstanceId,
|
|
3787
4564
|
availableChecks: checks,
|
|
3788
4565
|
sourceRevision: gitSnapshot(repository.canonicalRoot).head ?? undefined,
|
|
3789
|
-
materializeIsolatedWorkspace: ({ workId, title, baseRef }: { workId: string; title: string; baseRef?: string }) => {
|
|
3790
|
-
const workspace = ensureManagedWorkspace(ctx.controllerHome, repository, {
|
|
4566
|
+
materializeIsolatedWorkspace: ({ workId, title, baseRef, needsDependencies }: { workId: string; title: string; baseRef?: string; needsDependencies?: boolean }) => {
|
|
4567
|
+
const workspace = ensureManagedWorkspace(ctx.controllerHome, repository, {
|
|
4568
|
+
requestId: workId,
|
|
4569
|
+
title,
|
|
4570
|
+
baseRef,
|
|
4571
|
+
prepareDependencies: needsDependencies === true,
|
|
4572
|
+
});
|
|
3791
4573
|
if (!workspace.managed || !workspace.checkoutId || !workspace.root) throw new Error('MANAGED_WORKSPACE_NOT_MATERIALIZED');
|
|
3792
4574
|
return { checkoutId: workspace.checkoutId, root: workspace.root, baseRevision: workspace.baseRevision, managed: true as const };
|
|
3793
4575
|
},
|
|
3794
4576
|
};
|
|
3795
4577
|
|
|
4578
|
+
if (operation === 'requirement_create') {
|
|
4579
|
+
const requirementId = typeof args.requirement_id === 'string' ? args.requirement_id.trim() : '';
|
|
4580
|
+
const title = typeof args.requirement_title === 'string' ? args.requirement_title.trim().slice(0, 500) : '';
|
|
4581
|
+
const outcomeStatement = typeof args.requirement_outcome === 'string' ? args.requirement_outcome.trim().slice(0, 2_000) : '';
|
|
4582
|
+
const acceptanceCriteria = Array.isArray(args.requirement_acceptance_criteria)
|
|
4583
|
+
? args.requirement_acceptance_criteria.map((value) => String(value).trim()).filter(Boolean).slice(0, 50).map((value) => value.slice(0, 500))
|
|
4584
|
+
: [];
|
|
4585
|
+
const requiredDeliveryReferences = Array.isArray(args.requirement_delivery_references)
|
|
4586
|
+
? args.requirement_delivery_references.map((value) => String(value).trim()).filter(Boolean).slice(0, 50).map((value) => value.slice(0, 500))
|
|
4587
|
+
: [];
|
|
4588
|
+
const legacyAliases = Array.isArray(args.requirement_legacy_aliases)
|
|
4589
|
+
? args.requirement_legacy_aliases.map((value) => String(value).trim()).filter(Boolean).slice(0, 20).map((value) => value.slice(0, 160))
|
|
4590
|
+
: [];
|
|
4591
|
+
if (!requirementId || !title || !outcomeStatement) {
|
|
4592
|
+
return result(buildFacadeResult({
|
|
4593
|
+
status: 'blocked',
|
|
4594
|
+
summary: 'REQUIREMENT_CREATE_INPUT_REQUIRED: requirement_id, requirement_title, and requirement_outcome are required.',
|
|
4595
|
+
data: { requirementCreated: false },
|
|
4596
|
+
}) as unknown as Record<string, unknown>, true);
|
|
4597
|
+
}
|
|
4598
|
+
const existing = readRequirement({ controllerHome: ctx.controllerHome }, requirementId)?.value;
|
|
4599
|
+
if (existing) {
|
|
4600
|
+
const identical = existing.title === title
|
|
4601
|
+
&& existing.outcomeStatement === outcomeStatement
|
|
4602
|
+
&& JSON.stringify(existing.acceptanceCriteria) === JSON.stringify(acceptanceCriteria)
|
|
4603
|
+
&& JSON.stringify(existing.requiredDeliveryReferences) === JSON.stringify(requiredDeliveryReferences)
|
|
4604
|
+
&& JSON.stringify(existing.legacyAliases) === JSON.stringify(legacyAliases);
|
|
4605
|
+
if (!identical) {
|
|
4606
|
+
return result(buildFacadeResult({
|
|
4607
|
+
status: 'blocked',
|
|
4608
|
+
summary: `REQUIREMENT_ALREADY_EXISTS_CONFLICT: ${requirementId}. Existing Requirement authority was not changed.`,
|
|
4609
|
+
data: { requirement: existing, requirementCreated: false, admissionDecision: 'existing_conflict' },
|
|
4610
|
+
suggestedNextActions: [],
|
|
4611
|
+
}) as unknown as Record<string, unknown>, true);
|
|
4612
|
+
}
|
|
4613
|
+
return result(buildFacadeResult({
|
|
4614
|
+
summary: `REQUIREMENT_AUTHORITY_REUSED: ${requirementId}. Requirement authority does not imply a Plan; Controller chooses the next action.`,
|
|
4615
|
+
data: { requirement: existing, requirementCreated: false, admissionDecision: 'reuse_existing' },
|
|
4616
|
+
suggestedNextActions: [],
|
|
4617
|
+
}) as unknown as Record<string, unknown>);
|
|
4618
|
+
}
|
|
4619
|
+
const requirement = createRequirement({ controllerHome: ctx.controllerHome }, {
|
|
4620
|
+
requirementId,
|
|
4621
|
+
title,
|
|
4622
|
+
outcomeStatement,
|
|
4623
|
+
acceptanceCriteria,
|
|
4624
|
+
requiredDeliveryReferences,
|
|
4625
|
+
legacyAliases,
|
|
4626
|
+
});
|
|
4627
|
+
return result(buildFacadeResult({
|
|
4628
|
+
summary: `Requirement ${requirementId} created. Requirement authority does not imply a Plan; Controller chooses the next action.`,
|
|
4629
|
+
data: { requirement, requirementCreated: true, admissionDecision: 'created' },
|
|
4630
|
+
suggestedNextActions: [],
|
|
4631
|
+
}) as unknown as Record<string, unknown>);
|
|
4632
|
+
}
|
|
4633
|
+
|
|
3796
4634
|
if (operation.startsWith('plan_')) {
|
|
3797
4635
|
try {
|
|
3798
4636
|
if (operation === 'plan_create') {
|
|
3799
4637
|
const rawSteps = Array.isArray(args.plan_steps) ? args.plan_steps : [];
|
|
3800
4638
|
const requestedRequirementId = typeof args.requirement_id === 'string' && args.requirement_id.trim() ? args.requirement_id.trim() : undefined;
|
|
3801
|
-
const
|
|
3802
|
-
const activePlans = listPlanContracts({ ...store, status: 'active', limit: 100 });
|
|
3803
|
-
const exactScopeAuthority = requestedScopeKey
|
|
3804
|
-
? activePlans.find((candidate) => candidate.scopeKey === requestedScopeKey)
|
|
3805
|
-
: undefined;
|
|
3806
|
-
if (exactScopeAuthority) {
|
|
3807
|
-
const facade = buildFacadeResult({
|
|
3808
|
-
summary: `PLAN_AUTHORITY_REUSED: active Plan ${exactScopeAuthority.planId} already owns scope ${requestedScopeKey}; no duplicate draft was created.`,
|
|
3809
|
-
data: {
|
|
3810
|
-
plan: summarizePlanContract(exactScopeAuthority),
|
|
3811
|
-
executionStarted: false,
|
|
3812
|
-
planContractCreated: false,
|
|
3813
|
-
admissionDecision: 'reuse_existing',
|
|
3814
|
-
resolutionRequired: false,
|
|
3815
|
-
},
|
|
3816
|
-
suggestedNextActions: [{ label: 'Read active Plan', tool: 'rh_work', operation: 'plan_get', payload: { plan_id: exactScopeAuthority.planId }, risk: 'readonly', confidence: 'high' }],
|
|
3817
|
-
});
|
|
3818
|
-
return result(facade as unknown as Record<string, unknown>);
|
|
3819
|
-
}
|
|
3820
|
-
const requirementPlans = requestedRequirementId
|
|
3821
|
-
? activePlans.filter((candidate) => candidate.requirementId === requestedRequirementId)
|
|
3822
|
-
: [];
|
|
3823
|
-
const requestedPlanRelation = args.plan_relation === 'extend' || args.plan_relation === 'parallel'
|
|
4639
|
+
const requestedPlanRelation: 'extend' | 'parallel' | undefined = args.plan_relation === 'extend' || args.plan_relation === 'parallel'
|
|
3824
4640
|
? args.plan_relation
|
|
3825
4641
|
: undefined;
|
|
3826
4642
|
const relatedPlanId = typeof args.related_plan_id === 'string' && args.related_plan_id.trim() ? args.related_plan_id.trim() : undefined;
|
|
3827
|
-
|
|
3828
|
-
? requirementPlans.find((candidate) => candidate.planId === relatedPlanId)
|
|
3829
|
-
: requirementPlans.length === 1 ? requirementPlans[0] : undefined;
|
|
3830
|
-
if (requirementPlans.length > 0 && !requestedPlanRelation) {
|
|
4643
|
+
if (requestedRequirementId && !readRequirement({ controllerHome: ctx.controllerHome }, requestedRequirementId)) {
|
|
3831
4644
|
const facade = buildFacadeResult({
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
planContractCreated: false,
|
|
3836
|
-
admissionDecision: 'resolution_required',
|
|
3837
|
-
resolutionRequired: true,
|
|
3838
|
-
candidates: requirementPlans.slice(0, 8).map(summarizePlanContract),
|
|
3839
|
-
allowedPlanRelations: ['extend', 'parallel'],
|
|
3840
|
-
},
|
|
3841
|
-
suggestedNextActions: requirementPlans.slice(0, 3).map((candidate) => ({ label: `Read ${candidate.planId}`, tool: 'rh_work', operation: 'plan_get', payload: { plan_id: candidate.planId }, risk: 'readonly' as const, confidence: 'high' as const })),
|
|
4645
|
+
status: 'failed',
|
|
4646
|
+
summary: `PLAN_REQUIREMENT_NOT_FOUND: ${requestedRequirementId}. Plan was not persisted; create or reconcile the Requirement authority first.`,
|
|
4647
|
+
data: { executionStarted: false, planContractCreated: false, admissionDecision: 'missing_requirement', requirementId: requestedRequirementId },
|
|
3842
4648
|
});
|
|
3843
|
-
return result(facade as unknown as Record<string, unknown
|
|
4649
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
3844
4650
|
}
|
|
3845
|
-
|
|
3846
|
-
|
|
4651
|
+
const admissionInput = {
|
|
4652
|
+
requirementId: requestedRequirementId,
|
|
4653
|
+
scopeKey: String(args.scope_key ?? ''),
|
|
4654
|
+
planRelation: requestedPlanRelation,
|
|
4655
|
+
relatedPlanId,
|
|
4656
|
+
};
|
|
4657
|
+
const renderPlanAdmission = (admission: ReturnType<typeof resolvePlanAdmission>): CallToolResult | undefined => {
|
|
4658
|
+
if (admission.admissionDecision === 'create_new') return undefined;
|
|
4659
|
+
if (admission.reason === 'exact_scope_authority' && admission.plan) {
|
|
4660
|
+
const facade = buildFacadeResult({
|
|
4661
|
+
summary: `PLAN_AUTHORITY_REUSED: active Plan ${admission.plan.planId} already owns scope ${admission.normalizedScopeKey}; no duplicate draft was created.`,
|
|
4662
|
+
data: {
|
|
4663
|
+
plan: summarizePlanContract(admission.plan),
|
|
4664
|
+
executionStarted: false,
|
|
4665
|
+
planContractCreated: false,
|
|
4666
|
+
admissionDecision: 'reuse_existing',
|
|
4667
|
+
resolutionRequired: false,
|
|
4668
|
+
},
|
|
4669
|
+
suggestedNextActions: [{ label: 'Read active Plan', tool: 'rh_work', operation: 'plan_get', payload: { plan_id: admission.plan.planId }, risk: 'readonly', confidence: 'high' }],
|
|
4670
|
+
});
|
|
4671
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
4672
|
+
}
|
|
4673
|
+
if (admission.reason === 'requirement_relation_required') {
|
|
4674
|
+
const facade = buildFacadeResult({
|
|
4675
|
+
summary: `PLAN_RELATION_RESOLUTION_REQUIRED: Requirement ${requestedRequirementId} already has ${admission.candidates.length} active Plan slice(s). Decide whether this scope extends one of them or is an intentional parallel slice before creating a draft.`,
|
|
4676
|
+
data: {
|
|
4677
|
+
executionStarted: false,
|
|
4678
|
+
planContractCreated: false,
|
|
4679
|
+
admissionDecision: 'resolution_required',
|
|
4680
|
+
resolutionRequired: true,
|
|
4681
|
+
candidates: admission.candidates.map(summarizePlanContract),
|
|
4682
|
+
allowedPlanRelations: admission.allowedPlanRelations ?? ['extend', 'parallel'],
|
|
4683
|
+
},
|
|
4684
|
+
suggestedNextActions: admission.candidates.slice(0, 3).map((candidate) => ({ label: `Read ${candidate.planId}`, tool: 'rh_work', operation: 'plan_get', payload: { plan_id: candidate.planId }, risk: 'readonly' as const, confidence: 'high' as const })),
|
|
4685
|
+
});
|
|
4686
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
4687
|
+
}
|
|
4688
|
+
if (admission.reason === 'extension_target_required') {
|
|
3847
4689
|
const facade = buildFacadeResult({
|
|
3848
4690
|
summary: `PLAN_EXTENSION_TARGET_REQUIRED: select related_plan_id from the active Plan slices for Requirement ${requestedRequirementId}.`,
|
|
3849
|
-
data: { executionStarted: false, planContractCreated: false, admissionDecision: 'resolution_required', resolutionRequired: true, candidates:
|
|
4691
|
+
data: { executionStarted: false, planContractCreated: false, admissionDecision: 'resolution_required', resolutionRequired: true, candidates: admission.candidates.map(summarizePlanContract) },
|
|
3850
4692
|
});
|
|
3851
4693
|
return result(facade as unknown as Record<string, unknown>);
|
|
3852
4694
|
}
|
|
4695
|
+
if (admission.reason === 'extend_existing' && admission.plan) {
|
|
4696
|
+
// plan_create + plan_relation=extend is the atomic serial-replan
|
|
4697
|
+
// path. Admission continues under the same lock so successor
|
|
4698
|
+
// creation and predecessor supersession are persisted together.
|
|
4699
|
+
return undefined;
|
|
4700
|
+
}
|
|
4701
|
+
throw new Error(`PLAN_ADMISSION_RESULT_INVALID: ${admission.admissionDecision}:${admission.reason}`);
|
|
4702
|
+
};
|
|
4703
|
+
const activePlans = listPlanContracts({ ...store, status: 'active', limit: 100 });
|
|
4704
|
+
const preflightAdmission = resolvePlanAdmission(activePlans, admissionInput);
|
|
4705
|
+
const preflightResult = renderPlanAdmission(preflightAdmission);
|
|
4706
|
+
if (preflightResult) return preflightResult;
|
|
4707
|
+
const requestedPlanCheckIds = rawSteps
|
|
4708
|
+
.filter((step): step is Record<string, unknown> => Boolean(step) && typeof step === 'object' && !Array.isArray(step))
|
|
4709
|
+
.flatMap((step) => Array.isArray(step.check_ids) ? step.check_ids.map(String) : []);
|
|
4710
|
+
const normalizedPlanChecks = normalizeCheckIds(requestedPlanCheckIds, checks);
|
|
4711
|
+
if (normalizedPlanChecks.invalidCheckIds.length > 0) {
|
|
3853
4712
|
const facade = buildFacadeResult({
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
4713
|
+
status: 'failed',
|
|
4714
|
+
summary: `PLAN_CHECKS_INVALID: ${normalizedPlanChecks.invalidCheckIds.join(', ')}. Plan was not persisted; select replacement IDs from registeredCheckIds in this response, then request readiness only for the checks you choose.`,
|
|
4715
|
+
data: {
|
|
4716
|
+
executionStarted: false,
|
|
4717
|
+
planContractCreated: false,
|
|
4718
|
+
admissionDecision: 'invalid_checks',
|
|
4719
|
+
normalizedChecks: normalizedPlanChecks,
|
|
4720
|
+
registeredCheckIds: checks.map((check) => check.id).slice(0, 80),
|
|
4721
|
+
},
|
|
4722
|
+
suggestedNextActions: [],
|
|
3857
4723
|
});
|
|
3858
|
-
return result(facade as unknown as Record<string, unknown
|
|
4724
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
3859
4725
|
}
|
|
3860
|
-
const
|
|
4726
|
+
const admitted = await admitPlanContractAsync(store, {
|
|
3861
4727
|
planId: String(args.plan_id ?? ''),
|
|
3862
4728
|
repoId: repository.repoId,
|
|
3863
4729
|
requirementId: requestedRequirementId,
|
|
3864
4730
|
scopeKey: String(args.scope_key ?? ''),
|
|
4731
|
+
planRelation: requestedPlanRelation,
|
|
4732
|
+
relatedPlanId,
|
|
3865
4733
|
sourceRevision: String(args.source_revision ?? ''),
|
|
3866
4734
|
goal: String(args.objective ?? ''),
|
|
3867
4735
|
nonGoals: Array.isArray(args.non_goals) ? args.non_goals.map(String) : undefined,
|
|
@@ -3881,33 +4749,43 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3881
4749
|
acceptanceCriteria: Array.isArray(step.acceptance_criteria) ? step.acceptance_criteria.map(String) : [],
|
|
3882
4750
|
})),
|
|
3883
4751
|
});
|
|
4752
|
+
const racedAdmissionResult = renderPlanAdmission(admitted);
|
|
4753
|
+
if (racedAdmissionResult) return racedAdmissionResult;
|
|
4754
|
+
if (!admitted.plan) throw new Error('PLAN_ADMISSION_CREATE_MISSING_PLAN');
|
|
4755
|
+
const plan = admitted.plan;
|
|
3884
4756
|
const facade = buildFacadeResult({
|
|
3885
|
-
summary: `PlanContract ${plan.planId} created as draft after authority
|
|
4757
|
+
summary: `PlanContract ${plan.planId} created as draft after atomic authority admission; no execution was started.`,
|
|
3886
4758
|
data: { plan: summarizePlanContract(plan), executionStarted: false, planContractCreated: true, admissionDecision: 'create_new' },
|
|
3887
4759
|
suggestedNextActions: [{ label: 'Approve reviewed plan', tool: 'rh_work', operation: 'plan_approve', payload: { plan_id: plan.planId }, risk: 'workspace_write', confidence: 'medium' }],
|
|
3888
4760
|
});
|
|
3889
4761
|
return result(facade as unknown as Record<string, unknown>);
|
|
3890
4762
|
}
|
|
3891
|
-
if (operation === '
|
|
3892
|
-
const
|
|
4763
|
+
if (operation === 'plan_approve') {
|
|
4764
|
+
const plan = await approvePlanContractAsync(store, String(args.plan_id ?? ''));
|
|
3893
4765
|
const facade = buildFacadeResult({
|
|
3894
|
-
summary:
|
|
3895
|
-
data: {
|
|
4766
|
+
summary: `PlanContract ${plan.planId} approved at source revision ${plan.sourceRevision}; execution remains explicit.`,
|
|
4767
|
+
data: { plan: summarizePlanContract(plan), executionStarted: false },
|
|
3896
4768
|
});
|
|
3897
4769
|
return result(facade as unknown as Record<string, unknown>);
|
|
3898
4770
|
}
|
|
3899
|
-
if (operation === '
|
|
3900
|
-
const
|
|
3901
|
-
const
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
4771
|
+
if (operation === 'plan_accept_step') {
|
|
4772
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
4773
|
+
const planId = String(args.plan_id ?? '').trim();
|
|
4774
|
+
const stepId = String(args.plan_step_id ?? '').trim();
|
|
4775
|
+
const rationale = String(args.acceptance_rationale ?? '').trim();
|
|
4776
|
+
const plan = acceptPlanStepEvidence(store, {
|
|
4777
|
+
planId,
|
|
4778
|
+
stepId,
|
|
4779
|
+
reviewer: identity.principalId,
|
|
4780
|
+
rationale,
|
|
4781
|
+
acceptedSourceRevision: workloopCtx.sourceRevision,
|
|
4782
|
+
});
|
|
3908
4783
|
const facade = buildFacadeResult({
|
|
3909
|
-
summary: `
|
|
3910
|
-
data: { plan: summarizePlanContract(plan),
|
|
4784
|
+
summary: `Plan step ${stepId} semantically accepted by the current Controller.`,
|
|
4785
|
+
data: { plan: summarizePlanContract(plan), semanticAcceptanceRecorded: true, reviewer: identity.principalId },
|
|
4786
|
+
suggestedNextActions: plan.status === 'finalized'
|
|
4787
|
+
? []
|
|
4788
|
+
: [{ label: 'Continue the next approved Plan step', tool: 'rh_work', operation: 'plan_get', payload: { plan_id: plan.planId }, risk: 'readonly', confidence: 'high' }],
|
|
3911
4789
|
});
|
|
3912
4790
|
return result(facade as unknown as Record<string, unknown>);
|
|
3913
4791
|
}
|
|
@@ -3949,7 +4827,69 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3949
4827
|
}
|
|
3950
4828
|
|
|
3951
4829
|
if (operation === 'stop') {
|
|
3952
|
-
const
|
|
4830
|
+
const workId = String(args.work_id ?? '').trim();
|
|
4831
|
+
const currentOwner = workId ? getControllerSession(store, workId) : undefined;
|
|
4832
|
+
let terminalizationAuthority: ControllerTerminalizationAuthority | undefined;
|
|
4833
|
+
if (currentOwner) {
|
|
4834
|
+
try {
|
|
4835
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
4836
|
+
const ownerPrincipal = currentOwner.principalId?.trim() || currentOwner.controllerId;
|
|
4837
|
+
const ownerInstanceId = currentOwner.controllerInstanceId?.trim() || '';
|
|
4838
|
+
if (currentOwner.controllerId !== identity.controllerId || ownerPrincipal !== identity.principalId) {
|
|
4839
|
+
throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workId}`);
|
|
4840
|
+
}
|
|
4841
|
+
if (!ownerInstanceId || ownerInstanceId !== identity.controllerInstanceId) {
|
|
4842
|
+
throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${workId}`);
|
|
4843
|
+
}
|
|
4844
|
+
if (typeof currentOwner.claimGeneration !== 'number' || currentOwner.claimGeneration < 1) {
|
|
4845
|
+
throw new Error(`WORK_CONTROLLER_CLAIM_GENERATION_REQUIRED: ${workId}`);
|
|
4846
|
+
}
|
|
4847
|
+
terminalizationAuthority = {
|
|
4848
|
+
controllerId: currentOwner.controllerId,
|
|
4849
|
+
controllerType: currentOwner.controllerType,
|
|
4850
|
+
principalId: ownerPrincipal,
|
|
4851
|
+
controllerInstanceId: ownerInstanceId,
|
|
4852
|
+
claimGeneration: currentOwner.claimGeneration,
|
|
4853
|
+
};
|
|
4854
|
+
} catch (error) {
|
|
4855
|
+
const blocked = buildFacadeResult({
|
|
4856
|
+
status: 'blocked',
|
|
4857
|
+
summary: error instanceof Error ? error.message : `Work ${workId} terminalization authority check failed.`,
|
|
4858
|
+
data: { workId, terminalizationApplied: false },
|
|
4859
|
+
});
|
|
4860
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
4861
|
+
}
|
|
4862
|
+
} else if (!['user', 'system'].includes(String(args.requested_by ?? ''))) {
|
|
4863
|
+
const blocked = buildFacadeResult({
|
|
4864
|
+
status: 'blocked',
|
|
4865
|
+
summary: `WORK_CONTROLLER_OWNER_REQUIRED: ${workId}; unclaimed stop requires explicit requested_by=user or requested_by=system authority.`,
|
|
4866
|
+
data: { workId, terminalizationApplied: false },
|
|
4867
|
+
});
|
|
4868
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
4869
|
+
}
|
|
4870
|
+
|
|
4871
|
+
const fenced = withControllerSessionTerminalizationFence(
|
|
4872
|
+
store,
|
|
4873
|
+
{
|
|
4874
|
+
workId,
|
|
4875
|
+
actor: `rh-work-stop:${terminalizationAuthority?.controllerId ?? String(args.requested_by ?? 'explicit')}`,
|
|
4876
|
+
authority: terminalizationAuthority,
|
|
4877
|
+
},
|
|
4878
|
+
() => runGoalWorkloop({ ...workloopCtx, sourceRevision: workloopCtx.sourceRevision ?? undefined }, 'stop', args),
|
|
4879
|
+
);
|
|
4880
|
+
if (!fenced.allowed) {
|
|
4881
|
+
const blocked = buildFacadeResult({
|
|
4882
|
+
status: 'blocked',
|
|
4883
|
+
summary: `WORK_TERMINALIZATION_AUTHORITY_FENCED: ${workId}:${fenced.reason}`,
|
|
4884
|
+
data: {
|
|
4885
|
+
workId,
|
|
4886
|
+
terminalizationApplied: false,
|
|
4887
|
+
currentClaimGeneration: fenced.owner?.claimGeneration,
|
|
4888
|
+
},
|
|
4889
|
+
});
|
|
4890
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
4891
|
+
}
|
|
4892
|
+
const facade = fenced.value;
|
|
3953
4893
|
if (facade.status !== 'ok') {
|
|
3954
4894
|
return result(facade as unknown as Record<string, unknown>, true);
|
|
3955
4895
|
}
|
|
@@ -3958,20 +4898,25 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3958
4898
|
if (!physical) return result(facade as unknown as Record<string, unknown>);
|
|
3959
4899
|
const cleanup = contextRecord(physical.structuredContent);
|
|
3960
4900
|
const cleanupCompleted = cleanup.cleanupCompleted === true || contextRecord(cleanup.work).state === 'cleaned';
|
|
4901
|
+
const cleanupRetained = cleanup.cleanupRetained === true;
|
|
4902
|
+
const cleanupSettled = cleanupCompleted || cleanupRetained;
|
|
3961
4903
|
const response = {
|
|
3962
4904
|
...facade,
|
|
3963
|
-
status:
|
|
4905
|
+
status: cleanupSettled ? 'ok' : 'blocked',
|
|
3964
4906
|
summary: cleanupCompleted
|
|
3965
4907
|
? `${facade.summary} Managed worktree and branch cleanup completed automatically.`
|
|
3966
|
-
:
|
|
4908
|
+
: cleanupRetained
|
|
4909
|
+
? `${facade.summary} Managed worktree and branch retention was recorded durably; automatic cleanup is disabled for this terminal Work.`
|
|
4910
|
+
: `${facade.summary} Automatic managed-resource cleanup is incomplete and remains visible for retry.`,
|
|
3967
4911
|
data: {
|
|
3968
4912
|
...(facade.data && typeof facade.data === 'object' ? facade.data : {}),
|
|
3969
4913
|
worktreeDeleted: cleanupCompleted,
|
|
3970
|
-
cleanupPending: !
|
|
4914
|
+
cleanupPending: !cleanupSettled,
|
|
4915
|
+
cleanupRetained,
|
|
3971
4916
|
lifecycleCleanup: cleanup,
|
|
3972
4917
|
},
|
|
3973
4918
|
};
|
|
3974
|
-
return result(response as unknown as Record<string, unknown>, !
|
|
4919
|
+
return result(response as unknown as Record<string, unknown>, !cleanupSettled || physical.isError === true);
|
|
3975
4920
|
} catch (error) {
|
|
3976
4921
|
const response = {
|
|
3977
4922
|
...facade,
|
|
@@ -3989,6 +4934,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3989
4934
|
|
|
3990
4935
|
if (operation === 'finalize') {
|
|
3991
4936
|
const workId = String(args.work_id ?? '').trim();
|
|
4937
|
+
if (workId) reconcileTerminalFacadeWorkVerifications(ctx, repository, workId);
|
|
3992
4938
|
let before = workId ? getWorkContract(store, workId) : undefined;
|
|
3993
4939
|
if (before && !before.completionReceipt && args.reconcile_historical_delivery === true) {
|
|
3994
4940
|
try {
|
|
@@ -3998,7 +4944,13 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3998
4944
|
throw new Error(`DIRECT_EDIT_WORK_RECONCILIATION_CONTROLLER_CLAIM_REQUIRED: ${workId}`);
|
|
3999
4945
|
}
|
|
4000
4946
|
const historicalHandle = readWorkHandle(ctx.controllerHome, repository.repoId, workId);
|
|
4001
|
-
if (historicalHandle?.managedWorktree)
|
|
4947
|
+
if (historicalHandle?.managedWorktree) {
|
|
4948
|
+
const managedCleanupComplete = historicalHandle.finalization.merge === 'done'
|
|
4949
|
+
&& historicalHandle.finalization.branchCleanup === 'done'
|
|
4950
|
+
&& historicalHandle.finalization.worktreeCleanup === 'done'
|
|
4951
|
+
&& !existsSync(historicalHandle.worktreePath);
|
|
4952
|
+
if (!managedCleanupComplete) throw new Error(`DIRECT_EDIT_WORK_RECONCILIATION_MANAGED_CLEANUP_REQUIRED: ${workId}`);
|
|
4953
|
+
}
|
|
4002
4954
|
const reconciliation = acceptReviewedDirectEditWorkReconciliation({
|
|
4003
4955
|
controllerHome: ctx.controllerHome,
|
|
4004
4956
|
repoId: repository.repoId,
|
|
@@ -4013,7 +4965,9 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
4013
4965
|
cleanupOwnershipProof: String(args.reconcile_cleanup_proof ?? ''),
|
|
4014
4966
|
});
|
|
4015
4967
|
if (historicalHandle && historicalHandle.state !== 'cleaned') {
|
|
4016
|
-
const finalization: WorkHandleState['finalization'] =
|
|
4968
|
+
const finalization: WorkHandleState['finalization'] = historicalHandle.managedWorktree
|
|
4969
|
+
? historicalHandle.finalization
|
|
4970
|
+
: { validation: 'done', commit: 'done', merge: 'skipped', branchCleanup: 'skipped', worktreeCleanup: 'skipped' };
|
|
4017
4971
|
const delivered = historicalHandle.state === 'committed' || historicalHandle.state === 'merged' || historicalHandle.state === 'failed_terminal_cleanup'
|
|
4018
4972
|
? historicalHandle
|
|
4019
4973
|
: transitionWorkHandle(ctx.controllerHome, historicalHandle, 'committed', { expectedHead: reconciliation.receipt.targetRevision, finalization, failureReason: undefined });
|
|
@@ -4026,7 +4980,33 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
4026
4980
|
return result(blocked as unknown as Record<string, unknown>, true);
|
|
4027
4981
|
}
|
|
4028
4982
|
}
|
|
4029
|
-
|
|
4983
|
+
// Legacy callers could classify repository mutation + Git remote delivery
|
|
4984
|
+
// as remote_effect. A physical WorkHandle proves this is repository delivery,
|
|
4985
|
+
// so keep it on work_finalize instead of demanding a plugin action receipt.
|
|
4986
|
+
// Pure plugin remote effects have no repository WorkHandle and retain the
|
|
4987
|
+
// action-receipt finalization contract.
|
|
4988
|
+
const repositoryDeliveryHandle = workId
|
|
4989
|
+
? readWorkHandle(ctx.controllerHome, repository.repoId, workId)
|
|
4990
|
+
: undefined;
|
|
4991
|
+
if (before?.workKind === 'remote_effect' && !before.completionReceipt && !repositoryDeliveryHandle) {
|
|
4992
|
+
try {
|
|
4993
|
+
before = finalizeRemoteEffectWorkFromActionReceipt(ctx.controllerHome, repository.repoId, workId);
|
|
4994
|
+
} catch (error) {
|
|
4995
|
+
const blocked = buildFacadeResult({
|
|
4996
|
+
status: 'blocked',
|
|
4997
|
+
summary: error instanceof Error ? error.message : 'Remote-effect semantic finalization failed.',
|
|
4998
|
+
data: { workId, lifecycleClosed: false },
|
|
4999
|
+
});
|
|
5000
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
5001
|
+
}
|
|
5002
|
+
}
|
|
5003
|
+
const completedCleanupPending = Boolean(
|
|
5004
|
+
before?.completionReceipt
|
|
5005
|
+
&& args.cleanup !== false
|
|
5006
|
+
&& before.worktreeRef?.trim()
|
|
5007
|
+
&& existsSync(before.worktreeRef),
|
|
5008
|
+
);
|
|
5009
|
+
if (before && before.workKind !== 'local_effect' && (!before.completionReceipt || completedCleanupPending)) {
|
|
4030
5010
|
try {
|
|
4031
5011
|
const physical = await finalizeFacadeWorkHandle(ctx, repository, args, 'finalize');
|
|
4032
5012
|
if (physical?.isError === true) return physical;
|
|
@@ -4041,8 +5021,66 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
4041
5021
|
return result(blocked as unknown as Record<string, unknown>, true);
|
|
4042
5022
|
}
|
|
4043
5023
|
}
|
|
4044
|
-
const
|
|
5024
|
+
const semanticWork = getWorkContract(store, workId);
|
|
5025
|
+
let facade;
|
|
5026
|
+
if (semanticWork && !['completed', 'failed', 'cancelled'].includes(semanticWork.status)) {
|
|
5027
|
+
try {
|
|
5028
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
5029
|
+
let owner = getControllerSession(store, workId);
|
|
5030
|
+
if (!owner) {
|
|
5031
|
+
owner = claimControllerSession(store, {
|
|
5032
|
+
workId,
|
|
5033
|
+
controllerId: identity.controllerId,
|
|
5034
|
+
controllerType: identity.controllerType,
|
|
5035
|
+
sessionId: identity.sessionId,
|
|
5036
|
+
principalId: identity.principalId,
|
|
5037
|
+
controllerInstanceId: identity.controllerInstanceId,
|
|
5038
|
+
expectedClaimGeneration: 0,
|
|
5039
|
+
leaseMs: 3_600_000,
|
|
5040
|
+
});
|
|
5041
|
+
}
|
|
5042
|
+
const ownerPrincipal = owner.principalId?.trim() || owner.controllerId;
|
|
5043
|
+
const ownerInstanceId = owner.controllerInstanceId?.trim() || '';
|
|
5044
|
+
if (owner.controllerId !== identity.controllerId) throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workId}`);
|
|
5045
|
+
if (ownerPrincipal !== identity.principalId) throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${workId}`);
|
|
5046
|
+
if (!ownerInstanceId || ownerInstanceId !== identity.controllerInstanceId) throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${workId}`);
|
|
5047
|
+
if (typeof owner.claimGeneration !== 'number' || owner.claimGeneration < 1) throw new Error(`WORK_CONTROLLER_CLAIM_GENERATION_REQUIRED: ${workId}`);
|
|
5048
|
+
const fenced = withControllerSessionTerminalizationFence(
|
|
5049
|
+
store,
|
|
5050
|
+
{
|
|
5051
|
+
workId,
|
|
5052
|
+
actor: `rh-work-finalize:${identity.controllerId}:${identity.controllerInstanceId}`,
|
|
5053
|
+
authority: {
|
|
5054
|
+
controllerId: owner.controllerId,
|
|
5055
|
+
controllerType: owner.controllerType,
|
|
5056
|
+
principalId: ownerPrincipal,
|
|
5057
|
+
controllerInstanceId: ownerInstanceId,
|
|
5058
|
+
claimGeneration: owner.claimGeneration,
|
|
5059
|
+
},
|
|
5060
|
+
},
|
|
5061
|
+
() => runGoalWorkloop({ ...workloopCtx, sourceRevision: workloopCtx.sourceRevision ?? undefined }, 'finalize', args),
|
|
5062
|
+
);
|
|
5063
|
+
if (!fenced.allowed) {
|
|
5064
|
+
throw new Error(`WORK_TERMINALIZATION_AUTHORITY_FENCED: ${workId}:${fenced.reason}`);
|
|
5065
|
+
}
|
|
5066
|
+
facade = fenced.value;
|
|
5067
|
+
} catch (error) {
|
|
5068
|
+
const blocked = buildFacadeResult({
|
|
5069
|
+
status: 'blocked',
|
|
5070
|
+
summary: error instanceof Error ? error.message : `Work ${workId} semantic finalization authority check failed.`,
|
|
5071
|
+
data: { workId, terminalizationApplied: false, lifecycleClosed: false },
|
|
5072
|
+
});
|
|
5073
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
5074
|
+
}
|
|
5075
|
+
} else {
|
|
5076
|
+
facade = runGoalWorkloop({ ...workloopCtx, sourceRevision: workloopCtx.sourceRevision ?? undefined }, 'finalize', args);
|
|
5077
|
+
}
|
|
4045
5078
|
const completed = getWorkContract(store, workId);
|
|
5079
|
+
// Finalizing a Work proves the Work lifecycle only. A Plan step may aggregate
|
|
5080
|
+
// acceptance criteria that are broader than this Work (for example, a canary
|
|
5081
|
+
// plus a later stabilization soak), so finalize must never synthesize semantic
|
|
5082
|
+
// Plan acceptance. Only the explicit plan_accept_step operation may promote a
|
|
5083
|
+
// validating step to completed after the Controller reviews all criteria.
|
|
4046
5084
|
const lifecycleClosed = Boolean(completed?.completionReceipt)
|
|
4047
5085
|
&& (!readWorkHandle(ctx.controllerHome, repository.repoId, workId)
|
|
4048
5086
|
|| readWorkHandle(ctx.controllerHome, repository.repoId, workId)?.finalization.worktreeCleanup !== 'pending');
|
|
@@ -4057,13 +5095,23 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
4057
5095
|
}
|
|
4058
5096
|
|
|
4059
5097
|
let resumedControllerSession: ReturnType<typeof resumeControllerSession> | undefined;
|
|
5098
|
+
let cancelledWorkReauthorized = false;
|
|
5099
|
+
let reconstructedCancelledCheckout = false;
|
|
5100
|
+
let continuationSourceRevision = workloopCtx.sourceRevision;
|
|
5101
|
+
let continuationWorkspaceFingerprint: string | undefined;
|
|
5102
|
+
let continuationWorkspaceChangedPaths: string[] | undefined;
|
|
4060
5103
|
if (operation === 'continue') {
|
|
4061
5104
|
try {
|
|
4062
5105
|
const workId = String(args.work_id ?? '').trim();
|
|
4063
|
-
|
|
5106
|
+
let work = getWorkContract(store, workId);
|
|
5107
|
+
if (work?.status === 'cancelled') {
|
|
5108
|
+
const resumed = reauthorizeCancelledFacadeWork(ctx, repository, workId, args);
|
|
5109
|
+
cancelledWorkReauthorized = true;
|
|
5110
|
+
reconstructedCancelledCheckout = resumed.reconstructedCheckout;
|
|
5111
|
+
work = getWorkContract(store, workId);
|
|
5112
|
+
}
|
|
4064
5113
|
if (work && !['cancelled', 'completed', 'failed'].includes(work.status)) {
|
|
4065
5114
|
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
4066
|
-
const currentOwner = getControllerSession(store, workId);
|
|
4067
5115
|
resumedControllerSession = resumeControllerSession(store, {
|
|
4068
5116
|
workId,
|
|
4069
5117
|
controllerId: identity.controllerId,
|
|
@@ -4073,6 +5121,23 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
4073
5121
|
controllerInstanceId: identity.controllerInstanceId,
|
|
4074
5122
|
leaseMs: 3_600_000,
|
|
4075
5123
|
});
|
|
5124
|
+
const existingHandle = readWorkHandle(ctx.controllerHome, repository.repoId, workId);
|
|
5125
|
+
if (existingHandle && (existingHandle.principalId !== identity.principalId || existingHandle.sessionId !== identity.sessionId)) {
|
|
5126
|
+
writeWorkHandle(ctx.controllerHome, {
|
|
5127
|
+
...existingHandle,
|
|
5128
|
+
principalId: identity.principalId,
|
|
5129
|
+
sessionId: identity.sessionId,
|
|
5130
|
+
});
|
|
5131
|
+
}
|
|
5132
|
+
if (work.worktreePolicy.required === true && !work.worktreeRef) {
|
|
5133
|
+
materializeFacadeWorkPlacement(ctx, repository, workId, args);
|
|
5134
|
+
}
|
|
5135
|
+
}
|
|
5136
|
+
if (workId) {
|
|
5137
|
+
const reconciled = reconcileTerminalFacadeWorkVerifications(ctx, repository, workId);
|
|
5138
|
+
continuationSourceRevision = reconciled.sourceRevision ?? continuationSourceRevision;
|
|
5139
|
+
continuationWorkspaceFingerprint = reconciled.workspaceFingerprint ?? continuationWorkspaceFingerprint;
|
|
5140
|
+
continuationWorkspaceChangedPaths = reconciled.workspaceChangedPaths ?? continuationWorkspaceChangedPaths;
|
|
4076
5141
|
}
|
|
4077
5142
|
} catch (error) {
|
|
4078
5143
|
const facade = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Controller resume failed.', data: { operation, executionStarted: false, ownershipResumed: false } });
|
|
@@ -4080,11 +5145,30 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
4080
5145
|
}
|
|
4081
5146
|
}
|
|
4082
5147
|
|
|
4083
|
-
const
|
|
5148
|
+
const semanticAdmissionRequired = operation === 'start' && Boolean(
|
|
5149
|
+
(typeof args.plan_id === 'string' && args.plan_id.trim())
|
|
5150
|
+
|| (typeof args.plan_step_id === 'string' && args.plan_step_id.trim())
|
|
5151
|
+
|| (typeof args.requirement_id === 'string' && args.requirement_id.trim())
|
|
5152
|
+
|| (typeof args.related_work_id === 'string' && args.related_work_id.trim())
|
|
5153
|
+
|| (typeof args.work_relation === 'string' && args.work_relation.trim()),
|
|
5154
|
+
);
|
|
5155
|
+
const startContext = {
|
|
5156
|
+
...workloopCtx,
|
|
5157
|
+
sourceRevision: continuationSourceRevision ?? undefined,
|
|
5158
|
+
workspaceFingerprint: continuationWorkspaceFingerprint,
|
|
5159
|
+
workspaceChangedPaths: continuationWorkspaceChangedPaths,
|
|
5160
|
+
semanticAdmissionLocked: semanticAdmissionRequired,
|
|
5161
|
+
};
|
|
5162
|
+
const facade = semanticAdmissionRequired
|
|
5163
|
+
? await withPrimaryWorkAdmissionLockAsync(store, () => runGoalWorkloop(startContext, 'start', args))
|
|
5164
|
+
: runGoalWorkloop(startContext, operation as 'start' | 'continue', args);
|
|
4084
5165
|
const facadeData = facade.data && typeof facade.data === 'object' ? facade.data as Record<string, unknown> : {};
|
|
4085
5166
|
const facadeWorkId = contextText(contextRecord(facadeData.work).workId, 200);
|
|
4086
5167
|
if (facade.status === 'ok' && facadeWorkId) {
|
|
4087
5168
|
try {
|
|
5169
|
+
if (facadeData.workContractCreated === true) {
|
|
5170
|
+
materializeFacadeWorkPlacement(ctx, repository, facadeWorkId, args);
|
|
5171
|
+
}
|
|
4088
5172
|
const handle = ensureFacadeWorkHandle(ctx, repository, facadeWorkId, args);
|
|
4089
5173
|
if (handle) facadeData.executionHandle = { workId: handle.workId, checkoutId: handle.checkoutId, managedWorktree: handle.managedWorktree, state: handle.state };
|
|
4090
5174
|
if (facadeData.workContractCreated === true) {
|
|
@@ -4093,112 +5177,33 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
4093
5177
|
facadeData.ownershipClaimed = true;
|
|
4094
5178
|
}
|
|
4095
5179
|
} catch (error) {
|
|
4096
|
-
const blocked = buildFacadeResult({ status: 'blocked', summary: `WORK_HANDLE_MATERIALIZATION_FAILED: ${error instanceof Error ? error.message : String(error)}`, data: { ...facadeData, workId: facadeWorkId, executionStarted: false } });
|
|
5180
|
+
const blocked = buildFacadeResult({ status: 'blocked', summary: `WORK_HANDLE_MATERIALIZATION_FAILED: ${error instanceof Error ? error.message : String(error)}`, data: { ...facadeData, workId: facadeWorkId, executionStarted: false, canonicalWorkRetained: true } });
|
|
4097
5181
|
return result(blocked as unknown as Record<string, unknown>, true);
|
|
4098
5182
|
}
|
|
4099
5183
|
}
|
|
4100
5184
|
const response = resumedControllerSession
|
|
4101
5185
|
? {
|
|
4102
5186
|
...facade,
|
|
4103
|
-
|
|
5187
|
+
...(cancelledWorkReauthorized
|
|
5188
|
+
? {
|
|
5189
|
+
status: 'ok' as const,
|
|
5190
|
+
summary: `Explicit current-user reauthorization resumed ${resumedControllerSession.workId}; implementation may continue on the exact Work identity.`,
|
|
5191
|
+
}
|
|
5192
|
+
: { summary: `Controller ownership resumed for ${resumedControllerSession.workId}. ${facade.summary}` }),
|
|
4104
5193
|
data: {
|
|
4105
5194
|
...facadeData,
|
|
4106
5195
|
ownershipResumed: true,
|
|
4107
5196
|
controllerSession: resumedControllerSession,
|
|
5197
|
+
...(cancelledWorkReauthorized ? {
|
|
5198
|
+
cancelledWorkReauthorized: true,
|
|
5199
|
+
reconstructedCancelledCheckout,
|
|
5200
|
+
nextStep: 'execute',
|
|
5201
|
+
} : {}),
|
|
4108
5202
|
},
|
|
4109
5203
|
}
|
|
4110
5204
|
: facade;
|
|
4111
5205
|
return result(response as unknown as Record<string, unknown>, response.status === 'blocked' || response.status === 'failed' || response.status === 'not_found');
|
|
4112
5206
|
}
|
|
4113
|
-
case 'work_submit': {
|
|
4114
|
-
const operationArgs = args.arguments && typeof args.arguments === 'object' && !Array.isArray(args.arguments)
|
|
4115
|
-
? { ...(args.arguments as Record<string, unknown>) }
|
|
4116
|
-
: {};
|
|
4117
|
-
const requestedCheckoutId = typeof operationArgs.checkout_id === 'string' && operationArgs.checkout_id.trim()
|
|
4118
|
-
? operationArgs.checkout_id.trim()
|
|
4119
|
-
: undefined;
|
|
4120
|
-
const repository = selected(ctx, requestedCheckoutId ? { ...args, checkout_id: requestedCheckoutId } : args);
|
|
4121
|
-
const requestId = String(args.request_id ?? '').trim();
|
|
4122
|
-
const operation = String(args.operation ?? '').trim();
|
|
4123
|
-
if (!requestId) throw new Error('INVALID_ARGUMENT: work_submit is missing required argument(s): request_id');
|
|
4124
|
-
if (!operation) throw new Error('INVALID_ARGUMENT: work_submit is missing required argument(s): operation');
|
|
4125
|
-
if (operation.startsWith('work_')) {
|
|
4126
|
-
throw new Error('WORK_OPERATION_INVALID: choose an existing durable controller operation');
|
|
4127
|
-
}
|
|
4128
|
-
if (RETIRED_AGENT_OPERATIONS.has(operation)) {
|
|
4129
|
-
throw new Error('AGENT_RUN_RETIRED: Kernel-managed Agent Runs are retired. Accept a WorkContract and launch an external SuperController instead.');
|
|
4130
|
-
}
|
|
4131
|
-
const definition = getMcpToolDefinition(ctx, operation);
|
|
4132
|
-
if (!definition) {
|
|
4133
|
-
throw new Error(`WORK_OPERATION_INVALID: ${operation} is unknown or not eligible for durable execution`);
|
|
4134
|
-
}
|
|
4135
|
-
|
|
4136
|
-
const isRepositoryTool = operation.startsWith('repository_');
|
|
4137
|
-
const workerArgs = schemaAwareWorkSubmitArguments(
|
|
4138
|
-
operation,
|
|
4139
|
-
operationArgs,
|
|
4140
|
-
repository,
|
|
4141
|
-
definition,
|
|
4142
|
-
args.timeout_ms,
|
|
4143
|
-
);
|
|
4144
|
-
// Validate the target operation before any WorkContract / index write.
|
|
4145
|
-
validateMcpToolArguments(operation, injectDurableCommandFields(definition), workerArgs);
|
|
4146
|
-
delete workerArgs.request_id;
|
|
4147
|
-
delete workerArgs.apply_mode;
|
|
4148
|
-
delete workerArgs.wait;
|
|
4149
|
-
delete workerArgs.wait_ms;
|
|
4150
|
-
delete workerArgs.await_result;
|
|
4151
|
-
delete workerArgs.wait_for_result;
|
|
4152
|
-
|
|
4153
|
-
const existingRequest = getWorkContractByRequestId(ctx.controllerHome, requestId);
|
|
4154
|
-
if (existingRequest && existingRequest.repoId !== repository.repoId) {
|
|
4155
|
-
throw new Error(`REQUEST_ID_REPO_CONFLICT: ${requestId} already belongs to repository ${existingRequest.repoId}`);
|
|
4156
|
-
}
|
|
4157
|
-
|
|
4158
|
-
const argumentHash = hashMcpToolArguments(workerArgs);
|
|
4159
|
-
const semanticKey = `${isRepositoryTool ? 'repository-tool' : 'mcp-tool'}:${operation}:${repository.repoId}:${argumentHash}`;
|
|
4160
|
-
const claims = claimsForMcpOperation(operation, workerArgs, repository.repoId, repository.activeCheckoutId);
|
|
4161
|
-
const operationMetadata = operationMetadataForTool(
|
|
4162
|
-
operation,
|
|
4163
|
-
definition,
|
|
4164
|
-
claims,
|
|
4165
|
-
typeof args.timeout_ms === 'number' ? args.timeout_ms : 30_000,
|
|
4166
|
-
workerArgs,
|
|
4167
|
-
repository.defaultBranch,
|
|
4168
|
-
);
|
|
4169
|
-
|
|
4170
|
-
const accepted = acceptSubmittedWorkContract(ctx.controllerHome, {
|
|
4171
|
-
requestId,
|
|
4172
|
-
repoId: repository.repoId,
|
|
4173
|
-
parentWorkId: typeof args.parent_work_id === 'string' && args.parent_work_id.trim() ? args.parent_work_id.trim() : undefined,
|
|
4174
|
-
semanticKey,
|
|
4175
|
-
operation: {
|
|
4176
|
-
name: operation,
|
|
4177
|
-
semanticKey,
|
|
4178
|
-
argumentHash,
|
|
4179
|
-
mode: operationMetadata.mode,
|
|
4180
|
-
idempotent: operationMetadata.idempotent,
|
|
4181
|
-
replayable: operationMetadata.replayable,
|
|
4182
|
-
resourceClaims: operationMetadata.resourceClaims.map((claim) => ({
|
|
4183
|
-
resourceKey: claim.resourceKey,
|
|
4184
|
-
mode: claim.mode,
|
|
4185
|
-
...(claim.quantity !== undefined ? { quantity: claim.quantity } : {}),
|
|
4186
|
-
})),
|
|
4187
|
-
},
|
|
4188
|
-
objective: `Accepted operation ${operation}`,
|
|
4189
|
-
mode: 'direct_control',
|
|
4190
|
-
});
|
|
4191
|
-
|
|
4192
|
-
const work = summarizeSubmittedWorkContract(accepted.contract);
|
|
4193
|
-
return result({
|
|
4194
|
-
accepted: true,
|
|
4195
|
-
deduplicated: accepted.deduplicated,
|
|
4196
|
-
operation,
|
|
4197
|
-
nextAction: work.nextAction,
|
|
4198
|
-
work,
|
|
4199
|
-
workContract: accepted.contract,
|
|
4200
|
-
});
|
|
4201
|
-
}
|
|
4202
5207
|
case 'work_get': {
|
|
4203
5208
|
const repository = selected(ctx, args);
|
|
4204
5209
|
const workId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
|
|
@@ -4318,9 +5323,9 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
4318
5323
|
const job = resolveWorkJob(ctx, repository.repoId, args);
|
|
4319
5324
|
if (!job) {
|
|
4320
5325
|
const processRef = String(args.work_id ?? args.request_id ?? '').trim();
|
|
4321
|
-
const process =
|
|
5326
|
+
const process = getRepositoryCommandProcess(ctx.controllerHome, repository.repoId, processRef);
|
|
4322
5327
|
if (!process) return result({ error: { code: 'WORK_NOT_FOUND', message: 'No Work or managed process matched this repository and identifier.', errorClass: 'not_found', summary: '未找到对应任务。' } }, true);
|
|
4323
|
-
const waitedProcess = await
|
|
5328
|
+
const waitedProcess = await waitRepositoryCommandProcess(ctx.controllerHome, repository.repoId, processRef, { timeoutMs: waitMs });
|
|
4324
5329
|
const digest = managedProcessOperationDigest(waitedProcess);
|
|
4325
5330
|
return result({
|
|
4326
5331
|
work: { kind: 'managed_process', processId: processRef },
|
|
@@ -4642,27 +5647,6 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
4642
5647
|
nonBlocking: true,
|
|
4643
5648
|
});
|
|
4644
5649
|
}
|
|
4645
|
-
case 'controller_context_pack': {
|
|
4646
|
-
const repository = selected(ctx, args);
|
|
4647
|
-
const pack = buildControllerContextPack(repository.canonicalRoot, ctx.policy, {
|
|
4648
|
-
description: typeof args.description === 'string' ? args.description : undefined,
|
|
4649
|
-
issueId: typeof args.issue_id === 'string' ? args.issue_id : undefined,
|
|
4650
|
-
taskId: typeof args.task_id === 'string' ? args.task_id : undefined,
|
|
4651
|
-
knownPaths: stringList(args.known_paths),
|
|
4652
|
-
includeGlobs: stringList(args.include_globs),
|
|
4653
|
-
excludeGlobs: stringList(args.exclude_globs),
|
|
4654
|
-
searchTerms: stringList(args.search_terms),
|
|
4655
|
-
maxFiles: typeof args.max_files === 'number' ? args.max_files : undefined,
|
|
4656
|
-
maxSnippets: typeof args.max_snippets === 'number' ? args.max_snippets : undefined,
|
|
4657
|
-
maxCharsPerSnippet: typeof args.max_chars_per_snippet === 'number' ? args.max_chars_per_snippet : undefined,
|
|
4658
|
-
structuralContext: args.structural_context === 'auto' || args.structural_context === 'required' ? args.structural_context : 'off',
|
|
4659
|
-
});
|
|
4660
|
-
return result({
|
|
4661
|
-
repoId: repository.repoId,
|
|
4662
|
-
repository: repositorySummary(repository),
|
|
4663
|
-
contextPack: pack,
|
|
4664
|
-
});
|
|
4665
|
-
}
|
|
4666
5650
|
case 'controller_context': {
|
|
4667
5651
|
const responseStartedAt = performance.now();
|
|
4668
5652
|
const phaseTimingsMs: Record<string, number> = {};
|
|
@@ -4679,6 +5663,9 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
4679
5663
|
structuralContext: 'required',
|
|
4680
5664
|
maxFiles: 8,
|
|
4681
5665
|
maxSnippets: 20,
|
|
5666
|
+
session: ctx.sessionId?.trim()
|
|
5667
|
+
? { sessionId: ctx.sessionId.trim(), repoId: repository.repoId, checkoutId: repository.activeCheckoutId }
|
|
5668
|
+
: undefined,
|
|
4682
5669
|
})
|
|
4683
5670
|
: undefined;
|
|
4684
5671
|
markPhase('repositoryRouting', repositoryStartedAt);
|
|
@@ -5041,7 +6028,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
5041
6028
|
? 'Raw job state is intentionally not returned through MCP. Use the bounded job summary, events, and get_artifact with artifactId (ART-...), not evidenceId (EVD-...).'
|
|
5042
6029
|
: jobSummary.terminal
|
|
5043
6030
|
? String(jobSummary.summary ?? '')
|
|
5044
|
-
: 'Job is still active.
|
|
6031
|
+
: 'Historical Job is still active. Continue independent work; read it only if an observation can change the next decision, and use work_wait only when this exact result becomes a dependency. Do not periodically poll.',
|
|
5045
6032
|
}, jobSummary.phase === 'failed' || jobSummary.phase === 'timed_out');
|
|
5046
6033
|
}
|
|
5047
6034
|
case 'repository_change_verify': {
|
|
@@ -5289,154 +6276,6 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
5289
6276
|
cancelPendingApprovals: args.cancel_pending_approvals === true,
|
|
5290
6277
|
}) as unknown as Record<string, unknown>);
|
|
5291
6278
|
}
|
|
5292
|
-
case 'goal_create':
|
|
5293
|
-
case 'goal_list':
|
|
5294
|
-
case 'goal_get':
|
|
5295
|
-
case 'goal_start':
|
|
5296
|
-
case 'goal_continue':
|
|
5297
|
-
case 'goal_stop':
|
|
5298
|
-
case 'goal_finalize':
|
|
5299
|
-
case 'goal_status':
|
|
5300
|
-
case 'goal_tick_once':
|
|
5301
|
-
case 'goal_handoff_packet_create':
|
|
5302
|
-
case 'goal_handoff_packet_get':
|
|
5303
|
-
case 'provider_list':
|
|
5304
|
-
case 'provider_health':
|
|
5305
|
-
case 'provider_config_status':
|
|
5306
|
-
case 'executor_route_preview':
|
|
5307
|
-
case 'executor_dispatch':
|
|
5308
|
-
case 'repair_plan':
|
|
5309
|
-
case 'repair_continue': {
|
|
5310
|
-
const repository = selected(ctx, args);
|
|
5311
|
-
if (['goal_create', 'goal_start', 'goal_continue', 'goal_tick_once', 'executor_dispatch', 'repair_continue'].includes(name)) {
|
|
5312
|
-
return result({
|
|
5313
|
-
error: {
|
|
5314
|
-
code: 'GOAL_LOOP_DEPRECATED',
|
|
5315
|
-
message: 'Goal provider dispatch is retired. Use rh_work.plan_create/plan_approve, rh_work.controller_claim, rh_work.launcher_start, and rh_inbox for continuation.',
|
|
5316
|
-
},
|
|
5317
|
-
deprecated: true,
|
|
5318
|
-
migration: ['rh_work.plan_create', 'rh_work.controller_claim', 'rh_work.launcher_start', 'rh_inbox.create'],
|
|
5319
|
-
}, true);
|
|
5320
|
-
}
|
|
5321
|
-
const goalCtx: GoalLoopContext = {
|
|
5322
|
-
goalStore: { controllerHome: ctx.controllerHome, repoId: repository.repoId },
|
|
5323
|
-
packetStore: { controllerHome: ctx.controllerHome, repoId: repository.repoId },
|
|
5324
|
-
repoId: repository.repoId,
|
|
5325
|
-
};
|
|
5326
|
-
const goalId = typeof args.goal_id === 'string' ? args.goal_id : '';
|
|
5327
|
-
const taskIntent = typeof args.task_intent === 'string' ? args.task_intent as TaskIntent : undefined;
|
|
5328
|
-
switch (name) {
|
|
5329
|
-
case 'goal_create': {
|
|
5330
|
-
const goal = goalCreate(goalCtx, {
|
|
5331
|
-
title: String(args.title ?? ''),
|
|
5332
|
-
objective: String(args.objective ?? ''),
|
|
5333
|
-
mode: args.mode === 'manual' || args.mode === 'supervised' || args.mode === 'autonomous' ? args.mode : 'autonomous',
|
|
5334
|
-
issueId: typeof args.issue_id === 'string' ? args.issue_id : undefined,
|
|
5335
|
-
taskIds: Array.isArray(args.task_ids) ? args.task_ids.map(String) : undefined,
|
|
5336
|
-
acceptanceCriteria: Array.isArray(args.acceptance_criteria) ? args.acceptance_criteria.map(String) : undefined,
|
|
5337
|
-
checkIds: Array.isArray(args.check_ids) ? args.check_ids.map(String) : undefined,
|
|
5338
|
-
allowedExecutors: Array.isArray(args.allowed_executors) ? args.allowed_executors.map(String) : undefined,
|
|
5339
|
-
forbiddenExecutors: Array.isArray(args.forbidden_executors) ? args.forbidden_executors.map(String) : undefined,
|
|
5340
|
-
retryBudget: typeof args.retry_budget === 'number' ? args.retry_budget : undefined,
|
|
5341
|
-
});
|
|
5342
|
-
return result({ goal, summary: summarizeGoalPublic(goal) });
|
|
5343
|
-
}
|
|
5344
|
-
case 'goal_list': {
|
|
5345
|
-
const status = typeof args.status === 'string' ? args.status as GoalStatus | 'active' | 'all' : 'active';
|
|
5346
|
-
const goals = goalList(goalCtx, status, typeof args.limit === 'number' ? args.limit : 50);
|
|
5347
|
-
return result({ goals: goals.map(summarizeGoalPublic), count: goals.length });
|
|
5348
|
-
}
|
|
5349
|
-
case 'goal_get': {
|
|
5350
|
-
const goal = goalGet(goalCtx, goalId);
|
|
5351
|
-
if (!goal) return result({ error: { code: 'GOAL_NOT_FOUND', message: `Goal not found: ${goalId}` } }, true);
|
|
5352
|
-
return result({ goal, summary: summarizeGoalPublic(goal) });
|
|
5353
|
-
}
|
|
5354
|
-
case 'goal_start':
|
|
5355
|
-
return result({ tick: goalStart(goalCtx, goalId) });
|
|
5356
|
-
case 'goal_continue':
|
|
5357
|
-
return result({ tick: goalContinue(goalCtx, goalId) });
|
|
5358
|
-
case 'goal_stop':
|
|
5359
|
-
return result({ goal: summarizeGoalPublic(goalStop(goalCtx, goalId, typeof args.reason === 'string' ? args.reason : undefined)) });
|
|
5360
|
-
case 'goal_finalize': {
|
|
5361
|
-
const finalized = goalFinalize(goalCtx, goalId, { force: args.force === true });
|
|
5362
|
-
return result({ ok: finalized.ok, reason: finalized.reason, goal: summarizeGoalPublic(finalized.goal) }, !finalized.ok);
|
|
5363
|
-
}
|
|
5364
|
-
case 'goal_status':
|
|
5365
|
-
return result(goalStatus(goalCtx, goalId || undefined));
|
|
5366
|
-
case 'goal_tick_once': {
|
|
5367
|
-
if (goalId) {
|
|
5368
|
-
return result({
|
|
5369
|
-
tick: goalTickOnce(goalCtx, goalId, {
|
|
5370
|
-
taskIntent,
|
|
5371
|
-
providerFailure: args.provider_failure === true,
|
|
5372
|
-
externalWrite: args.external_write === true,
|
|
5373
|
-
approvalConfirmed: args.approval_confirmed === true,
|
|
5374
|
-
verificationResult: typeof args.verification_check_id === 'string'
|
|
5375
|
-
? {
|
|
5376
|
-
checkId: args.verification_check_id,
|
|
5377
|
-
ok: args.verification_ok === true,
|
|
5378
|
-
}
|
|
5379
|
-
: undefined,
|
|
5380
|
-
}),
|
|
5381
|
-
});
|
|
5382
|
-
}
|
|
5383
|
-
return result({ ticks: tickActiveGoals(goalCtx) });
|
|
5384
|
-
}
|
|
5385
|
-
case 'goal_handoff_packet_create':
|
|
5386
|
-
return result({
|
|
5387
|
-
packet: goalHandoffPacketCreate(goalCtx, goalId, {
|
|
5388
|
-
blockers: Array.isArray(args.blockers) ? args.blockers.map(String) : undefined,
|
|
5389
|
-
requiredUserDecision: typeof args.required_user_decision === 'string' ? args.required_user_decision : undefined,
|
|
5390
|
-
recommendedProvider: typeof args.recommended_provider === 'string' ? args.recommended_provider : undefined,
|
|
5391
|
-
}),
|
|
5392
|
-
});
|
|
5393
|
-
case 'goal_handoff_packet_get': {
|
|
5394
|
-
const packet = goalHandoffPacketGet(goalCtx, String(args.packet_id ?? ''));
|
|
5395
|
-
if (!packet) return result({ error: { code: 'PACKET_NOT_FOUND', message: 'Handoff packet not found.' } }, true);
|
|
5396
|
-
return result({ packet });
|
|
5397
|
-
}
|
|
5398
|
-
case 'provider_list':
|
|
5399
|
-
return result({ providers: providerListAction(goalCtx), policyOwner: 'forge' });
|
|
5400
|
-
case 'provider_health':
|
|
5401
|
-
return result({
|
|
5402
|
-
health: providerHealthAction(goalCtx, typeof args.provider_id === 'string' ? args.provider_id : undefined),
|
|
5403
|
-
redacted: true,
|
|
5404
|
-
});
|
|
5405
|
-
case 'provider_config_status':
|
|
5406
|
-
return result(providerConfigStatusAction(goalCtx));
|
|
5407
|
-
case 'executor_route_preview':
|
|
5408
|
-
return result({
|
|
5409
|
-
route: executorRoutePreview(goalCtx, {
|
|
5410
|
-
goalId: goalId || undefined,
|
|
5411
|
-
taskIntent,
|
|
5412
|
-
risk: typeof args.risk === 'string' ? args.risk as 'readonly' | 'local_repo_write' | 'workspace_write' | 'remote_write' | 'destructive' | 'raw_secret_config' : undefined,
|
|
5413
|
-
objective: typeof args.objective === 'string' ? args.objective : undefined,
|
|
5414
|
-
}),
|
|
5415
|
-
});
|
|
5416
|
-
case 'executor_dispatch':
|
|
5417
|
-
return result(executorDispatch(goalCtx, {
|
|
5418
|
-
goalId,
|
|
5419
|
-
providerId: typeof args.provider_id === 'string' ? args.provider_id : undefined,
|
|
5420
|
-
taskIntent,
|
|
5421
|
-
risk: typeof args.risk === 'string' ? args.risk as 'readonly' | 'local_repo_write' | 'workspace_write' | 'remote_write' | 'destructive' | 'raw_secret_config' : undefined,
|
|
5422
|
-
approvalConfirmed: args.approval_confirmed === true,
|
|
5423
|
-
externalWrite: args.external_write === true,
|
|
5424
|
-
strongConfirmationText: typeof args.strong_confirmation_text === 'string' ? args.strong_confirmation_text : undefined,
|
|
5425
|
-
}));
|
|
5426
|
-
case 'repair_plan':
|
|
5427
|
-
return result(repairPlan(goalCtx, goalId));
|
|
5428
|
-
case 'repair_continue':
|
|
5429
|
-
return result({
|
|
5430
|
-
tick: repairContinue(goalCtx, goalId, {
|
|
5431
|
-
forceFailureClass: typeof args.force_failure_class === 'string'
|
|
5432
|
-
? args.force_failure_class as import('../../control-plane/goal-loop').FailureClass
|
|
5433
|
-
: undefined,
|
|
5434
|
-
}),
|
|
5435
|
-
});
|
|
5436
|
-
default:
|
|
5437
|
-
return result({ error: { code: 'GOAL_LOOP_UNKNOWN', message: name } }, true);
|
|
5438
|
-
}
|
|
5439
|
-
}
|
|
5440
6279
|
case 'workspace_auth_status': {
|
|
5441
6280
|
const repository = selected(ctx, args);
|
|
5442
6281
|
return result(buildWorkspaceAuthStatus(listAssistantPluginManifests(ctx.controllerHome, repository)));
|
|
@@ -5449,66 +6288,6 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
5449
6288
|
redirectUri: typeof args.redirect_uri === 'string' ? args.redirect_uri : undefined,
|
|
5450
6289
|
}));
|
|
5451
6290
|
}
|
|
5452
|
-
case 'assistant_model_readiness': {
|
|
5453
|
-
return result(assistantModelReadiness());
|
|
5454
|
-
}
|
|
5455
|
-
case 'assistant_standing_grants': {
|
|
5456
|
-
const repository = selected(ctx, args);
|
|
5457
|
-
return result(listAssistantStandingGrants(ctx.controllerHome, repository, {
|
|
5458
|
-
status: typeof args.status === 'string' ? args.status as any : undefined,
|
|
5459
|
-
limit: typeof args.limit === 'number' ? args.limit : undefined,
|
|
5460
|
-
}));
|
|
5461
|
-
}
|
|
5462
|
-
case 'assistant_standing_grant_create': {
|
|
5463
|
-
const repository = selected(ctx, args);
|
|
5464
|
-
return result({ grant: createAssistantStandingGrant(ctx.controllerHome, repository, {
|
|
5465
|
-
name: typeof args.name === 'string' ? args.name : undefined,
|
|
5466
|
-
pluginId: String(args.plugin_id ?? '').trim(),
|
|
5467
|
-
actionId: String(args.action_id ?? '').trim(),
|
|
5468
|
-
routineIds: Array.isArray(args.routine_ids) ? args.routine_ids.map(String) : undefined,
|
|
5469
|
-
senderAllowlist: Array.isArray(args.sender_allowlist) ? args.sender_allowlist.map(String) : undefined,
|
|
5470
|
-
subjectContains: Array.isArray(args.subject_contains) ? args.subject_contains.map(String) : undefined,
|
|
5471
|
-
minConfidence: typeof args.min_confidence === 'number' ? args.min_confidence : undefined,
|
|
5472
|
-
maxPerRun: typeof args.max_per_run === 'number' ? args.max_per_run : undefined,
|
|
5473
|
-
expiresInDays: typeof args.expires_in_days === 'number' ? args.expires_in_days : undefined,
|
|
5474
|
-
confirmAuthorization: args.confirm_authorization === true,
|
|
5475
|
-
origin: { surface: 'mcp', actor: 'assistant_standing_grant_create' },
|
|
5476
|
-
}) });
|
|
5477
|
-
}
|
|
5478
|
-
case 'assistant_standing_grant_revoke': {
|
|
5479
|
-
const repository = selected(ctx, args);
|
|
5480
|
-
return result({ grant: revokeAssistantStandingGrant(ctx.controllerHome, repository, {
|
|
5481
|
-
grantId: String(args.grant_id ?? '').trim(),
|
|
5482
|
-
reason: typeof args.reason === 'string' ? args.reason : undefined,
|
|
5483
|
-
confirmAuthorization: args.confirm_authorization === true,
|
|
5484
|
-
origin: { surface: 'mcp', actor: 'assistant_standing_grant_revoke' },
|
|
5485
|
-
}) });
|
|
5486
|
-
}
|
|
5487
|
-
case 'assistant_action_proposals': {
|
|
5488
|
-
const repository = selected(ctx, args);
|
|
5489
|
-
const proposalId = typeof args.proposal_id === 'string' ? args.proposal_id.trim() : '';
|
|
5490
|
-
return result(proposalId
|
|
5491
|
-
? { proposal: getAssistantActionProposal(ctx.controllerHome, repository, proposalId) }
|
|
5492
|
-
: listAssistantActionProposals(ctx.controllerHome, repository, {
|
|
5493
|
-
status: typeof args.status === 'string' ? args.status as any : undefined,
|
|
5494
|
-
limit: typeof args.limit === 'number' ? args.limit : undefined,
|
|
5495
|
-
}));
|
|
5496
|
-
}
|
|
5497
|
-
case 'assistant_action_proposal_resolve': {
|
|
5498
|
-
const repository = selected(ctx, args);
|
|
5499
|
-
const proposalId = String(args.proposal_id ?? '').trim();
|
|
5500
|
-
if (args.decision === 'reject') {
|
|
5501
|
-
return result({ proposal: rejectAssistantActionProposal(ctx.controllerHome, repository, proposalId, typeof args.reason === 'string' ? args.reason : undefined) });
|
|
5502
|
-
}
|
|
5503
|
-
if (args.confirm_authorization !== true) throw new Error('ASSISTANT_ACTION_APPROVAL_REQUIRED: confirm_authorization=true');
|
|
5504
|
-
const requestId = String(args.request_id ?? `assistant-proposal:${proposalId}`).trim();
|
|
5505
|
-
return result({ proposal: await approveAssistantActionProposal(ctx.controllerHome, repository, {
|
|
5506
|
-
proposalId,
|
|
5507
|
-
requestId,
|
|
5508
|
-
confirmationText: typeof args.confirmation_text === 'string' ? args.confirmation_text : undefined,
|
|
5509
|
-
origin: { surface: 'mcp', actor: 'assistant_action_proposal_resolve' },
|
|
5510
|
-
}) });
|
|
5511
|
-
}
|
|
5512
6291
|
case 'external_filesystem_targets_list': {
|
|
5513
6292
|
const repository = selected(ctx, args);
|
|
5514
6293
|
return result(listExternalFilesystemTargets(repository.canonicalRoot));
|
|
@@ -5711,27 +6490,6 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
5711
6490
|
plugin: summarizePlugin(getAssistantPluginManifest(ctx.controllerHome, repository, pluginId)),
|
|
5712
6491
|
});
|
|
5713
6492
|
}
|
|
5714
|
-
case 'assistant_readiness': {
|
|
5715
|
-
const repository = selected(ctx, args);
|
|
5716
|
-
const readiness = buildAssistantReadinessReport(ctx.controllerHome, repository);
|
|
5717
|
-
return result({ ...readiness });
|
|
5718
|
-
}
|
|
5719
|
-
|
|
5720
|
-
case 'gmail_triage_rules': {
|
|
5721
|
-
const repository = selected(ctx, args);
|
|
5722
|
-
return result({ repoId: repository.repoId, checkoutId: repository.activeCheckoutId, path: '.forge/assistant/gmail-triage-rules.json', ...readGmailTriageRules(repository) });
|
|
5723
|
-
}
|
|
5724
|
-
case 'gmail_triage_rule_upsert': {
|
|
5725
|
-
const repository = selected(ctx, args);
|
|
5726
|
-
const upserted = upsertGmailTriageRule(repository, args);
|
|
5727
|
-
return result({ repoId: repository.repoId, checkoutId: repository.activeCheckoutId, ...upserted });
|
|
5728
|
-
}
|
|
5729
|
-
case 'gmail_triage_plan': {
|
|
5730
|
-
const repository = selected(ctx, args);
|
|
5731
|
-
let manifest;
|
|
5732
|
-
try { manifest = getAssistantPluginManifest(ctx.controllerHome, repository, 'gmail'); } catch (_error) { manifest = undefined; }
|
|
5733
|
-
return result(buildGmailTriagePlan(repository, { manifest, items: args.items, query: args.query }) as unknown as Record<string, unknown>);
|
|
5734
|
-
}
|
|
5735
6493
|
case 'review_artifacts_prepare': {
|
|
5736
6494
|
const repository = selected(ctx, args);
|
|
5737
6495
|
return result(ensureReviewArtifactRoots(repository));
|
|
@@ -5752,93 +6510,64 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
5752
6510
|
const repository = selected(ctx, args);
|
|
5753
6511
|
return result(buildWorkflowWatchdogReport(ctx.controllerHome, repository, { staleMinutes: args.stale_minutes, includeProcesses: args.include_processes }) as unknown as Record<string, unknown>);
|
|
5754
6512
|
}
|
|
5755
|
-
case 'ios_xcode_status':
|
|
5756
|
-
return
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
return result({ ...iosSimulatorsList({
|
|
6513
|
+
case 'ios_xcode_status':
|
|
6514
|
+
return legacyIosPluginAction(ctx, name, args, 'xcode_status', {});
|
|
6515
|
+
case 'ios_simulators_list':
|
|
6516
|
+
return legacyIosPluginAction(ctx, name, args, 'list_simulators', {
|
|
5760
6517
|
runtime: typeof args.runtime === 'string' ? args.runtime : undefined,
|
|
5761
6518
|
name: typeof args.name === 'string' ? args.name : undefined,
|
|
5762
|
-
})
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
return
|
|
5767
|
-
}
|
|
5768
|
-
case 'ios_schemes_list': {
|
|
5769
|
-
const repository = selected(ctx, args);
|
|
5770
|
-
return result({ repoId: repository.repoId, ...iosSchemesList(repository, {
|
|
6519
|
+
});
|
|
6520
|
+
case 'ios_project_discover':
|
|
6521
|
+
return legacyIosPluginAction(ctx, name, args, 'discover_project', {});
|
|
6522
|
+
case 'ios_schemes_list':
|
|
6523
|
+
return legacyIosPluginAction(ctx, name, args, 'list_schemes', {
|
|
5771
6524
|
workspace: typeof args.workspace === 'string' ? args.workspace : undefined,
|
|
5772
6525
|
project: typeof args.project === 'string' ? args.project : undefined,
|
|
5773
|
-
})
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
if (args.confirm_authorization !== true) throw new Error('IOS_AUTHORIZATION_REQUIRED: confirm_authorization must be true');
|
|
5777
|
-
return result({ ...iosSimulatorBoot({
|
|
6526
|
+
});
|
|
6527
|
+
case 'ios_simulator_boot':
|
|
6528
|
+
return legacyIosPluginAction(ctx, name, args, 'launch_simulator', {
|
|
5778
6529
|
udid: String(args.udid ?? '').trim(),
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
})
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
const repository = selected(ctx, args);
|
|
5785
|
-
return result({ repoId: repository.repoId, ...iosAppBuild(repository, {
|
|
6530
|
+
open_simulator: args.open_simulator !== false,
|
|
6531
|
+
timeout_ms: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
|
|
6532
|
+
});
|
|
6533
|
+
case 'ios_app_build':
|
|
6534
|
+
return legacyIosPluginAction(ctx, name, args, 'build', {
|
|
5786
6535
|
scheme: String(args.scheme ?? '').trim(),
|
|
5787
6536
|
udid: typeof args.udid === 'string' ? args.udid : undefined,
|
|
5788
|
-
|
|
6537
|
+
simulator_name: typeof args.simulator_name === 'string' ? args.simulator_name : undefined,
|
|
5789
6538
|
workspace: typeof args.workspace === 'string' ? args.workspace : undefined,
|
|
5790
6539
|
project: typeof args.project === 'string' ? args.project : undefined,
|
|
5791
6540
|
configuration: typeof args.configuration === 'string' ? args.configuration : undefined,
|
|
5792
|
-
|
|
5793
|
-
})
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
if (args.confirm_authorization !== true) throw new Error('IOS_AUTHORIZATION_REQUIRED: confirm_authorization must be true');
|
|
5797
|
-
const repository = selected(ctx, args);
|
|
5798
|
-
return result({ repoId: repository.repoId, ...iosAppInstall(repository, {
|
|
5799
|
-
udid: String(args.udid ?? '').trim(),
|
|
5800
|
-
appPath: String(args.app_path ?? '').trim(),
|
|
5801
|
-
}) });
|
|
5802
|
-
}
|
|
5803
|
-
case 'ios_app_launch': {
|
|
5804
|
-
if (args.confirm_authorization !== true) throw new Error('IOS_AUTHORIZATION_REQUIRED: confirm_authorization must be true');
|
|
5805
|
-
return result({ ...iosAppLaunch({
|
|
5806
|
-
udid: String(args.udid ?? '').trim(),
|
|
5807
|
-
bundleId: String(args.bundle_id ?? '').trim(),
|
|
5808
|
-
arguments: Array.isArray(args.arguments) ? args.arguments.map(String) : undefined,
|
|
5809
|
-
}) });
|
|
5810
|
-
}
|
|
5811
|
-
case 'ios_simulator_screenshot': {
|
|
5812
|
-
const repository = selected(ctx, args);
|
|
5813
|
-
return result({ repoId: repository.repoId, ...iosSimulatorScreenshot(repository, {
|
|
6541
|
+
timeout_ms: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
|
|
6542
|
+
});
|
|
6543
|
+
case 'ios_simulator_screenshot':
|
|
6544
|
+
return legacyIosPluginAction(ctx, name, args, 'capture_screenshot', {
|
|
5814
6545
|
udid: String(args.udid ?? '').trim(),
|
|
5815
6546
|
label: typeof args.label === 'string' ? args.label : undefined,
|
|
5816
|
-
})
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
const repository = selected(ctx, args);
|
|
5820
|
-
return result({ repoId: repository.repoId, ...iosSimulatorLogTail(repository, {
|
|
5821
|
-
udid: String(args.udid ?? '').trim(),
|
|
5822
|
-
process: typeof args.process === 'string' ? args.process : undefined,
|
|
5823
|
-
last: typeof args.last === 'string' ? args.last : undefined,
|
|
5824
|
-
maxBytes: typeof args.max_bytes === 'number' ? args.max_bytes : undefined,
|
|
5825
|
-
}) });
|
|
5826
|
-
}
|
|
5827
|
-
case 'ios_ui_smoke_test': {
|
|
5828
|
-
if (args.confirm_authorization !== true) throw new Error('IOS_AUTHORIZATION_REQUIRED: confirm_authorization must be true');
|
|
5829
|
-
const repository = selected(ctx, args);
|
|
5830
|
-
return result({ repoId: repository.repoId, ...iosUiSmokeTest(repository, {
|
|
5831
|
-
udid: typeof args.udid === 'string' ? args.udid : undefined,
|
|
5832
|
-
simulatorName: typeof args.simulator_name === 'string' ? args.simulator_name : undefined,
|
|
6547
|
+
});
|
|
6548
|
+
case 'ios_ui_smoke_test':
|
|
6549
|
+
return legacyIosPluginAction(ctx, name, args, 'smoke_review', {
|
|
5833
6550
|
scheme: typeof args.scheme === 'string' ? args.scheme : undefined,
|
|
5834
|
-
|
|
6551
|
+
bundle_id: typeof args.bundle_id === 'string' ? args.bundle_id : undefined,
|
|
6552
|
+
udid: typeof args.udid === 'string' ? args.udid : undefined,
|
|
6553
|
+
simulator_name: typeof args.simulator_name === 'string' ? args.simulator_name : undefined,
|
|
5835
6554
|
workspace: typeof args.workspace === 'string' ? args.workspace : undefined,
|
|
5836
6555
|
project: typeof args.project === 'string' ? args.project : undefined,
|
|
5837
6556
|
configuration: typeof args.configuration === 'string' ? args.configuration : undefined,
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
})
|
|
5841
|
-
|
|
6557
|
+
app_path: typeof args.app_path === 'string' ? args.app_path : undefined,
|
|
6558
|
+
screenshot_label: typeof args.screenshot_label === 'string' ? args.screenshot_label : undefined,
|
|
6559
|
+
});
|
|
6560
|
+
case 'ios_app_install':
|
|
6561
|
+
case 'ios_app_launch':
|
|
6562
|
+
case 'ios_simulator_log_tail':
|
|
6563
|
+
return result({
|
|
6564
|
+
accepted: false,
|
|
6565
|
+
mode: 'compatibility_migration',
|
|
6566
|
+
path: 'plugin_action_execute',
|
|
6567
|
+
rejectCode: 'LEGACY_IOS_ATOMIC_RETIRED',
|
|
6568
|
+
message: `${name} no longer owns an independent iOS execution path. Use plugin_action_execute with plugin_id=ios and action_id=smoke_review for staged simulator validation.`,
|
|
6569
|
+
migration: { tool: 'plugin_action_execute', plugin_id: 'ios', action_id: 'smoke_review' },
|
|
6570
|
+
}, true);
|
|
5842
6571
|
case 'runtime_cleanup_preview': {
|
|
5843
6572
|
const repository = selected(ctx, args);
|
|
5844
6573
|
const preview = previewRuntimeCleanup(repository.canonicalRoot, {
|
|
@@ -5912,7 +6641,63 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
5912
6641
|
};
|
|
5913
6642
|
return resultWithPluginArtifactImages(value, ctx.controllerHome, repository.repoId, direct.result);
|
|
5914
6643
|
}
|
|
6644
|
+
if (repository.repoId !== '__controller__' && action?.executionMode === 'lightweight_process') {
|
|
6645
|
+
const timeoutMs = Math.max(1_000, request.timeoutMs ?? action?.defaultTimeoutMs ?? 10 * 60_000);
|
|
6646
|
+
let { handle } = await startLightweightPluginAction({
|
|
6647
|
+
controllerHome: ctx.controllerHome,
|
|
6648
|
+
repository,
|
|
6649
|
+
request,
|
|
6650
|
+
interactiveWaitMs: typeof args.interactive_wait_ms === 'number' ? args.interactive_wait_ms : 750,
|
|
6651
|
+
timeoutMs,
|
|
6652
|
+
});
|
|
6653
|
+
if (!handle.completed && args.wait === true) {
|
|
6654
|
+
handle = await waitLightweightPluginAction(
|
|
6655
|
+
ctx.controllerHome,
|
|
6656
|
+
repository.repoId,
|
|
6657
|
+
handle.processId,
|
|
6658
|
+
typeof args.wait_ms === 'number' ? Math.max(1, args.wait_ms) : 15_000,
|
|
6659
|
+
ctx.signal,
|
|
6660
|
+
);
|
|
6661
|
+
}
|
|
6662
|
+
if (!handle.completed) {
|
|
6663
|
+
return result({
|
|
6664
|
+
accepted: true,
|
|
6665
|
+
direct: false,
|
|
6666
|
+
durable: false,
|
|
6667
|
+
mode: 'lightweight_process',
|
|
6668
|
+
plugin: summarizePluginActionReceipt(manifest),
|
|
6669
|
+
action: action ? {
|
|
6670
|
+
actionId: action.actionId,
|
|
6671
|
+
risk: action.risk,
|
|
6672
|
+
confirmation: action.confirmation,
|
|
6673
|
+
requiredConfirmationText: action.requiredConfirmationText,
|
|
6674
|
+
} : { actionId },
|
|
6675
|
+
scope: 'repository',
|
|
6676
|
+
requestId,
|
|
6677
|
+
process: handle,
|
|
6678
|
+
resultRef: { kind: 'process_logs', processId: handle.processId },
|
|
6679
|
+
next: 'The typed plugin action is isolated from the Canonical Runtime. Use process_wait on processId; after completion, call plugin_action_execute again with the same request_id to retrieve the deduplicated structured receipt.',
|
|
6680
|
+
});
|
|
6681
|
+
}
|
|
6682
|
+
if (!handle.ok) {
|
|
6683
|
+
return result({
|
|
6684
|
+
accepted: true,
|
|
6685
|
+
direct: false,
|
|
6686
|
+
durable: false,
|
|
6687
|
+
mode: 'lightweight_process',
|
|
6688
|
+
requestId,
|
|
6689
|
+
process: handle,
|
|
6690
|
+
error: {
|
|
6691
|
+
code: handle.timedOut ? 'PLUGIN_ACTION_TIMEOUT' : handle.cancelled ? 'PLUGIN_ACTION_CANCELLED' : 'PLUGIN_ACTION_FAILED',
|
|
6692
|
+
message: handle.stderrTail || handle.stdoutTail || `Plugin action process exited with code ${String(handle.exitCode)}`,
|
|
6693
|
+
},
|
|
6694
|
+
}, true);
|
|
6695
|
+
}
|
|
6696
|
+
}
|
|
6697
|
+
// The sidecar writes the authoritative receipt. Re-entering the store
|
|
6698
|
+
// with the same request id is a bounded deduplicated read of that result.
|
|
5915
6699
|
const submitted = await submitAssistantPluginAction(ctx.controllerHome, repository, request);
|
|
6700
|
+
const compactResult = compactSubmittedPluginActionResult(submitted.result);
|
|
5916
6701
|
const value = {
|
|
5917
6702
|
accepted: true,
|
|
5918
6703
|
deduplicated: submitted.deduplicated,
|
|
@@ -5929,7 +6714,8 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
5929
6714
|
receiptId: submitted.receipt.receiptId,
|
|
5930
6715
|
requestId: submitted.receipt.requestId,
|
|
5931
6716
|
...(submitted.receipt.workId ? { workId: submitted.receipt.workId } : {}),
|
|
5932
|
-
|
|
6717
|
+
authorization: submitted.authorization,
|
|
6718
|
+
result: compactResult,
|
|
5933
6719
|
detail: {
|
|
5934
6720
|
tool: 'rh_context',
|
|
5935
6721
|
arguments: {
|
|
@@ -5940,7 +6726,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
5940
6726
|
},
|
|
5941
6727
|
next: 'Continue with the returned bounded plugin result; use rh_context capability detail only when the typed action schema/policy is needed.',
|
|
5942
6728
|
};
|
|
5943
|
-
return resultWithPluginArtifactImages(value, ctx.controllerHome, repository.repoId,
|
|
6729
|
+
return resultWithPluginArtifactImages(value, ctx.controllerHome, repository.repoId, compactResult);
|
|
5944
6730
|
}
|
|
5945
6731
|
case 'toolchain_plugin_summary': {
|
|
5946
6732
|
const pluginId = String(args.plugin_id ?? '').trim();
|
|
@@ -5990,7 +6776,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
5990
6776
|
next: continuation.nextSafeAction,
|
|
5991
6777
|
}, contract.status === 'failed' || continuation.reconciliationRequired);
|
|
5992
6778
|
}
|
|
5993
|
-
const process =
|
|
6779
|
+
const process = getRepositoryCommandProcess(ctx.controllerHome, repository.repoId, workRef);
|
|
5994
6780
|
if (!process) return result({ error: { code: 'WORK_NOT_FOUND', message: 'No Work or managed process matched work_ref.', errorClass: 'not_found', summary: '未找到对应任务。' } }, true);
|
|
5995
6781
|
const digest = managedProcessOperationDigest(process);
|
|
5996
6782
|
return result({
|
|
@@ -5999,7 +6785,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
5999
6785
|
taskLedgerStatus: taskLedger.status,
|
|
6000
6786
|
next: process.completed === true
|
|
6001
6787
|
? 'Managed process is terminal; inspect the bounded digest above.'
|
|
6002
|
-
: `
|
|
6788
|
+
: `Continue independent work. Use process_get only if an observation can change the next decision; join once with process_wait when this exact result becomes a dependency. Do not re-run the original operation.`,
|
|
6003
6789
|
}, digest.phase === 'failed' || digest.phase === 'timed_out');
|
|
6004
6790
|
}
|
|
6005
6791
|
case 'model_clients_summary': {
|
|
@@ -6044,70 +6830,6 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
6044
6830
|
model: typeof args.model === 'string' ? args.model : undefined,
|
|
6045
6831
|
}) });
|
|
6046
6832
|
}
|
|
6047
|
-
case 'create_schedule': {
|
|
6048
|
-
const repository = selected(ctx, args);
|
|
6049
|
-
const operation = String(args.operation ?? '').trim();
|
|
6050
|
-
const operationArgs = args.arguments && typeof args.arguments === 'object' ? args.arguments as Record<string, unknown> : {};
|
|
6051
|
-
assertAutomatedOperationAllowed(operation, operationArgs);
|
|
6052
|
-
const scheduleRequestId = typeof args.request_id === 'string' && args.request_id.trim()
|
|
6053
|
-
? args.request_id.trim()
|
|
6054
|
-
: `schedule:auto:${repository.repoId}:${createHash('sha256').update(JSON.stringify({ name: args.name, operation, arguments: operationArgs, everyMinutes: args.every_minutes })).digest('hex').slice(0, 20)}:${Math.floor(Date.now() / (5 * 60_000))}`;
|
|
6055
|
-
const schedule = createSchedule(ctx.controllerHome, {
|
|
6056
|
-
requestId: scheduleRequestId,
|
|
6057
|
-
repoId: repository.repoId,
|
|
6058
|
-
name: String(args.name ?? '').trim(),
|
|
6059
|
-
enabled: true,
|
|
6060
|
-
trigger: {
|
|
6061
|
-
type: ['interval', 'cron', 'calendar', 'condition', 'repository-event', 'dependency-checkpoint', 'manual'].includes(String(args.trigger_type))
|
|
6062
|
-
? String(args.trigger_type) as 'interval' | 'cron' | 'calendar' | 'condition' | 'repository-event' | 'dependency-checkpoint' | 'manual'
|
|
6063
|
-
: typeof args.every_minutes === 'number' ? 'interval' : 'manual',
|
|
6064
|
-
everyMinutes: typeof args.every_minutes === 'number' ? Math.max(1, args.every_minutes) : undefined,
|
|
6065
|
-
cronExpression: typeof args.cron_expression === 'string' ? args.cron_expression : undefined,
|
|
6066
|
-
calendarAt: typeof args.calendar_at === 'string' ? args.calendar_at : undefined,
|
|
6067
|
-
condition: args.condition && typeof args.condition === 'object' ? args.condition as never : undefined,
|
|
6068
|
-
eventName: typeof args.event_name === 'string' ? args.event_name : undefined,
|
|
6069
|
-
dependencyJobIds: Array.isArray(args.dependency_job_ids) ? args.dependency_job_ids.map(String) : undefined,
|
|
6070
|
-
},
|
|
6071
|
-
policy: {
|
|
6072
|
-
maxActiveOccurrences: 1,
|
|
6073
|
-
maxFailures: typeof args.max_failures === 'number' ? Math.max(1, args.max_failures) : 3,
|
|
6074
|
-
cooldownMinutes: typeof args.cooldown_minutes === 'number' ? Math.max(0, args.cooldown_minutes) : 120,
|
|
6075
|
-
dailyBudgetMinutes: typeof args.daily_budget_minutes === 'number' ? Math.max(1, args.daily_budget_minutes) : 180,
|
|
6076
|
-
shadowMode: args.shadow_mode !== false,
|
|
6077
|
-
backoffBaseMinutes: typeof args.backoff_base_minutes === 'number' ? Math.max(1, args.backoff_base_minutes) : 5,
|
|
6078
|
-
backoffMaxMinutes: typeof args.backoff_max_minutes === 'number' ? Math.max(1, args.backoff_max_minutes) : 24 * 60,
|
|
6079
|
-
},
|
|
6080
|
-
action: { operation, arguments: operationArgs, resourceClaims: claimsForMcpOperation(operation, operationArgs, repository.repoId, repository.activeCheckoutId) },
|
|
6081
|
-
stopConditions: Array.isArray(args.stop_conditions) ? args.stop_conditions.map(String) : ['release_ready', 'external_blocker', 'human_review_required'],
|
|
6082
|
-
});
|
|
6083
|
-
return result({ schedule });
|
|
6084
|
-
}
|
|
6085
|
-
case 'list_schedules': {
|
|
6086
|
-
const repository = selected(ctx, args);
|
|
6087
|
-
const schedules = listSchedules(ctx.controllerHome, repository.repoId);
|
|
6088
|
-
if (args.include_occurrences !== true) return result({ schedules });
|
|
6089
|
-
const occurrences = listOccurrences(ctx.controllerHome, repository.repoId, undefined, 100);
|
|
6090
|
-
const decisions = occurrences.flatMap((occurrence) => occurrence.decisionId
|
|
6091
|
-
? [getScheduleDecision(ctx.controllerHome, repository.repoId, occurrence.decisionId)].filter(Boolean)
|
|
6092
|
-
: []);
|
|
6093
|
-
return result({ schedules, occurrences, decisions });
|
|
6094
|
-
}
|
|
6095
|
-
case 'pause_schedule': {
|
|
6096
|
-
const repository = selected(ctx, args);
|
|
6097
|
-
const schedule = getSchedule(ctx.controllerHome, repository.repoId, String(args.schedule_id ?? ''));
|
|
6098
|
-
return result({ schedule: saveSchedule(ctx.controllerHome, { ...schedule, enabled: false, pausedReason: typeof args.reason === 'string' ? args.reason : 'Paused by user.' }) });
|
|
6099
|
-
}
|
|
6100
|
-
case 'trigger_schedule': {
|
|
6101
|
-
const repository = selected(ctx, args);
|
|
6102
|
-
const schedule = getSchedule(ctx.controllerHome, repository.repoId, String(args.schedule_id ?? ''));
|
|
6103
|
-
const occurrence = await evaluateSchedule(ctx.controllerHome, schedule, true, {
|
|
6104
|
-
source: typeof args.event_name === 'string' ? 'repository-event' : 'manual',
|
|
6105
|
-
eventName: typeof args.event_name === 'string' ? args.event_name : undefined,
|
|
6106
|
-
eventId: typeof args.event_id === 'string' ? args.event_id : undefined,
|
|
6107
|
-
data: args.event_data && typeof args.event_data === 'object' ? args.event_data as Record<string, unknown> : undefined,
|
|
6108
|
-
});
|
|
6109
|
-
return result({ occurrence });
|
|
6110
|
-
}
|
|
6111
6833
|
case 'request_release_gate': {
|
|
6112
6834
|
const repository = selected(ctx, args);
|
|
6113
6835
|
const requestId = typeof args.request_id === 'string' && args.request_id.trim()
|
|
@@ -6123,93 +6845,6 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
6123
6845
|
suggestedOperation: 'rh_work.controller_claim followed by Process Runtime checks and explicit release authorization.',
|
|
6124
6846
|
});
|
|
6125
6847
|
}
|
|
6126
|
-
case 'create_portfolio_workflow': {
|
|
6127
|
-
const rawSteps = Array.isArray(args.steps) ? args.steps : [];
|
|
6128
|
-
const workflow = createPortfolioWorkflow(ctx.controllerHome, {
|
|
6129
|
-
name: String(args.name ?? '').trim(),
|
|
6130
|
-
requestId: String(args.request_id ?? '').trim(),
|
|
6131
|
-
failurePolicy: args.failure_policy === 'compensate' ? 'compensate' : 'stop',
|
|
6132
|
-
steps: rawSteps.map((raw, index) => {
|
|
6133
|
-
if (!raw || typeof raw !== 'object') throw new Error(`PORTFOLIO_STEP_INVALID: step ${index + 1}`);
|
|
6134
|
-
const step = raw as Record<string, unknown>;
|
|
6135
|
-
const stepRepoId = String(step.repo_id ?? '').trim();
|
|
6136
|
-
const operation = String(step.operation ?? '').trim();
|
|
6137
|
-
const operationArgs = step.arguments && typeof step.arguments === 'object' ? step.arguments as Record<string, unknown> : {};
|
|
6138
|
-
if (!stepRepoId || !operation) throw new Error(`PORTFOLIO_STEP_INVALID: step ${index + 1} requires repo_id and operation`);
|
|
6139
|
-
assertAutomatedOperationAllowed(operation, operationArgs);
|
|
6140
|
-
const checkoutId = resolveRepositorySelection({ repoId: stepRepoId, controllerHome: ctx.controllerHome, allowSoleRepository: false }).activeCheckoutId;
|
|
6141
|
-
const compensation = step.compensation && typeof step.compensation === 'object'
|
|
6142
|
-
? step.compensation as Record<string, unknown>
|
|
6143
|
-
: undefined;
|
|
6144
|
-
if (compensation) {
|
|
6145
|
-
const compensationOperation = String(compensation.operation ?? '').trim();
|
|
6146
|
-
const compensationArguments = compensation.arguments && typeof compensation.arguments === 'object'
|
|
6147
|
-
? compensation.arguments as Record<string, unknown>
|
|
6148
|
-
: {};
|
|
6149
|
-
assertAutomatedOperationAllowed(compensationOperation, compensationArguments);
|
|
6150
|
-
}
|
|
6151
|
-
return {
|
|
6152
|
-
stepId: String(step.step_id ?? `step-${index + 1}`),
|
|
6153
|
-
repoId: stepRepoId,
|
|
6154
|
-
operation,
|
|
6155
|
-
arguments: operationArgs,
|
|
6156
|
-
dependsOn: Array.isArray(step.depends_on) ? step.depends_on.map(String) : [],
|
|
6157
|
-
priority: ['P0', 'P1', 'P2', 'P3', 'P4'].includes(String(step.priority)) ? String(step.priority) as 'P0' | 'P1' | 'P2' | 'P3' | 'P4' : 'P2',
|
|
6158
|
-
resourceClaims: claimsForMcpOperation(operation, operationArgs, stepRepoId, checkoutId),
|
|
6159
|
-
compensation: compensation ? { operation: String(compensation.operation ?? ''), arguments: compensation.arguments && typeof compensation.arguments === 'object' ? compensation.arguments as Record<string, unknown> : undefined } : undefined,
|
|
6160
|
-
status: 'pending' as const,
|
|
6161
|
-
};
|
|
6162
|
-
}),
|
|
6163
|
-
});
|
|
6164
|
-
return result({ workflow });
|
|
6165
|
-
}
|
|
6166
|
-
case 'list_portfolio_workflows':
|
|
6167
|
-
return result({ workflows: listPortfolioWorkflows(ctx.controllerHome, typeof args.limit === 'number' ? args.limit : 100) });
|
|
6168
|
-
case 'get_portfolio_workflow':
|
|
6169
|
-
return result({ workflow: getPortfolioWorkflow(ctx.controllerHome, String(args.workflow_id ?? '')) });
|
|
6170
|
-
case 'record_candidate_finding': {
|
|
6171
|
-
const repository = selected(ctx, args);
|
|
6172
|
-
const semanticKey = String(args.semantic_key ?? '').trim();
|
|
6173
|
-
const requestId = typeof args.request_id === 'string' && args.request_id.trim()
|
|
6174
|
-
? args.request_id.trim()
|
|
6175
|
-
: `candidate:${repository.repoId}:${createHash('sha256').update(semanticKey).digest('hex').slice(0, 20)}:${Math.floor(Date.now() / (5 * 60_000))}`;
|
|
6176
|
-
const finding = recordCandidateFinding(ctx.controllerHome, {
|
|
6177
|
-
repoId: repository.repoId,
|
|
6178
|
-
requestId,
|
|
6179
|
-
semanticKey,
|
|
6180
|
-
title: String(args.title ?? '').trim(),
|
|
6181
|
-
summary: typeof args.summary === 'string' ? args.summary : undefined,
|
|
6182
|
-
severity: ['low', 'medium', 'high', 'critical'].includes(String(args.severity))
|
|
6183
|
-
? String(args.severity) as 'low' | 'medium' | 'high' | 'critical'
|
|
6184
|
-
: 'medium',
|
|
6185
|
-
evidence: {
|
|
6186
|
-
source: 'mcp',
|
|
6187
|
-
reference: typeof args.reference === 'string' ? args.reference : undefined,
|
|
6188
|
-
details: args.evidence && typeof args.evidence === 'object' ? args.evidence as Record<string, unknown> : undefined,
|
|
6189
|
-
},
|
|
6190
|
-
});
|
|
6191
|
-
return result({ finding });
|
|
6192
|
-
}
|
|
6193
|
-
case 'list_candidate_findings': {
|
|
6194
|
-
const repository = selected(ctx, args);
|
|
6195
|
-
return result({ findings: listCandidateFindings(ctx.controllerHome, repository.repoId, {
|
|
6196
|
-
includeTerminal: args.include_terminal === true,
|
|
6197
|
-
limit: typeof args.limit === 'number' ? args.limit : 100,
|
|
6198
|
-
}) });
|
|
6199
|
-
}
|
|
6200
|
-
case 'promote_candidate_finding': {
|
|
6201
|
-
const repository = selected(ctx, args);
|
|
6202
|
-
const finding = getCandidateFinding(ctx.controllerHome, repository.repoId, String(args.finding_id ?? ''));
|
|
6203
|
-
return result({
|
|
6204
|
-
accepted: false,
|
|
6205
|
-
mode: 'external_controller_required',
|
|
6206
|
-
repoId: repository.repoId,
|
|
6207
|
-
finding,
|
|
6208
|
-
rejectCode: 'EXECUTION_JOB_RETIRED',
|
|
6209
|
-
message: 'Candidate promotion no longer creates an ExecutionJob. An external Controller must create the Issue after claiming the related Work.',
|
|
6210
|
-
suggestedOperation: 'Create or claim WorkContract, then use the explicit issue-creation tool from the external Controller session.',
|
|
6211
|
-
});
|
|
6212
|
-
}
|
|
6213
6848
|
default: return undefined;
|
|
6214
6849
|
}
|
|
6215
6850
|
} catch (error) {
|