@yemi33/minions 0.1.2447 → 0.1.2449
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/bin/cli-api-client.js +1 -1
- package/bin/install-internal-minions.js +1382 -44
- package/bin/install-layout.js +150 -0
- package/bin/minions.js +460 -167
- package/dashboard/docs/typography.md +65 -12
- package/dashboard/js/command-center.js +66 -7
- package/dashboard/js/detail-panel.js +36 -0
- package/dashboard/js/memory-panel.js +59 -12
- package/dashboard/js/qa.js +186 -45
- package/dashboard/js/refresh.js +148 -12
- package/dashboard/js/render-dispatch.js +3 -4
- package/dashboard/js/render-inbox.js +2 -2
- package/dashboard/js/render-other.js +3 -3
- package/dashboard/js/render-pipelines.js +14 -0
- package/dashboard/js/render-plans.js +57 -9
- package/dashboard/js/render-prd.js +132 -23
- package/dashboard/js/render-prs.js +195 -166
- package/dashboard/js/render-schedules.js +63 -3
- package/dashboard/js/render-utils.js +3 -3
- package/dashboard/js/render-watches.js +19 -3
- package/dashboard/js/render-work-items.js +238 -30
- package/dashboard/js/settings.js +205 -54
- package/dashboard/js/utils.js +51 -1
- package/dashboard/pages/home.html +1 -1
- package/dashboard/pages/qa.html +1 -16
- package/dashboard/pages/work.html +40 -0
- package/dashboard/shared/cc-limits.js +79 -0
- package/dashboard/shared/pr-filters.js +21 -38
- package/dashboard/shared/project-git-summary.js +1 -1
- package/dashboard/shared/record-filters.js +169 -0
- package/dashboard/shared/watches-source.js +1 -1
- package/dashboard/shared/welcome-popup.js +1 -1
- package/dashboard/shared/wi-filters.js +302 -0
- package/dashboard/slim/body.html +1 -0
- package/dashboard/slim/js/command-send.js +26 -0
- package/dashboard/slim/js/modals-tiles.js +380 -39
- package/dashboard/slim/js/status.js +13 -21
- package/dashboard/slim/layout.html +1 -0
- package/dashboard/slim/panel-bootstrap.js +6 -2
- package/dashboard/slim/styles.css +38 -0
- package/dashboard/styles.css +159 -55
- package/dashboard-build.js +52 -6
- package/dashboard.js +956 -423
- package/docs/README.md +11 -6
- package/docs/api-errors.md +2 -2
- package/docs/architecture-review-2026-07-09.md +1 -1
- package/docs/architecture.excalidraw +2 -2
- package/docs/auto-discovery.md +18 -9
- package/docs/branch-derivation.md +4 -4
- package/docs/capture-demos.js +39 -2
- package/docs/ci-runner-canary.md +123 -0
- package/docs/claude-md-propagation.md +2 -2
- package/docs/cloud-agent-dispatch.md +204 -0
- package/docs/command-center.md +7 -7
- package/docs/completion-reports.md +43 -20
- package/docs/constants.md +10 -3
- package/docs/constellation-bridge.md +134 -6
- package/docs/constellation-style-telemetry.md +4 -4
- package/docs/contracts/capability-protocol.v1.json +165 -0
- package/docs/cooldown-merge-semantics.md +12 -12
- package/docs/copilot-cli-schema.md +7 -7
- package/docs/cross-repo-plans.md +10 -10
- package/docs/dead-code-audit-retractions.md +5 -5
- package/docs/default-branch-ci.md +173 -0
- package/docs/deprecated.json +31 -31
- package/docs/design-inbox-entries-schema.md +3 -3
- package/docs/design-language.md +1051 -0
- package/docs/design-state-storage.md +11 -11
- package/docs/diagnostics-crash-reports.md +9 -9
- package/docs/diagnostics-memory.md +5 -5
- package/docs/documentation-audit-2026-07-09.md +7 -7
- package/docs/engine-restart.md +90 -5
- package/docs/harness-mode.md +1 -1
- package/docs/internal-install.md +338 -39
- package/docs/kb-dedup-duplicate-pair-investigation.md +5 -5
- package/docs/kb-pr3223-cascade-archiving.md +1 -1
- package/docs/kb-pr696-merge-conflict-docs.md +6 -6
- package/docs/kb-sweep.md +35 -35
- package/docs/keep-processes.md +1 -1
- package/docs/live-checkout-mode.md +30 -30
- package/docs/managed-spawn.md +18 -14
- package/docs/named-agents.md +7 -7
- package/docs/plan-lifecycle.md +69 -2
- package/docs/pr-author-identity.md +114 -0
- package/docs/pr-auto-fix-dispatch.md +19 -4
- package/docs/pr-comment-followup.md +6 -6
- package/docs/pr-review-fix-loop.md +59 -10
- package/docs/process-termination.md +40 -0
- package/docs/proposals/repo-pool-for-live-checkout.md +13 -13
- package/docs/qa-runbook-lifecycle.md +367 -17
- package/docs/qa-runbooks.md +3 -3
- package/docs/rfc-completion-json.md +18 -18
- package/docs/runtime-adapters.md +26 -21
- package/docs/security.md +6 -6
- package/docs/self-improvement.md +4 -4
- package/docs/shared-lifecycle-module-map.md +473 -472
- package/docs/skills.md +52 -3
- package/docs/slim-ux/concepts.md +121 -116
- package/docs/specs/agent-configurability.md +18 -18
- package/docs/specs/agent-rename.md +18 -18
- package/docs/team-memory.md +38 -21
- package/docs/timeouts-and-liveness.md +118 -10
- package/docs/tutorials/01-install-and-connect.md +1 -1
- package/docs/watches.md +40 -39
- package/docs/workspace-manifests.md +4 -4
- package/docs/worktree-lifecycle.md +293 -14
- package/engine/README.md +46 -0
- package/engine/{ado-comment.js → ado/comment.js} +8 -8
- package/engine/{ado-git-auth.js → ado/git-auth.js} +4 -4
- package/engine/{ado.js → ado/index.js} +417 -63
- package/engine/{ado-status.js → ado/status.js} +6 -8
- package/engine/{ado-token.js → ado/token.js} +1 -1
- package/engine/{acp-transport.js → agents/acp-transport.js} +62 -22
- package/engine/{agent-worker-pool.js → agents/agent-worker-pool.js} +17 -8
- package/engine/{cc-worker-pool.js → agents/cc-worker-pool.js} +16 -6
- package/engine/{claude-md-context.js → agents/claude-md-context.js} +5 -5
- package/engine/{harness-context.js → agents/harness-context.js} +5 -5
- package/engine/{harness.js → agents/harness.js} +3 -3
- package/engine/{llm.js → agents/llm.js} +18 -14
- package/engine/{model-discovery.js → agents/model-discovery.js} +2 -2
- package/engine/{playbook.js → agents/playbook.js} +155 -22
- package/engine/{pooled-agent-process.js → agents/pooled-agent-process.js} +14 -12
- package/engine/{preflight.js → agents/preflight.js} +29 -10
- package/engine/{spawn-agent.js → agents/spawn-agent.js} +25 -14
- package/engine/{spawn-phase-watchdog.js → agents/spawn-phase-watchdog.js} +16 -7
- package/engine/{steering.js → agents/steering.js} +5 -5
- package/engine/{tools-inventory.js → agents/tools-inventory.js} +2 -2
- package/engine/{agent-api-validation.js → api/agent-api-validation.js} +2 -2
- package/engine/{api-validation.js → api/api-validation.js} +1 -1
- package/engine/api/bridge.js +787 -0
- package/engine/{cc-api-validation.js → api/cc-api-validation.js} +1 -1
- package/engine/api/companion.js +560 -0
- package/engine/{content-api-validation.js → api/content-api-validation.js} +2 -2
- package/engine/{pr-issue-validation.js → api/pr-issue-validation.js} +33 -6
- package/engine/{settings-validation.js → api/settings-validation.js} +32 -4
- package/engine/api-contracts/agent-content.js +4 -4
- package/engine/api-contracts/capability-manifest.js +236 -0
- package/engine/api-contracts/capability-protocol.js +333 -0
- package/engine/api-contracts/cc-ops.js +1 -1
- package/engine/api-contracts/config-runtime.js +5 -0
- package/engine/api-contracts/core.js +28 -1
- package/engine/api-contracts/index.js +100 -0
- package/engine/api-contracts/orchestration.js +18 -5
- package/engine/api-contracts/pull-requests.js +37 -6
- package/engine/api-contracts/qa-process.js +29 -6
- package/engine/api-contracts/work-plan-prd.js +21 -1
- package/engine/cloud/contract.js +212 -0
- package/engine/cloud/index.js +159 -0
- package/engine/{execution-model.js → core/execution-model.js} +1 -1
- package/engine/{features.js → core/features.js} +4 -4
- package/engine/{operator-identity.js → core/operator-identity.js} +1 -1
- package/engine/{queries.js → core/queries.js} +201 -36
- package/engine/{safe-expr.js → core/safe-expr.js} +1 -1
- package/engine/{shared.js → core/shared.js} +1637 -175
- package/engine/{stdio-timestamps.js → core/stdio-timestamps.js} +1 -1
- package/engine/{untrusted-fence.js → core/untrusted-fence.js} +3 -3
- package/engine/db/index.js +11 -2
- package/engine/db/migrations/002-dispatches.js +3 -3
- package/engine/db/migrations/003-work-items.js +1 -1
- package/engine/db/migrations/004-pull-requests.js +1 -1
- package/engine/db/migrations/006-metrics.js +1 -1
- package/engine/db/migrations/007-watches.js +2 -2
- package/engine/db/migrations/008-small-state.js +1 -1
- package/engine/db/migrations/009-qa.js +1 -1
- package/engine/db/migrations/010-pr-links.js +1 -1
- package/engine/db/migrations/011-remaining-state.js +1 -1
- package/engine/db/migrations/012-steering-deliveries.js +2 -2
- package/engine/db/migrations/013-backfill-broken-note-links.js +1 -1
- package/engine/db/migrations/014-pr-fix-target-prefs.js +2 -2
- package/engine/db/migrations/015-plans-prds.js +0 -0
- package/engine/db/migrations/018-sql-only-cutover.js +2 -2
- package/engine/db/migrations/021-archived-work-items.js +1 -1
- package/engine/db/migrations/022-global-cc-session.js +1 -1
- package/engine/db/migrations/023-engine-state.js +1 -1
- package/engine/db/migrations/025-malformed-work-item-phantoms.js +1 -1
- package/engine/db/migrations/027-review-learning-lifecycle.js +1 -1
- package/engine/db/migrations/029-repair-reused-versions.js +20 -0
- package/engine/db/migrations/031-pr-author-identity.js +137 -0
- package/engine/{consolidation.js → memory/consolidation.js} +6 -6
- package/engine/{kb-sweep-runner.js → memory/kb-sweep-runner.js} +2 -2
- package/engine/{kb-sweep.js → memory/kb-sweep.js} +9 -7
- package/engine/{memory-retrieval.js → memory/memory-retrieval.js} +46 -4
- package/engine/{memory-store.js → memory/memory-store.js} +3 -3
- package/engine/{promotion.js → memory/promotion.js} +3 -3
- package/engine/{review-learning-backfill.js → memory/review-learning-backfill.js} +6 -6
- package/engine/{review-learning.js → memory/review-learning.js} +10 -5
- package/engine/{diagnostics-memory.js → observability/diagnostics-memory.js} +1 -1
- package/engine/{logs-store.js → observability/logs-store.js} +5 -5
- package/engine/{metrics-store.js → observability/metrics-store.js} +4 -4
- package/engine/{check-status.js → operations/check-status.js} +3 -3
- package/engine/{cli.js → operations/cli.js} +271 -113
- package/engine/{distribution.js → operations/distribution.js} +5 -6
- package/engine/{cleanup.js → orchestration/cleanup.js} +72 -45
- package/engine/{cooldown.js → orchestration/cooldown.js} +5 -5
- package/engine/{dispatch-events.js → orchestration/dispatch-events.js} +2 -2
- package/engine/{dispatch.js → orchestration/dispatch.js} +129 -36
- package/engine/orchestration/failed-scheduled-cleanup.js +274 -0
- package/engine/{lifecycle.js → orchestration/lifecycle.js} +198 -90
- package/engine/{meeting.js → orchestration/meeting.js} +6 -16
- package/engine/{pipeline.js → orchestration/pipeline.js} +12 -12
- package/engine/{pre-dispatch-eval.js → orchestration/pre-dispatch-eval.js} +10 -9
- package/engine/{routing.js → orchestration/routing.js} +3 -3
- package/engine/{schedule-bootstrap.js → orchestration/schedule-bootstrap.js} +4 -4
- package/engine/{scheduler.js → orchestration/scheduler.js} +38 -8
- package/engine/{timeout.js → orchestration/timeout.js} +158 -109
- package/engine/{db-events.js → persistence/db-events.js} +2 -2
- package/engine/{dispatch-store.js → persistence/dispatch-store.js} +7 -7
- package/engine/{inbox-store.js → persistence/inbox-store.js} +2 -2
- package/engine/{note-link-backfill.js → persistence/note-link-backfill.js} +4 -4
- package/engine/{pr-fix-target-store.js → persistence/pr-fix-target-store.js} +8 -8
- package/engine/{pull-requests-store.js → persistence/pull-requests-store.js} +21 -7
- package/engine/{small-state-store.js → persistence/small-state-store.js} +31 -31
- package/engine/persistence/state-operations.js +350 -0
- package/engine/{steering-store.js → persistence/steering-store.js} +6 -6
- package/engine/{issues.js → planning/issues.js} +2 -2
- package/engine/{plan-prd-validation.js → planning/plan-prd-validation.js} +8 -2
- package/engine/planning/prd-result-sidecar.js +190 -0
- package/engine/{prd-store.js → planning/prd-store.js} +17 -17
- package/engine/{project-discovery.js → planning/project-discovery.js} +5 -5
- package/engine/{projects.js → planning/projects.js} +10 -10
- package/engine/{resolve-area.js → planning/resolve-area.js} +1 -1
- package/engine/{work-item-validation.js → planning/work-item-validation.js} +39 -3
- package/engine/{work-items-store.js → planning/work-items-store.js} +29 -21
- package/engine/{keep-process-sweep.js → processes/keep-process-sweep.js} +57 -17
- package/engine/{managed-spawn-launcher.js → processes/managed-spawn-launcher.js} +3 -3
- package/engine/{managed-spawn.js → processes/managed-spawn.js} +97 -46
- package/engine/{process-utils.js → processes/process-utils.js} +599 -55
- package/engine/{abandoned-pr-reconciliation.js → providers/abandoned-pr-reconciliation.js} +17 -7
- package/engine/{comment-classifier.js → providers/comment-classifier.js} +85 -17
- package/engine/{comment-format.js → providers/comment-format.js} +5 -5
- package/engine/{gh-comment.js → providers/gh-comment.js} +15 -15
- package/engine/{gh-token.js → providers/gh-token.js} +4 -4
- package/engine/{github.js → providers/github.js} +131 -54
- package/engine/{pr-action.js → providers/pr-action.js} +13 -12
- package/engine/{pr-clone-keep.js → providers/pr-clone-keep.js} +7 -7
- package/engine/{pr-devbox.js → providers/pr-devbox.js} +6 -6
- package/engine/{pr-fix-target.js → providers/pr-fix-target.js} +13 -13
- package/engine/{pr-remote-patch.js → providers/pr-remote-patch.js} +4 -4
- package/engine/{pr-resolve.js → providers/pr-resolve.js} +7 -7
- package/engine/{pr-temp-clone.js → providers/pr-temp-clone.js} +5 -5
- package/engine/{pr-track.js → providers/pr-track.js} +11 -13
- package/engine/{shared-branch-pr-reconcile.js → providers/shared-branch-pr-reconcile.js} +4 -4
- package/engine/qa/auto-prd-qa.js +313 -0
- package/engine/{qa-from-prd.js → qa/from-prd.js} +42 -12
- package/engine/qa/prd-session.js +240 -0
- package/engine/{qa-process-validation.js → qa/process-validation.js} +14 -9
- package/engine/{qa-runbooks.js → qa/runbooks.js} +1 -1
- package/engine/{qa-runs.js → qa/runs.js} +286 -15
- package/engine/{qa-sessions.js → qa/sessions.js} +595 -49
- package/engine/qa/visual-journey.js +654 -0
- package/engine/{qa-runners.js → qa-runners/index.js} +7 -7
- package/engine/qa-runners/maestro.js +3 -3
- package/engine/qa-runners/playwright.js +2 -2
- package/engine/{restart-health.js → recovery/restart-health.js} +48 -4
- package/engine/recovery/stop-stack.js +607 -0
- package/engine/{supervisor.js → recovery/supervisor.js} +105 -175
- package/engine/{watchdog.js → recovery/watchdog.js} +136 -13
- package/engine/runtimes/claude.js +14 -12
- package/engine/runtimes/codex.js +8 -6
- package/engine/runtimes/copilot.js +17 -16
- package/engine/{watch-actions.js → watches/actions.js} +13 -13
- package/engine/{watches.js → watches/index.js} +43 -32
- package/engine/{watches-store.js → watches/store.js} +4 -4
- package/engine/{create-pr-worktree.js → worktrees/create-pr.js} +1 -1
- package/engine/{worktree-gc.js → worktrees/gc.js} +70 -22
- package/engine/worktrees/inventory.js +671 -0
- package/engine/{live-checkout.js → worktrees/live-checkout.js} +4 -4
- package/engine/{worktree-pool.js → worktrees/pool.js} +2 -2
- package/engine/{worktree-preflight.js → worktrees/preflight.js} +1 -0
- package/engine/worktrees/quarantine-refs.js +173 -0
- package/engine.js +1137 -208
- package/minions.js +147 -77
- package/package.json +10 -6
- package/playbooks/_pr-description-audit.md +110 -78
- package/playbooks/build-fix-complex.md +2 -0
- package/playbooks/fix.md +16 -12
- package/playbooks/implement-shared.md +2 -0
- package/playbooks/implement.md +19 -20
- package/playbooks/plan-to-prd.md +18 -3
- package/playbooks/qa-session-draft.md +136 -1
- package/playbooks/qa-session-execute.md +80 -2
- package/playbooks/qa-session-setup.md +17 -1
- package/playbooks/qa-validate.md +1 -1
- package/playbooks/setup.md +2 -0
- package/playbooks/shared-rules.md +25 -32
- package/playbooks/templates/followup-dispatch.md +4 -3
- package/playbooks/verify.md +1 -1
- package/prompts/cc-system.md +19 -27
- package/watch-plugins/README.md +92 -0
- package/watch-plugins/ado-author-prs.js +336 -0
- package/watch-plugins/gh-author-prs.js +375 -0
- package/watch-plugins/http.js +474 -0
- package/watch-plugins/teams-channel.js +869 -0
- package/docs/dev-composite-workflow.md +0 -101
- package/docs/pr-screenshots/pr-886/after-single-header.png +0 -0
- package/docs/pr-screenshots/pr-886/before-duplicate-header.png +0 -0
- package/docs/pr-screenshots/pr-895/01-cancellation-reason-detail.png +0 -0
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-901/projects-tab-default.png +0 -0
- package/docs/pr-screenshots/pr-901/projects-tab-fmf-selected.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-AFTER-crop.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-BEFORE-crop.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-dropdown-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
- package/docs/visual-evidence-ci.md +0 -103
- package/engine/bridge.js +0 -379
- package/engine/quarantine-refs.js +0 -103
- package/engine/state-operations.js +0 -178
- /package/engine/{steering-constraints.js → agents/steering-constraints.js} +0 -0
package/engine.js
CHANGED
|
@@ -24,13 +24,13 @@
|
|
|
24
24
|
// Install ISO timestamp prefixes on console.{log,info,warn,error} so
|
|
25
25
|
// engine-stdio.log is diagnosable to the second. Must run BEFORE any other
|
|
26
26
|
// require that might log during module init.
|
|
27
|
-
require('./engine/stdio-timestamps').installIfNotInstalled();
|
|
27
|
+
require('./engine/core/stdio-timestamps').installIfNotInstalled();
|
|
28
28
|
|
|
29
29
|
const fs = require('fs');
|
|
30
30
|
const path = require('path');
|
|
31
31
|
const os = require('os');
|
|
32
32
|
const crypto = require('crypto');
|
|
33
|
-
const shared = require('./engine/shared');
|
|
33
|
+
const shared = require('./engine/core/shared');
|
|
34
34
|
const { exec, execAsync, execSilent, runFile, ts, ENGINE_DEFAULTS,
|
|
35
35
|
WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, REVIEW_STATUS, DISPATCH_RESULT, AGENT_STATUS,
|
|
36
36
|
FAILURE_CLASS, resolvePollFlag } = shared;
|
|
@@ -40,18 +40,24 @@ const {
|
|
|
40
40
|
buildSpawnFlags,
|
|
41
41
|
prepareWorkspace,
|
|
42
42
|
} = require('./engine/runtimes');
|
|
43
|
-
|
|
43
|
+
// Cloud dispatch provider registry (Agency / Copilot cloud / ADO assignment).
|
|
44
|
+
// A DIFFERENT registry from ./engine/runtimes: runtimes describe a local CLI
|
|
45
|
+
// process, cloud providers describe a remote session that authors its own PR.
|
|
46
|
+
// Engine code resolves through this facade and never imports a provider.
|
|
47
|
+
const cloudProviders = require('./engine/cloud');
|
|
48
|
+
const { assertStaleHeadOk } = require('./engine/agents/spawn-agent');
|
|
44
49
|
// P-1d8f0b93 — fleet ACP worker pool + its child_process-shaped facade.
|
|
45
|
-
// NOTE: engine/agent-worker-pool.js is a DIFFERENT module from the
|
|
50
|
+
// NOTE: engine/agents/agent-worker-pool.js is a DIFFERENT module from the
|
|
46
51
|
// dashboard's single-tab CC pool (engine.js must never import that one —
|
|
47
52
|
// see the cc-tab-pool wiring regression guard test).
|
|
48
|
-
const agentWorkerPool = require('./engine/agent-worker-pool');
|
|
49
|
-
const { PooledAgentProcess } = require('./engine/pooled-agent-process');
|
|
50
|
-
const { normalizeExecutionModel } = require('./engine/execution-model');
|
|
51
|
-
const adoGitAuth = require('./engine/ado
|
|
52
|
-
const queries = require('./engine/queries');
|
|
53
|
-
const dispatchEvents = require('./engine/dispatch-events');
|
|
54
|
-
const prdStore = require('./engine/prd-store');
|
|
53
|
+
const agentWorkerPool = require('./engine/agents/agent-worker-pool');
|
|
54
|
+
const { PooledAgentProcess } = require('./engine/agents/pooled-agent-process');
|
|
55
|
+
const { normalizeExecutionModel } = require('./engine/core/execution-model');
|
|
56
|
+
const adoGitAuth = require('./engine/ado/git-auth');
|
|
57
|
+
const queries = require('./engine/core/queries');
|
|
58
|
+
const dispatchEvents = require('./engine/orchestration/dispatch-events');
|
|
59
|
+
const prdStore = require('./engine/planning/prd-store');
|
|
60
|
+
const prdResultSidecar = require('./engine/planning/prd-result-sidecar');
|
|
55
61
|
|
|
56
62
|
// ─── Paths ──────────────────────────────────────────────────────────────────
|
|
57
63
|
|
|
@@ -132,66 +138,67 @@ const CHECKPOINT_CAP_FAIL_REASON = 'Exceeded 3 checkpoint-resumes; manual interv
|
|
|
132
138
|
// re-aliased here for the existing call sites in this file.
|
|
133
139
|
const READ_ONLY_ROOT_TASK_TYPES = shared.READ_ONLY_ROOT_TASK_TYPES;
|
|
134
140
|
|
|
135
|
-
// ─── Dispatch Management (extracted to engine/dispatch.js) ───────────────────
|
|
141
|
+
// ─── Dispatch Management (extracted to engine/orchestration/dispatch.js) ───────────────────
|
|
136
142
|
|
|
137
143
|
const { mutateDispatch, addToDispatch, addToDispatchWithValidation, isRetryableFailureReason,
|
|
138
|
-
deferDispatchForWorktreeProtection, completeDispatch,
|
|
139
|
-
writeInboxAlert, updateAgentStatus, pruneStalePrDispatches, pruneStalePrDispatchesAsync } = require('./engine/dispatch');
|
|
144
|
+
deferDispatchForWorktreeProtection, completeDispatch, deferDispatchToPending,
|
|
145
|
+
writeInboxAlert, updateAgentStatus, pruneStalePrDispatches, pruneStalePrDispatchesAsync } = require('./engine/orchestration/dispatch');
|
|
140
146
|
|
|
141
|
-
// ─── Timeout / Steering / Idle (extracted to engine/timeout.js) ──────────────
|
|
147
|
+
// ─── Timeout / Steering / Idle (extracted to engine/orchestration/timeout.js) ──────────────
|
|
142
148
|
|
|
143
|
-
const { checkTimeouts, checkSteering, checkIdleThreshold, dropSteeringForPurgedSession } = require('./engine/timeout');
|
|
144
|
-
const steering = require('./engine/steering');
|
|
149
|
+
const { checkTimeouts, checkSteering, checkIdleThreshold, dropSteeringForPurgedSession } = require('./engine/orchestration/timeout');
|
|
150
|
+
const steering = require('./engine/agents/steering');
|
|
145
151
|
|
|
146
|
-
// ─── Cleanup (extracted to engine/cleanup.js) ────────────────────────────────
|
|
152
|
+
// ─── Cleanup (extracted to engine/orchestration/cleanup.js) ────────────────────────────────
|
|
147
153
|
|
|
148
|
-
const { runCleanup } = require('./engine/cleanup');
|
|
154
|
+
const { runCleanup } = require('./engine/orchestration/cleanup');
|
|
149
155
|
|
|
150
156
|
// ─── Worktree pool (W-mp73ya3e000me6c5 — opt-in cross-branch warm reuse) ────
|
|
151
157
|
|
|
152
|
-
const worktreePool = require('./engine/
|
|
153
|
-
const worktreePreflight = require('./engine/
|
|
158
|
+
const worktreePool = require('./engine/worktrees/pool');
|
|
159
|
+
const worktreePreflight = require('./engine/worktrees/preflight');
|
|
154
160
|
|
|
155
|
-
// ─── State Readers (delegated to engine/queries.js) ─────────────────────────
|
|
161
|
+
// ─── State Readers (delegated to engine/core/queries.js) ─────────────────────────
|
|
156
162
|
|
|
157
163
|
const { getConfig, getControl, getDispatch, getNotes,
|
|
158
164
|
getAgentStatus, getInboxFiles,
|
|
159
165
|
collectSkillFiles, getSkillIndex, getKnowledgeBaseIndex,
|
|
160
166
|
getPrs, SKILLS_DIR } = queries;
|
|
161
167
|
|
|
162
|
-
// ─── Routing (extracted to engine/routing.js) ───────────────────────────────
|
|
168
|
+
// ─── Routing (extracted to engine/orchestration/routing.js) ───────────────────────────────
|
|
163
169
|
|
|
164
|
-
const routing = require('./engine/routing');
|
|
165
|
-
const reviewLearning = require('./engine/review-learning');
|
|
170
|
+
const routing = require('./engine/orchestration/routing');
|
|
171
|
+
const reviewLearning = require('./engine/memory/review-learning');
|
|
166
172
|
const { getRouting, parseRoutingTable, getRoutingTableCached, getMonthlySpend,
|
|
167
173
|
getAgentErrorRate, isAgentIdle, resolveAgent, resetClaimedAgents,
|
|
168
174
|
resolveAgentReservation, setTempBudget, tempAgents } = routing;
|
|
169
175
|
|
|
170
|
-
// ─── Playbook, system prompt, agent context (extracted to engine/playbook.js) ─
|
|
176
|
+
// ─── Playbook, system prompt, agent context (extracted to engine/agents/playbook.js) ─
|
|
171
177
|
|
|
172
178
|
const { renderPlaybook, validatePlaybookVars, PLAYBOOK_REQUIRED_VARS,
|
|
173
179
|
buildSystemPrompt, buildAgentContext, selectPlaybook,
|
|
174
180
|
buildBaseVars, buildPrDispatch, resolveTaskContext,
|
|
175
|
-
getRepoHost, getRepoHostLabel, getRepoHostToolRule } = require('./engine/playbook');
|
|
181
|
+
getRepoHost, getRepoHostLabel, getRepoHostToolRule } = require('./engine/agents/playbook');
|
|
176
182
|
|
|
177
183
|
// Per-slug GitHub PAT resolution — mirrors getAdoToken/MINIONS_ADO_TOKEN.
|
|
178
184
|
// Used at agent spawn time to inject GH_TOKEN for GitHub projects so child
|
|
179
185
|
// `gh`/`git push` calls authenticate as the right account without falling
|
|
180
186
|
// through to an interactive `gh auth login` device-code flow.
|
|
181
|
-
const ghToken = require('./engine/gh-token');
|
|
187
|
+
const ghToken = require('./engine/providers/gh-token');
|
|
182
188
|
|
|
183
|
-
// ─── Lifecycle (extracted to engine/lifecycle.js) ────────────────────────────
|
|
189
|
+
// ─── Lifecycle (extracted to engine/orchestration/lifecycle.js) ────────────────────────────
|
|
184
190
|
|
|
185
191
|
const { runPostCompletionHooks, handleQaSessionSetupCompletion, updateWorkItemStatus, syncPrdItemStatus, reconcilePrdStatuses, handlePostMerge, checkPlanCompletion,
|
|
186
192
|
syncPrsFromOutput, updatePrAfterReview, updatePrAfterFix, checkForLearnings, extractSkillsFromOutput,
|
|
187
193
|
updateAgentHistory, updateMetrics, parseAgentOutput, syncPrdFromPrs, persistVerifyPrsToPrd,
|
|
188
194
|
isItemCompleted, classifyFailure: classifyFailureFallback, diagnoseEmptyOutput, processPendingRebases, resolveWorkItemScope,
|
|
189
195
|
mergeArtifactNotes, promoteCompletionArtifacts, pruneScopeMismatchDuplicatePrs, collapseAllDuplicatePrRecords,
|
|
190
|
-
|
|
196
|
+
parseCompletionReportFile,
|
|
197
|
+
recordWorktreeHeldPause, clearWorktreeHeldPause } = require('./engine/orchestration/lifecycle');
|
|
191
198
|
|
|
192
199
|
// ─── Diagnostics: memory + event-loop + GC sampler (P-a1b2c3d4 / P-b2c3d4e5) ─
|
|
193
200
|
|
|
194
|
-
const diagnosticsMemory = require('./engine/diagnostics-memory');
|
|
201
|
+
const diagnosticsMemory = require('./engine/observability/diagnostics-memory');
|
|
195
202
|
const DIAGNOSTICS_MEMORY_PATH = path.join(ENGINE_DIR, 'diagnostics-memory.json');
|
|
196
203
|
|
|
197
204
|
// P-e5f6a7b8 — sentinel consumed each tick; dashboard.js writes it after
|
|
@@ -208,7 +215,7 @@ const realActivityMap = new Map(); // dispatchId → timestamp of last agent std
|
|
|
208
215
|
const SPAWN_ATTEMPT_ABORTED = Symbol('spawn-attempt-aborted');
|
|
209
216
|
const FOREIGN_SPAWN_LEASE_MAX_AGE_MS = 30 * 60 * 1000;
|
|
210
217
|
const _spawnAttemptLeases = new Map(); // dispatchId → in-flight pre-runtime lease
|
|
211
|
-
// tempAgents imported from engine/routing.js
|
|
218
|
+
// tempAgents imported from engine/orchestration/routing.js
|
|
212
219
|
let engineRestartGraceUntil = 0; // timestamp — suppress orphan detection until this time
|
|
213
220
|
const engineRestartGraceExempt = new Set(); // dispatch IDs with confirmed-dead PIDs at restart — bypass grace period
|
|
214
221
|
|
|
@@ -367,10 +374,19 @@ async function _checkWorktreeBranchHoldForDispatch(project, type, branch, config
|
|
|
367
374
|
if (!project?.localPath) return null;
|
|
368
375
|
let spawnPaths;
|
|
369
376
|
try {
|
|
370
|
-
spawnPaths = shared.resolveSpawnPaths(project, type, MINIONS_DIR
|
|
377
|
+
spawnPaths = shared.resolveSpawnPaths(project, type, MINIONS_DIR, {
|
|
378
|
+
executionSurface: _deps.executionSurface,
|
|
379
|
+
});
|
|
371
380
|
} catch {
|
|
372
381
|
return null; // spawnAgent owns structural path diagnostics
|
|
373
382
|
}
|
|
383
|
+
// W-ms5tb6ha016fda10: a control-plane dispatch allocates no worktree, so no
|
|
384
|
+
// operator checkout can hold "its" branch. A cloud dispatch is covered by the
|
|
385
|
+
// `!spawnPaths.worktreeRootDir` check on the next line — reachable only
|
|
386
|
+
// because the caller resolves and forwards the CLOUD surface (see the
|
|
387
|
+
// `_itemSkipsCheckoutAllocation` block in dispatchPending); a caller that
|
|
388
|
+
// forwarded `repo` for a cloud item would silently get repo-shaped gating.
|
|
389
|
+
if (spawnPaths.controlPlane) return notApplicable;
|
|
374
390
|
if (spawnPaths.liveMode || !spawnPaths.worktreeRootDir) return notApplicable;
|
|
375
391
|
|
|
376
392
|
const listWorktrees = _deps.listWorktrees || (repoDir =>
|
|
@@ -477,14 +493,24 @@ function _clearWorktreeBranchHeld(dispatchItem) {
|
|
|
477
493
|
}
|
|
478
494
|
}
|
|
479
495
|
|
|
480
|
-
function _checkWorktreeDiskSpaceForDispatch(project, type, config) {
|
|
496
|
+
function _checkWorktreeDiskSpaceForDispatch(project, type, config, options = {}) {
|
|
481
497
|
if (!project?.localPath) return null;
|
|
482
498
|
let spawnPaths;
|
|
483
499
|
try {
|
|
484
|
-
spawnPaths = shared.resolveSpawnPaths(project, type, MINIONS_DIR
|
|
500
|
+
spawnPaths = shared.resolveSpawnPaths(project, type, MINIONS_DIR, {
|
|
501
|
+
executionSurface: options.executionSurface,
|
|
502
|
+
});
|
|
485
503
|
} catch {
|
|
486
504
|
return null; // spawnAgent owns structural path diagnostics
|
|
487
505
|
}
|
|
506
|
+
// W-ms5tb6ha016fda10: a control-plane dispatch never runs `git worktree add`,
|
|
507
|
+
// so worktree free-space capacity is irrelevant to whether it can run. A
|
|
508
|
+
// cloud dispatch is covered by the `!spawnPaths.worktreeRootDir` check on the
|
|
509
|
+
// next line — reachable only because the caller resolves and forwards the
|
|
510
|
+
// CLOUD surface (see the `_itemSkipsCheckoutAllocation` block in
|
|
511
|
+
// dispatchPending); a caller that forwarded `repo` for a cloud item would
|
|
512
|
+
// silently subject it to a disk preflight it can never legitimately fail.
|
|
513
|
+
if (spawnPaths.controlPlane) return null;
|
|
488
514
|
if (spawnPaths.liveMode || !spawnPaths.worktreeRootDir) return null;
|
|
489
515
|
const worktreeRoot = path.resolve(
|
|
490
516
|
spawnPaths.worktreeRootDir,
|
|
@@ -738,11 +764,133 @@ function buildDepConflictFixItem({
|
|
|
738
764
|
featureBranch: depConflictBranch,
|
|
739
765
|
_branch: depConflictBranch,
|
|
740
766
|
_blockedItem: blockedItem ? blockedItem.id : null,
|
|
767
|
+
// W-mrzqfozm00035365 — track EVERY downstream item blocked by this dep
|
|
768
|
+
// conflict (not just the first), so completing the repair can wake all of
|
|
769
|
+
// them and restart/reconciliation can rebuild the links idempotently.
|
|
770
|
+
_blockedItems: blockedItem ? [blockedItem.id] : [],
|
|
741
771
|
_isInterDepConflict: !!isInterDepConflict,
|
|
742
772
|
project: projectName || null,
|
|
743
773
|
};
|
|
744
774
|
}
|
|
745
775
|
|
|
776
|
+
// ─── W-mrzqfozm00035365: recoverable dependency merge-conflict gate ──────────
|
|
777
|
+
// A dependency-branch merge conflict is detected in spawnAgent BEFORE the agent
|
|
778
|
+
// process starts. It is NOT an execution failure of the downstream item and must
|
|
779
|
+
// not burn its retry budget or drive it to terminal `failed` while the
|
|
780
|
+
// deterministic conflict-repair WI is still being worked. Instead the downstream
|
|
781
|
+
// item is stamped with a `_dependencyConflict` marker + `_pendingReason` and
|
|
782
|
+
// held pending; the helpers below make the orchestration decisions pure and
|
|
783
|
+
// unit-testable.
|
|
784
|
+
const DEP_CONFLICT_PENDING_REASON = 'dependency_conflict'; // repair in progress — auto-clears
|
|
785
|
+
const DEP_CONFLICT_UNRESOLVED_PENDING_REASON = 'dependency_conflict_unresolved'; // repair failed/cancelled or still conflicting — actionable block
|
|
786
|
+
const DEP_CONFLICT_FIX_CREATED_BY = 'engine:dep-conflict-fix';
|
|
787
|
+
|
|
788
|
+
// Build the marker stamped on a downstream WI when its dependency branches
|
|
789
|
+
// conflict. `files` is capped so the persisted record stays small.
|
|
790
|
+
function buildDependencyConflictMarker({
|
|
791
|
+
repairWiId,
|
|
792
|
+
conflictBranch,
|
|
793
|
+
conflictsWith,
|
|
794
|
+
isInterDep = false,
|
|
795
|
+
files = [],
|
|
796
|
+
detectedAt = null,
|
|
797
|
+
}) {
|
|
798
|
+
return {
|
|
799
|
+
repairWiId: repairWiId || null,
|
|
800
|
+
conflictBranch: conflictBranch || null,
|
|
801
|
+
conflictsWith: conflictsWith || null,
|
|
802
|
+
isInterDep: !!isInterDep,
|
|
803
|
+
files: Array.isArray(files) ? files.slice(0, 10) : [],
|
|
804
|
+
detectedAt: detectedAt || ts(),
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Locate the deterministic conflict-repair WI for a blocked downstream item.
|
|
809
|
+
// Prefers the id recorded on the marker; falls back to the persisted
|
|
810
|
+
// back-reference (`_blockedItems` / legacy `_blockedItem`) so links survive
|
|
811
|
+
// even when the marker was never written (legacy terminal-failed items).
|
|
812
|
+
function findConflictRepairForBlockedItem(itemId, allItems, markerRepairId = null) {
|
|
813
|
+
if (!Array.isArray(allItems)) return null;
|
|
814
|
+
if (markerRepairId) {
|
|
815
|
+
const byId = allItems.find(w => w && w.id === markerRepairId);
|
|
816
|
+
if (byId) return byId;
|
|
817
|
+
}
|
|
818
|
+
return allItems.find(w =>
|
|
819
|
+
w && w.createdBy === DEP_CONFLICT_FIX_CREATED_BY
|
|
820
|
+
&& ((Array.isArray(w._blockedItems) && w._blockedItems.includes(itemId)) || w._blockedItem === itemId)
|
|
821
|
+
) || null;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Decide what to do with a downstream item held on a dependency merge conflict,
|
|
825
|
+
// given the current state of its conflict-repair WI. Pure + deterministic.
|
|
826
|
+
// downstream: the blocked WI (carries `_dependencyConflict`)
|
|
827
|
+
// repair: the conflict-fix WI (or null when not yet materialized / pruned)
|
|
828
|
+
// nowMs: Date.now() (injectable for tests; unused today but kept for parity)
|
|
829
|
+
// Returns { action, pendingReason, reason }:
|
|
830
|
+
// 'wait' → keep pending, repair still in progress (never redispatch into the
|
|
831
|
+
// same conflict)
|
|
832
|
+
// 'wake' → clear the marker so the item re-runs dependency integration on
|
|
833
|
+
// freshly-fetched refs
|
|
834
|
+
// 'block' → keep pending with an actionable unresolved reason and DO NOT loop
|
|
835
|
+
function evaluateDependencyConflictGate(downstream, repair, nowMs = Date.now()) {
|
|
836
|
+
const marker = downstream && downstream._dependencyConflict;
|
|
837
|
+
if (!marker) {
|
|
838
|
+
return { action: 'wake', pendingReason: null, reason: 'no active dependency-conflict marker' };
|
|
839
|
+
}
|
|
840
|
+
if (!repair) {
|
|
841
|
+
// Repair not found yet (creation race, or store not visible this tick).
|
|
842
|
+
// Hold — the detection path (re)creates it; never redispatch blindly.
|
|
843
|
+
return { action: 'wait', pendingReason: DEP_CONFLICT_PENDING_REASON, reason: 'conflict-repair work item not found yet' };
|
|
844
|
+
}
|
|
845
|
+
const st = repair.status;
|
|
846
|
+
if (st === WI_STATUS.FAILED || st === WI_STATUS.CANCELLED) {
|
|
847
|
+
return {
|
|
848
|
+
action: 'block',
|
|
849
|
+
pendingReason: DEP_CONFLICT_UNRESOLVED_PENDING_REASON,
|
|
850
|
+
reason: `conflict-repair ${repair.id} is ${st}`,
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
if (DONE_STATUSES.has(st)) {
|
|
854
|
+
// Wake only if the repair completed AFTER we detected THIS conflict. A repair
|
|
855
|
+
// that completed BEFORE (or exactly at) detection means the freshly-fetched
|
|
856
|
+
// merge still conflicts despite the repair — block instead of looping.
|
|
857
|
+
const detectedAtMs = marker.detectedAt ? Date.parse(marker.detectedAt) : NaN;
|
|
858
|
+
const completedAtMs = repair.completedAt ? Date.parse(repair.completedAt) : NaN;
|
|
859
|
+
const repairAfterDetection = (Number.isFinite(completedAtMs) && Number.isFinite(detectedAtMs))
|
|
860
|
+
? completedAtMs >= detectedAtMs
|
|
861
|
+
: true; // missing timestamps → wake once (repair is done)
|
|
862
|
+
if (repairAfterDetection) {
|
|
863
|
+
return { action: 'wake', pendingReason: null, reason: `conflict-repair ${repair.id} done` };
|
|
864
|
+
}
|
|
865
|
+
return {
|
|
866
|
+
action: 'block',
|
|
867
|
+
pendingReason: DEP_CONFLICT_UNRESOLVED_PENDING_REASON,
|
|
868
|
+
reason: `conflict-repair ${repair.id} completed before the conflict was re-detected — dependency branches still conflict`,
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
// pending / dispatched / decomposed / paused → still being repaired.
|
|
872
|
+
return { action: 'wait', pendingReason: DEP_CONFLICT_PENDING_REASON, reason: `conflict-repair ${repair.id} in progress (${st})` };
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Reconciliation (requirement 7): should a terminally-FAILED item be revived to
|
|
876
|
+
// pending because it failed on a dependency merge conflict AND its deterministic
|
|
877
|
+
// repair WI is live (pending/dispatched) or done? Guards against reviving
|
|
878
|
+
// unrelated failures: only fires for MERGE_CONFLICT-class failures linked to a
|
|
879
|
+
// repair WI that is not itself terminal-dead. Pure + deterministic.
|
|
880
|
+
function shouldReviveFailedDependencyConflict(failedItem, repair) {
|
|
881
|
+
if (!failedItem || failedItem.status !== WI_STATUS.FAILED) return false;
|
|
882
|
+
const fc = failedItem._failureClass || '';
|
|
883
|
+
const fr = String(failedItem.failReason || '');
|
|
884
|
+
const looksLikeDepConflict = fc === FAILURE_CLASS.MERGE_CONFLICT
|
|
885
|
+
|| /\bMERGE_CONFLICT\b/i.test(fr)
|
|
886
|
+
|| /Dependency merge failed/i.test(fr);
|
|
887
|
+
if (!looksLikeDepConflict) return false;
|
|
888
|
+
if (!repair) return false;
|
|
889
|
+
// The repair must be actively repairing or already done — not itself dead.
|
|
890
|
+
if (repair.status === WI_STATUS.FAILED || repair.status === WI_STATUS.CANCELLED) return false;
|
|
891
|
+
return true;
|
|
892
|
+
}
|
|
893
|
+
|
|
746
894
|
// Prune dep branches that are ancestors of other dep branches (#958)
|
|
747
895
|
// When B already contains A's commits, merging both A and B causes conflicts.
|
|
748
896
|
async function pruneAncestorDeps(deps, gitOpts, cwd) {
|
|
@@ -821,6 +969,10 @@ function _buildAgentSpawnFlags(runtime, opts = {}) {
|
|
|
821
969
|
function _syncAgentWorkerPoolConfig(engineConfig = {}) {
|
|
822
970
|
const enabled = (engineConfig.agentUseWorkerPool ?? ENGINE_DEFAULTS.agentUseWorkerPool) === true;
|
|
823
971
|
agentWorkerPool.setEnabled(enabled);
|
|
972
|
+
// Issue #1105 — cold ACP handshake budget (`initialize` + `session/new` /
|
|
973
|
+
// `session/load`). Pushed unconditionally so a pool enabled later in the same
|
|
974
|
+
// process already carries the operator-configured budget.
|
|
975
|
+
agentWorkerPool.setHandshakeTimeoutMs(shared.resolveAcpHandshakeTimeoutMs(engineConfig));
|
|
824
976
|
if (enabled) agentWorkerPool.setPoolSize(shared.resolveAgentAcpPoolSize(engineConfig));
|
|
825
977
|
return enabled;
|
|
826
978
|
}
|
|
@@ -956,7 +1108,18 @@ function _classifyAgentFailure(runtime, code, stdout, stderr) {
|
|
|
956
1108
|
function ackPendingSteeringFiles(agentId, procInfo, rawOutput, observedAtMs = Date.now()) {
|
|
957
1109
|
if (!procInfo?._pendingSteeringFiles?.length || !rawOutput) return;
|
|
958
1110
|
if (procInfo._steeringMessage || procInfo._steeringNoSession) return;
|
|
959
|
-
|
|
1111
|
+
let acked;
|
|
1112
|
+
try {
|
|
1113
|
+
acked = steering.ackProcessedSteeringMessages(
|
|
1114
|
+
agentId,
|
|
1115
|
+
procInfo._pendingSteeringFiles,
|
|
1116
|
+
rawOutput,
|
|
1117
|
+
{ observedAtMs },
|
|
1118
|
+
);
|
|
1119
|
+
} catch (err) {
|
|
1120
|
+
log('warn', `Steering ACK persistence failed for ${agentId}: ${err.message}`);
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
960
1123
|
if (acked.length === 0) return;
|
|
961
1124
|
|
|
962
1125
|
const ackedPaths = new Set(acked.map(entry => entry.path));
|
|
@@ -977,17 +1140,21 @@ function captureSessionIdFromStdoutChunk(agentId, dispatchId, branchName, runtim
|
|
|
977
1140
|
if (!sessionId) continue;
|
|
978
1141
|
procInfo.sessionId = sessionId;
|
|
979
1142
|
if (runtime && typeof runtime.saveSession === 'function') {
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1143
|
+
try {
|
|
1144
|
+
runtime.saveSession({
|
|
1145
|
+
agentId,
|
|
1146
|
+
dispatchId,
|
|
1147
|
+
branch: branchName,
|
|
1148
|
+
sessionId,
|
|
1149
|
+
agentsDir: AGENTS_DIR,
|
|
1150
|
+
// W-ms0vk9nv000z985b — the directory this session is anchored to. A
|
|
1151
|
+
// later dispatch refuses to --resume it once that directory is gone.
|
|
1152
|
+
cwd: sessionCwd || null,
|
|
1153
|
+
logger: _runtimeLogger(),
|
|
1154
|
+
});
|
|
1155
|
+
} catch (err) {
|
|
1156
|
+
log('warn', `Could not persist runtime session for ${dispatchId}: ${err.message}`);
|
|
1157
|
+
}
|
|
991
1158
|
}
|
|
992
1159
|
|
|
993
1160
|
return;
|
|
@@ -1097,7 +1264,7 @@ function promoteCheckpointSteeringForClose(agentId, procInfo, runtime, liveOutpu
|
|
|
1097
1264
|
// 're_spawning' — captures that the engine has committed to deliver
|
|
1098
1265
|
// these messages via session resume at the natural checkpoint.
|
|
1099
1266
|
try {
|
|
1100
|
-
const store = require('./engine/steering-store');
|
|
1267
|
+
const store = require('./engine/persistence/steering-store');
|
|
1101
1268
|
for (const entry of checkpointEntries) {
|
|
1102
1269
|
if (entry?.steerId) {
|
|
1103
1270
|
store.updateStatus(entry.steerId, 're_spawning', { runtime: runtime?.name || null });
|
|
@@ -1107,6 +1274,85 @@ function promoteCheckpointSteeringForClose(agentId, procInfo, runtime, liveOutpu
|
|
|
1107
1274
|
return { status: 'promoted', entries: checkpointEntries };
|
|
1108
1275
|
}
|
|
1109
1276
|
|
|
1277
|
+
// W-ms9tcs3o01y03383 — the prompt completion boundary, as one decision.
|
|
1278
|
+
//
|
|
1279
|
+
// A dispatch is provably finished when BOTH halves hold:
|
|
1280
|
+
// 1. the tracked child exited terminally and successfully (`code === 0`, which
|
|
1281
|
+
// the caller only has once the `close` event fired), and
|
|
1282
|
+
// 2. a structurally valid completion report exists whose nonce proves it was
|
|
1283
|
+
// written by THIS dispatch (the nonce is minted per spawn and carried
|
|
1284
|
+
// across steering resumes).
|
|
1285
|
+
//
|
|
1286
|
+
// Fails closed on every other shape: non-zero exit, missing report, malformed
|
|
1287
|
+
// report, missing/forged nonce. Those keep the existing failure/timeout paths
|
|
1288
|
+
// untouched — nothing here weakens success validation.
|
|
1289
|
+
function resolveCloseCompletionBoundary(dispatchItem, code, expectedNonce, readReport = parseCompletionReportFile) {
|
|
1290
|
+
if (code !== 0) return null;
|
|
1291
|
+
try {
|
|
1292
|
+
return shared.acceptCompletionReportForNonce(readReport(dispatchItem), expectedNonce);
|
|
1293
|
+
} catch (e) {
|
|
1294
|
+
log('warn', `close completion-report probe failed for ${dispatchItem?.id}: ${e.message}`);
|
|
1295
|
+
return null;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// W-ms9tcs3o01y03383 — after a confirmed terminal exit whose completion report
|
|
1300
|
+
// is structurally valid AND owned by this dispatch's nonce, the prompt is
|
|
1301
|
+
// finished. A steering resume from that point is pure bookkeeping: it cannot
|
|
1302
|
+
// change the completed turn, but it CAN hold the dispatch open for a full
|
|
1303
|
+
// `engine.resumeHeartbeatTimeout` when the resumed runtime never emits a first
|
|
1304
|
+
// heartbeat (observed 310s on lambert-plan-to-prd-ms9skh2q01a91a35 — the whole
|
|
1305
|
+
// point of this work item).
|
|
1306
|
+
//
|
|
1307
|
+
// So we release the resume intent instead of spawning it, and deliberately do
|
|
1308
|
+
// NOT ack/consume the steering messages: they stay UNREAD on disk, so the
|
|
1309
|
+
// agent's next dispatch picks them up through the normal
|
|
1310
|
+
// steering.buildPendingSteeringPrompt path. That is the same durability
|
|
1311
|
+
// contract the existing no-sessionId `[steering-pending]` branch relies on, and
|
|
1312
|
+
// it matches what the human was already told when the message was deferred
|
|
1313
|
+
// ("queued until the agent reaches a resumable checkpoint or the next dispatch").
|
|
1314
|
+
//
|
|
1315
|
+
// The same rule applies to every other steering disposition that would override
|
|
1316
|
+
// durable completion evidence, including the `_steeringNoSession` re-queue.
|
|
1317
|
+
function releasePendingSteeringResumeForCompletedTurn(agentId, procInfo, liveOutputPath) {
|
|
1318
|
+
if (!procInfo) return { released: false, deferred: 0, entries: [] };
|
|
1319
|
+
const entries = Array.isArray(procInfo._steeringEntry)
|
|
1320
|
+
? procInfo._steeringEntry
|
|
1321
|
+
: (procInfo._steeringEntry ? [procInfo._steeringEntry] : []);
|
|
1322
|
+
const released = !!procInfo._steeringMessage || !!procInfo._steeringNoSession;
|
|
1323
|
+
delete procInfo._steeringMessage;
|
|
1324
|
+
delete procInfo._steeringSessionId;
|
|
1325
|
+
delete procInfo._steeringEntry;
|
|
1326
|
+
delete procInfo._steeringDeferredCheckpoint;
|
|
1327
|
+
// The no-session steering kill (#1014) re-queues the dispatch back to pending
|
|
1328
|
+
// so the message rides along on the next attempt. That is right for an agent
|
|
1329
|
+
// that was interrupted mid-work — and wrong for one that provably finished,
|
|
1330
|
+
// because it discards a real success and re-runs completed work. Clearing the
|
|
1331
|
+
// flag lets the report finalize the dispatch; the message is unread either
|
|
1332
|
+
// way, so the next dispatch still delivers it.
|
|
1333
|
+
delete procInfo._steeringNoSession;
|
|
1334
|
+
|
|
1335
|
+
let unread = [];
|
|
1336
|
+
try { unread = steering.listUnreadSteeringMessages(agentId).filter(entry => entry.message.trim()); }
|
|
1337
|
+
catch { /* inbox read is best-effort — the release itself must still happen */ }
|
|
1338
|
+
if (!released && unread.length === 0) return { released: false, deferred: 0, entries: [] };
|
|
1339
|
+
|
|
1340
|
+
// Roll any row we had already moved to `re_spawning` back to `queued` so the
|
|
1341
|
+
// dashboard does not advertise a resume that intentionally never ran.
|
|
1342
|
+
try {
|
|
1343
|
+
const store = require('./engine/persistence/steering-store');
|
|
1344
|
+
for (const entry of entries) {
|
|
1345
|
+
if (entry?.steerId) store.updateStatus(entry.steerId, 'queued');
|
|
1346
|
+
}
|
|
1347
|
+
} catch { /* best-effort */ }
|
|
1348
|
+
|
|
1349
|
+
log('info', `Steering: ${agentId} finished its turn with a valid completion report — ${unread.length} message(s) stay unread for the next dispatch instead of holding the dispatch open for a resume`);
|
|
1350
|
+
try {
|
|
1351
|
+
fs.appendFileSync(liveOutputPath, `\n[steering-pending] Agent completed this task and wrote a valid completion report, so no resume was started. Your message remains unread and will be delivered on the next dispatch.\n`);
|
|
1352
|
+
} catch { /* optional */ }
|
|
1353
|
+
return { released, deferred: unread.length, entries };
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1110
1356
|
// Resolve dependency plan item IDs to their PR branches
|
|
1111
1357
|
function resolveDependencyBranches(depIds, sourcePlan, project, config) {
|
|
1112
1358
|
// P-faea3206: each entry now carries projectName + projectRoot + isCrossRepo
|
|
@@ -1221,7 +1467,13 @@ async function syncReusedWorktree(rootDir, worktreePath, branchName, gitOpts = {
|
|
|
1221
1467
|
// auto-pick-up master mid-flight, so the carve-out is explicit.
|
|
1222
1468
|
const { mainRef, isSharedBranch } = opts;
|
|
1223
1469
|
if (mainRef && !isSharedBranch) {
|
|
1224
|
-
|
|
1470
|
+
// W-ms5tb6ha016fda10: serialize the shared-ref refresh (see
|
|
1471
|
+
// _fetchWithTransientRetry). Only the fetch is inside the chain — the
|
|
1472
|
+
// merge below is per-worktree work and must NOT be serialized with it.
|
|
1473
|
+
try {
|
|
1474
|
+
await shared.withSerializedRemoteRefRefresh(rootDir, () =>
|
|
1475
|
+
shared.shellSafeGit(['fetch', 'origin', mainRef], { ...gitOpts, cwd: rootDir }));
|
|
1476
|
+
}
|
|
1225
1477
|
catch (e) { log('warn', `git: failed to fetch origin/${mainRef} during reuse-sync: ${e.message}`); }
|
|
1226
1478
|
try { await shared.shellSafeGit(['merge', `origin/${mainRef}`, '--no-edit', '--no-ff'], { ...gitOpts, cwd: worktreePath }); }
|
|
1227
1479
|
catch (e) {
|
|
@@ -1336,13 +1588,29 @@ function _isTransientGitNetworkError(err) {
|
|
|
1336
1588
|
//
|
|
1337
1589
|
// `args` is the array passed to git AFTER 'fetch' (e.g. ['origin', branch]).
|
|
1338
1590
|
// `label` is a short string used in the log line for triage.
|
|
1591
|
+
//
|
|
1592
|
+
// W-ms5tb6ha016fda10 — every attempt runs inside
|
|
1593
|
+
// `shared.withSerializedRemoteRefRefresh(<repo>)`. Concurrent fresh-worktree
|
|
1594
|
+
// preparation in the SAME clone used to race on `refs/remotes/origin/<main>`
|
|
1595
|
+
// and the loser died with `cannot lock ref ... but expected <sha>`, which then
|
|
1596
|
+
// classified as a bogus NETWORK_ERROR. Chaining per repo removes that race at
|
|
1597
|
+
// its source; the chain is keyed on `opts.cwd`, so unrelated projects still
|
|
1598
|
+
// fetch in parallel. The lock is in-process and covers ONLY the fetch — no
|
|
1599
|
+
// worktree add / checkout / merge work is serialized with it. A residual
|
|
1600
|
+
// cross-process ref-lock loss (an operator's own git, the dashboard, the CLI)
|
|
1601
|
+
// is treated as a transient worth one retry, and is classified as
|
|
1602
|
+
// GIT_REF_LOCK rather than a connectivity failure.
|
|
1339
1603
|
async function _fetchWithTransientRetry(args, opts, label, { throwOnFailure = false } = {}) {
|
|
1340
|
-
const _attempt = async () => shared.
|
|
1604
|
+
const _attempt = async () => shared.withSerializedRemoteRefRefresh(
|
|
1605
|
+
opts?.cwd,
|
|
1606
|
+
() => shared.shellSafeGit(['fetch', ...args], opts),
|
|
1607
|
+
);
|
|
1341
1608
|
try {
|
|
1342
1609
|
await _attempt();
|
|
1343
1610
|
return true;
|
|
1344
1611
|
} catch (e) {
|
|
1345
|
-
|
|
1612
|
+
const refLock = shared.isGitRefLockError(e);
|
|
1613
|
+
if (!refLock && !_isTransientGitNetworkError(e)) {
|
|
1346
1614
|
// #3045 — log the FULL exec error message (redacted), not just the first
|
|
1347
1615
|
// line. child_process.exec rejections format as "Command failed: <cmd>"
|
|
1348
1616
|
// on line 1 and the real stderr ("TF400813 unknown user", "HTTP 401",
|
|
@@ -1356,7 +1624,7 @@ async function _fetchWithTransientRetry(args, opts, label, { throwOnFailure = fa
|
|
|
1356
1624
|
return false; // swallow non-transient — caller falls back to local ref
|
|
1357
1625
|
}
|
|
1358
1626
|
const transientMsg = adoGitAuth.redactBearer(String(e.message || e));
|
|
1359
|
-
log('warn', `git fetch ${label}: transient (${transientMsg}) — retrying once after 1.5s`);
|
|
1627
|
+
log('warn', `git fetch ${label}: ${refLock ? 'ref-lock contention' : 'transient'} (${transientMsg}) — retrying once after 1.5s`);
|
|
1360
1628
|
await new Promise(r => setTimeout(r, 1500));
|
|
1361
1629
|
try {
|
|
1362
1630
|
await _attempt();
|
|
@@ -1372,6 +1640,11 @@ async function _fetchWithTransientRetry(args, opts, label, { throwOnFailure = fa
|
|
|
1372
1640
|
}
|
|
1373
1641
|
|
|
1374
1642
|
function _classifyFreshBaseFetchFailure(err) {
|
|
1643
|
+
// Order matters: a lost ref-lock race is a LOCAL serialization defect, not
|
|
1644
|
+
// connectivity. Classifying it as NETWORK_ERROR (the historical behavior)
|
|
1645
|
+
// hid the real cause behind a "flaky network" story and burned retries on
|
|
1646
|
+
// maintenance jobs that never needed a checkout at all.
|
|
1647
|
+
if (shared.isGitRefLockError(err)) return FAILURE_CLASS.GIT_REF_LOCK;
|
|
1375
1648
|
return adoGitAuth.isAdoAuthFailure(err)
|
|
1376
1649
|
? FAILURE_CLASS.AUTH
|
|
1377
1650
|
: FAILURE_CLASS.NETWORK_ERROR;
|
|
@@ -1841,32 +2114,34 @@ function _reapProbeTimeoutMs() {
|
|
|
1841
2114
|
return ENGINE_DEFAULTS.statusProbeKillTimeoutMs || 12000;
|
|
1842
2115
|
}
|
|
1843
2116
|
|
|
1844
|
-
// W-mq5n1zx5 — Layer 2a: on Windows,
|
|
1845
|
-
//
|
|
1846
|
-
//
|
|
1847
|
-
|
|
1848
|
-
// PowerShell's CIM cmdlets to find matching processes and Stop-Process
|
|
1849
|
-
// them. POSIX is a no-op (the EBUSY race is Windows-specific). Best-effort;
|
|
1850
|
-
// failure is logged but never blocks the quarantine.
|
|
1851
|
-
function _killGitDescendantsForWorktree(worktreePath) {
|
|
2117
|
+
// W-mq5n1zx5 — Layer 2a: on Windows, terminate live `git.exe` processes whose
|
|
2118
|
+
// command line proves they target the quarantine path. POSIX is a no-op (the
|
|
2119
|
+
// EBUSY race is Windows-specific). Best-effort; failure never blocks quarantine.
|
|
2120
|
+
function _killGitDescendantsForWorktree(worktreePath, opts = {}) {
|
|
1852
2121
|
if (process.platform !== 'win32') return { killed: 0, skipped: true };
|
|
1853
2122
|
if (!ENGINE_DEFAULTS.statusProbeKillDescendantsWin32) return { killed: 0, skipped: true };
|
|
1854
2123
|
if (!worktreePath) return { killed: 0, skipped: true };
|
|
1855
|
-
// PowerShell expects single-quoted literals; escape any embedded single
|
|
1856
|
-
// quote by doubling it (PowerShell's standard single-quote escape).
|
|
1857
|
-
const safePath = String(worktreePath).replace(/'/g, "''");
|
|
1858
|
-
const ps = [
|
|
1859
|
-
"$ErrorActionPreference='SilentlyContinue';",
|
|
1860
|
-
`$matches = Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'git.exe' -and $_.CommandLine -like '*${safePath}*' };`,
|
|
1861
|
-
"if ($matches) { $matches | ForEach-Object { Stop-Process -Id $_.ProcessId -Force; $_.ProcessId }; }",
|
|
1862
|
-
].join(' ');
|
|
1863
2124
|
try {
|
|
1864
|
-
const
|
|
1865
|
-
const
|
|
2125
|
+
const target = path.resolve(worktreePath).replace(/\//g, '\\').toLowerCase();
|
|
2126
|
+
const processes = Array.isArray(opts.processes) ? opts.processes : shared.listAllProcesses();
|
|
2127
|
+
const matches = processes
|
|
2128
|
+
.filter(proc => String(proc?.name || '').toLowerCase() === 'git.exe')
|
|
2129
|
+
.filter(proc => String(proc?.cmd || '').replace(/\//g, '\\').toLowerCase().includes(target));
|
|
2130
|
+
const pids = matches.map(proc => proc.pid);
|
|
2131
|
+
const expectedIdentities = new Map(
|
|
2132
|
+
matches.map(proc => [Number(proc.pid), Number(proc.startedAt)]),
|
|
2133
|
+
);
|
|
2134
|
+
let killed = 0;
|
|
2135
|
+
if (pids.length > 0) {
|
|
2136
|
+
const ownership = { expectedIdentities };
|
|
2137
|
+
killed = opts.terminateProcess
|
|
2138
|
+
? opts.terminateProcess('engine.git-status-probe-cleanup', pids, ownership)
|
|
2139
|
+
: shared.terminateProcess('engine.git-status-probe-cleanup', pids, ownership);
|
|
2140
|
+
}
|
|
1866
2141
|
if (killed > 0) log('info', `_killGitDescendantsForWorktree: killed ${killed} git.exe descendant(s) holding ${worktreePath}`);
|
|
1867
2142
|
return { killed };
|
|
1868
2143
|
} catch (e) {
|
|
1869
|
-
log('warn', `_killGitDescendantsForWorktree:
|
|
2144
|
+
log('warn', `_killGitDescendantsForWorktree: process probe failed: ${e.message}`);
|
|
1870
2145
|
return { killed: 0, error: e.message };
|
|
1871
2146
|
}
|
|
1872
2147
|
}
|
|
@@ -1888,11 +2163,11 @@ function _findTerminalWorktreeOwners(worktreePath, _deps = {}) {
|
|
|
1888
2163
|
try {
|
|
1889
2164
|
const reader = typeof _deps.readDispatchSectioned === 'function'
|
|
1890
2165
|
? _deps.readDispatchSectioned
|
|
1891
|
-
: require('./engine/dispatch-store').readDispatchSectioned;
|
|
2166
|
+
: require('./engine/persistence/dispatch-store').readDispatchSectioned;
|
|
1892
2167
|
sectioned = reader();
|
|
1893
2168
|
} catch {
|
|
1894
2169
|
try {
|
|
1895
|
-
sectioned = require(path.join(__dirname, 'engine', 'dispatch-store')).readDispatchSectioned();
|
|
2170
|
+
sectioned = require(path.join(__dirname, 'engine', 'persistence', 'dispatch-store')).readDispatchSectioned();
|
|
1896
2171
|
} catch { return []; }
|
|
1897
2172
|
}
|
|
1898
2173
|
if (!sectioned || typeof sectioned !== 'object') return [];
|
|
@@ -1979,7 +2254,7 @@ function _reapWorktreeHolders(worktreePath, opts = {}) {
|
|
|
1979
2254
|
|
|
1980
2255
|
// Layer 0 — legacy git.exe-by-cmdline sweep. NOT redundant with Layer 2
|
|
1981
2256
|
// (P-e2a6c9d4, evaluated): Layer 2 (findProcessCwdHolders) matches purely on a
|
|
1982
|
-
// process's CWD resolving at/under THIS worktree (engine/shared.js
|
|
2257
|
+
// process's CWD resolving at/under THIS worktree (engine/core/shared.js
|
|
1983
2258
|
// _windowsCwdProbeScript + _parseCwdHolderLines filter on cwd only). Layer 0
|
|
1984
2259
|
// matches `git.exe` by `CommandLine -like '*<worktreePath>*'` regardless of CWD.
|
|
1985
2260
|
// The case Layer 2 cannot see: a git.exe the engine itself spawns FROM the repo
|
|
@@ -3433,7 +3708,17 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
3433
3708
|
|
|
3434
3709
|
const qaRunId = meta?.qaRunId || meta?.item?.meta?.qaRunId;
|
|
3435
3710
|
if (qaRunId) {
|
|
3436
|
-
require('./engine/qa
|
|
3711
|
+
require('./engine/qa/runs').prepareResultSidecar(agentId, qaRunId);
|
|
3712
|
+
}
|
|
3713
|
+
|
|
3714
|
+
// W-msb9kgs402813a97-b — same contract for the DRAFT visual-journey
|
|
3715
|
+
// manifest. Load-bearing on the re-draft path (`editDraft` re-queues DRAFT
|
|
3716
|
+
// for the SAME session id): a leftover manifest would pass the session-id
|
|
3717
|
+
// check and be consumed as if it described the new draft.
|
|
3718
|
+
const qaSessionIdForSpawn = meta?.sessionId || meta?.item?.meta?.sessionId;
|
|
3719
|
+
const qaSessionPhaseForSpawn = meta?.sessionPhase || meta?.item?.meta?.sessionPhase;
|
|
3720
|
+
if (qaSessionIdForSpawn && qaSessionPhaseForSpawn === 'draft') {
|
|
3721
|
+
require('./engine/qa/visual-journey').prepareDraftResultSidecar(agentId, qaSessionIdForSpawn);
|
|
3437
3722
|
}
|
|
3438
3723
|
|
|
3439
3724
|
// W-mp73x32w000l143d: decouple agent cwd from worktree placement.
|
|
@@ -3492,10 +3777,100 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
3492
3777
|
}
|
|
3493
3778
|
const validatedWorkdir = _wdValidation.value; // null when unset / empty
|
|
3494
3779
|
|
|
3495
|
-
|
|
3780
|
+
// ── Control-plane execution surface (W-ms5tb6ha016fda10) ──────────────
|
|
3781
|
+
// Explicit, structured opt-out of repository checkout for tasks whose whole
|
|
3782
|
+
// contract is Minions/dashboard REST calls plus external CLIs (`gh`, `az`).
|
|
3783
|
+
// Never inferred from prose. Validation is fail-closed at the ALLOWLIST in
|
|
3784
|
+
// shared.CONTROL_PLANE_ELIGIBLE_TYPES, so a repo-mutating type that declares
|
|
3785
|
+
// `control-plane` is REJECTED here rather than quietly losing its worktree.
|
|
3786
|
+
// The dashboard/scheduler validate on create; this is the defense-in-depth
|
|
3787
|
+
// gate for ad-hoc dispatches and pre-validator work items.
|
|
3788
|
+
const _rawSurface = meta?.item?.meta?.executionSurface ?? meta?.executionSurface;
|
|
3789
|
+
const _surfaceValidation = shared.validateExecutionSurface(_rawSurface, type);
|
|
3790
|
+
if (!_surfaceValidation.valid) {
|
|
3791
|
+
const _wiId = meta?.item?.id || meta?.workItemId || id;
|
|
3792
|
+
log('warn', `spawnAgent: executionSurface validation rejected dispatch ${id} (WI ${_wiId}): ${_surfaceValidation.error}`);
|
|
3793
|
+
try {
|
|
3794
|
+
writeInboxAlert(`invalid-execution-surface-${_wiId}`, [
|
|
3795
|
+
`# Invalid meta.executionSurface on ${_wiId}`,
|
|
3796
|
+
``,
|
|
3797
|
+
`Dispatch \`${id}\` for agent \`${agentId}\` (type \`${type}\`) was rejected before spawn.`,
|
|
3798
|
+
``,
|
|
3799
|
+
`**Reason:** ${_surfaceValidation.error}`,
|
|
3800
|
+
``,
|
|
3801
|
+
`**Submitted value:** \`${typeof _rawSurface === 'string' ? _rawSurface : JSON.stringify(_rawSurface)}\``,
|
|
3802
|
+
``,
|
|
3803
|
+
`\`meta.executionSurface\` is an explicit declaration, never inferred. Valid values are \`repo\` (default — normal worktree/live checkout), \`control-plane\` (no repository checkout; the task only calls the Minions API / external CLIs), and \`cloud\` (handed to a remote coding agent that authors its own PR). Only these work types may declare \`control-plane\`: ${[...shared.CONTROL_PLANE_ELIGIBLE_TYPES].sort().join(', ')}. Only these may declare \`cloud\`: ${[...shared.CLOUD_ELIGIBLE_TYPES].sort().join(', ')}.`,
|
|
3804
|
+
``,
|
|
3805
|
+
`Fix the WI's \`meta.executionSurface\` (or remove it) and re-dispatch.`,
|
|
3806
|
+
].join('\n'));
|
|
3807
|
+
} catch (e) { log('warn', `invalid-execution-surface inbox alert write failed: ${e.message}`); }
|
|
3808
|
+
completeDispatch(
|
|
3809
|
+
id,
|
|
3810
|
+
DISPATCH_RESULT.ERROR,
|
|
3811
|
+
_surfaceValidation.error.slice(0, 800),
|
|
3812
|
+
'meta.executionSurface validation rejected this dispatch — fix or remove the field before re-dispatch.',
|
|
3813
|
+
{ failureClass: FAILURE_CLASS.INVALID_EXECUTION_SURFACE, agentRetryable: false },
|
|
3814
|
+
);
|
|
3815
|
+
cleanupTempAgent(agentId);
|
|
3816
|
+
return null;
|
|
3817
|
+
}
|
|
3818
|
+
const executionSurface = _surfaceValidation.value;
|
|
3819
|
+
const isControlPlane = executionSurface === shared.EXECUTION_SURFACE.CONTROL_PLANE;
|
|
3820
|
+
|
|
3821
|
+
// ── Cloud execution surface — fail-closed provider gate ────────────────
|
|
3822
|
+
// A validly-declared `cloud` item is handed to a REMOTE coding agent; it
|
|
3823
|
+
// must never quietly fall through to a local dispatch, because the two
|
|
3824
|
+
// produce different work in different places under different identities.
|
|
3825
|
+
// So the gate refuses the dispatch unless a configured provider resolves,
|
|
3826
|
+
// and (until the hand-off itself lands) refuses it even when one does.
|
|
3827
|
+
//
|
|
3828
|
+
// Every refusal here is STRUCTURAL — an unregistered/undeclared provider or
|
|
3829
|
+
// one that reports itself unconfigured reproduces identically on retry — so
|
|
3830
|
+
// it is non-retryable and must not consume the item's retry budget.
|
|
3831
|
+
if (executionSurface === shared.EXECUTION_SURFACE.CLOUD) {
|
|
3832
|
+
const _wiId = meta?.item?.id || meta?.workItemId || id;
|
|
3833
|
+
const _cloudTarget = cloudProviders.resolveCloudDispatchTarget({
|
|
3834
|
+
item: meta?.item || meta,
|
|
3835
|
+
project,
|
|
3836
|
+
engine: config?.engine,
|
|
3837
|
+
config,
|
|
3838
|
+
});
|
|
3839
|
+
const _cloudReason = _cloudTarget.ok
|
|
3840
|
+
? `cloud provider "${_cloudTarget.id}" is registered and configured, but the cloud dispatch `
|
|
3841
|
+
+ `hand-off is not implemented yet — the engine refuses to run cloud-declared work locally`
|
|
3842
|
+
: _cloudTarget.reason;
|
|
3843
|
+
log('warn', `spawnAgent: cloud dispatch ${id} (WI ${_wiId}) refused: ${_cloudReason}`);
|
|
3844
|
+
try {
|
|
3845
|
+
writeInboxAlert(`cloud-provider-unavailable-${_wiId}`, [
|
|
3846
|
+
`# Cloud dispatch refused for ${_wiId}`,
|
|
3847
|
+
``,
|
|
3848
|
+
`Dispatch \`${id}\` for agent \`${agentId}\` (type \`${type}\`) declared \`meta.executionSurface: "cloud"\` but was not sent anywhere.`,
|
|
3849
|
+
``,
|
|
3850
|
+
`**Reason:** ${_cloudReason}`,
|
|
3851
|
+
``,
|
|
3852
|
+
`**Registered providers:** ${cloudProviders.listCloudProviders().join(', ') || '(none)'}`,
|
|
3853
|
+
``,
|
|
3854
|
+
`The engine never downgrades a cloud declaration to a local dispatch — a remote agent works in its own checkout, under its own identity, and opens its own PR, so silently running it here would produce different work than was asked for.`,
|
|
3855
|
+
``,
|
|
3856
|
+
`Either configure a cloud provider (\`meta.cloudProvider\` on the WI, \`cloudProvider\` on the project, or \`engine.cloudProvider\`) or remove \`meta.executionSurface\` to dispatch locally.`,
|
|
3857
|
+
].join('\n'));
|
|
3858
|
+
} catch (e) { log('warn', `cloud-provider-unavailable inbox alert write failed: ${e.message}`); }
|
|
3859
|
+
completeDispatch(
|
|
3860
|
+
id,
|
|
3861
|
+
DISPATCH_RESULT.ERROR,
|
|
3862
|
+
_cloudReason.slice(0, 800),
|
|
3863
|
+
'Cloud dispatch refused — no usable cloud provider for this work item.',
|
|
3864
|
+
{ failureClass: FAILURE_CLASS.CLOUD_PROVIDER_UNAVAILABLE, agentRetryable: false },
|
|
3865
|
+
);
|
|
3866
|
+
cleanupTempAgent(agentId);
|
|
3867
|
+
return null;
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3870
|
+
let cwd, worktreeRootDir, liveMode = false, readOnlyWorktree = false, controlPlane = false;
|
|
3496
3871
|
try {
|
|
3497
|
-
({ cwd, worktreeRootDir, liveMode = false, readOnlyWorktree = false } =
|
|
3498
|
-
shared.resolveSpawnPaths(project, type, MINIONS_DIR, { workdir: validatedWorkdir }));
|
|
3872
|
+
({ cwd, worktreeRootDir, liveMode = false, readOnlyWorktree = false, controlPlane = false } =
|
|
3873
|
+
shared.resolveSpawnPaths(project, type, MINIONS_DIR, { workdir: validatedWorkdir, executionSurface }));
|
|
3499
3874
|
} catch (rootErr) {
|
|
3500
3875
|
if (rootErr?.code === 'WORKTREE_ROOTDIR_COLLAPSED_TO_DRIVE_ROOT' || rootErr?.code === 'WORKTREE_ROOTDIR_MISSING_BASE') {
|
|
3501
3876
|
log('error', `spawnAgent: project rootDir resolution failed for ${id}: ${rootErr.message}`);
|
|
@@ -3551,6 +3926,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
3551
3926
|
? { name: project.name, localPath: project.localPath }
|
|
3552
3927
|
: null;
|
|
3553
3928
|
dispatchItem.checkoutMode = checkoutModeAtDispatch;
|
|
3929
|
+
dispatchItem.executionSurface = executionSurface;
|
|
3554
3930
|
if (resolvedProjectMeta && dispatchItem.meta) {
|
|
3555
3931
|
dispatchItem.meta.project = resolvedProjectMeta;
|
|
3556
3932
|
}
|
|
@@ -3560,6 +3936,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
3560
3936
|
const found = (dispatch?.[queue] || []).find(d => d && d.id === id);
|
|
3561
3937
|
if (!found) continue;
|
|
3562
3938
|
found.checkoutMode = checkoutModeAtDispatch;
|
|
3939
|
+
found.executionSurface = executionSurface;
|
|
3563
3940
|
if (resolvedProjectMeta && found.meta) found.meta.project = resolvedProjectMeta;
|
|
3564
3941
|
}
|
|
3565
3942
|
return dispatch;
|
|
@@ -3567,6 +3944,13 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
3567
3944
|
} catch (e) {
|
|
3568
3945
|
log('warn', `spawnAgent: failed to persist checkout context for ${id}: ${e.message}`);
|
|
3569
3946
|
}
|
|
3947
|
+
if (controlPlane) {
|
|
3948
|
+
// Observability: the dispatch record itself carries `executionSurface`, so
|
|
3949
|
+
// "how many checkouts did the control-plane surface avoid?" is derived
|
|
3950
|
+
// from dispatch history by GET /api/worktree-inventory — no extra counter
|
|
3951
|
+
// state to migrate or drift.
|
|
3952
|
+
log('info', `${type}: control-plane dispatch ${id} — no repository checkout allocated; cwd ${cwd}`);
|
|
3953
|
+
}
|
|
3570
3954
|
// Legacy local alias: downstream git ops (worktree add, prune, fetch) and
|
|
3571
3955
|
// the `cwd === rootDir` safety warn at line ~1387 reference `rootDir`. For
|
|
3572
3956
|
// read-only rootless tasks (no worktree, no branch) this is null — the
|
|
@@ -3578,7 +3962,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
3578
3962
|
// allocation probe and checkout. This runs before prompt/worktree creation
|
|
3579
3963
|
// and leaves the existing dispatch pending rather than completing it as an
|
|
3580
3964
|
// agent failure.
|
|
3581
|
-
const _spawnDiskPreflight = _checkWorktreeDiskSpaceForDispatch(project, type, config);
|
|
3965
|
+
const _spawnDiskPreflight = _checkWorktreeDiskSpaceForDispatch(project, type, config, { executionSurface });
|
|
3582
3966
|
if (_spawnDiskPreflight && !_spawnDiskPreflight.ok) {
|
|
3583
3967
|
const pendingReason = _persistWorktreeDiskPreflight(dispatchItem, _spawnDiskPreflight);
|
|
3584
3968
|
_writeWorktreeDiskPreflightAlert(dispatchItem, project, _spawnDiskPreflight);
|
|
@@ -3597,6 +3981,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
3597
3981
|
type,
|
|
3598
3982
|
_preBranchName,
|
|
3599
3983
|
config,
|
|
3984
|
+
{ executionSurface },
|
|
3600
3985
|
);
|
|
3601
3986
|
if (spawnAttemptLease && !_isSpawnAttemptCurrent(spawnAttemptLease)) {
|
|
3602
3987
|
return SPAWN_ATTEMPT_ABORTED;
|
|
@@ -3651,7 +4036,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
3651
4036
|
// engine compares report.nonce against the in-memory value below; on
|
|
3652
4037
|
// mismatch the report is treated as forged (e.g. a prompt-injected agent
|
|
3653
4038
|
// writing into a sibling agent's completion path) and discarded. See
|
|
3654
|
-
// engine/lifecycle.js:runPostCompletionHooks and docs/completion-reports.md.
|
|
4039
|
+
// engine/orchestration/lifecycle.js:runPostCompletionHooks and docs/completion-reports.md.
|
|
3655
4040
|
const completionNonce = crypto.randomBytes(16).toString('hex');
|
|
3656
4041
|
const completionReportInstruction = completionReportPath ? [
|
|
3657
4042
|
'## Completion Report',
|
|
@@ -3890,11 +4275,18 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
3890
4275
|
};
|
|
3891
4276
|
_phaseT.afterPrompt = Date.now();
|
|
3892
4277
|
|
|
3893
|
-
if (branchName && READ_ONLY_ROOT_TASK_TYPES.has(type)) {
|
|
4278
|
+
if (branchName && (READ_ONLY_ROOT_TASK_TYPES.has(type) || isControlPlane)) {
|
|
3894
4279
|
// Read-only dispatches never own a branch. Project-bound worktree-mode
|
|
3895
4280
|
// dispatches still get a detached worktree below; project-less/live tasks
|
|
3896
4281
|
// keep their resolved cwd.
|
|
3897
|
-
|
|
4282
|
+
//
|
|
4283
|
+
// W-ms5tb6ha016fda10: a validly-declared control-plane dispatch owns no
|
|
4284
|
+
// branch either — it has no checkout to put one in. Dropping the label
|
|
4285
|
+
// here (rather than only at derivation) also covers explicitly-set
|
|
4286
|
+
// `item.branch` and schedule-supplied labels, and keeps the dispatch out
|
|
4287
|
+
// of the branch mutex so independent maintenance jobs don't serialize.
|
|
4288
|
+
const reason = isControlPlane ? 'control-plane' : 'read-only';
|
|
4289
|
+
log('info', `${type}: ignoring branch label ${branchName} for ${reason} dispatch`);
|
|
3898
4290
|
branchName = null;
|
|
3899
4291
|
}
|
|
3900
4292
|
|
|
@@ -3906,7 +4298,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
3906
4298
|
// target branch. W-mrcifup2000c218a: the dirty/stale-base conditions below
|
|
3907
4299
|
// are now caught EARLIER, at allocation time, by the pre-dispatch checks in
|
|
3908
4300
|
// the dispatch-existing-pending loop (`checkLiveCheckoutDirty` /
|
|
3909
|
-
// `checkLiveCheckoutStaleBase` in engine/live-checkout.js) — a positive hit
|
|
4301
|
+
// `checkLiveCheckoutStaleBase` in engine/worktrees/live-checkout.js) — a positive hit
|
|
3910
4302
|
// there leaves the WI pending with `_pendingReason` stamped and never reaches
|
|
3911
4303
|
// spawnAgent at all, so this block is now the EXCEPTION path (defense in
|
|
3912
4304
|
// depth for the race window between that probe and the git operations
|
|
@@ -3959,7 +4351,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
3959
4351
|
// fork off baseline" signal.
|
|
3960
4352
|
const _liveAllowNonMainBase = !!(meta?.useExistingBranch || meta?.branchStrategy === 'shared-branch');
|
|
3961
4353
|
const _wiIdForAlert = meta?.item?.id || id;
|
|
3962
|
-
const _liveCheckout = require('./engine/live-checkout');
|
|
4354
|
+
const _liveCheckout = require('./engine/worktrees/live-checkout');
|
|
3963
4355
|
// W-mrawgw4q000a6bff: stash-before-reset ordering. Resolve BOTH recovery
|
|
3964
4356
|
// policies here (before the first prepareLiveCheckout call) so we can
|
|
3965
4357
|
// decide precedence up front instead of letting prepareLiveCheckout's
|
|
@@ -4825,7 +5217,11 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
4825
5217
|
}
|
|
4826
5218
|
try {
|
|
4827
5219
|
const _mainRef = sanitizeBranch(shared.resolveMainBranch(rootDir, project.mainBranch));
|
|
4828
|
-
|
|
5220
|
+
// W-ms5tb6ha016fda10: shared remote-tracking ref — serialize the
|
|
5221
|
+
// refresh so a concurrent pool borrow / fresh create in the same
|
|
5222
|
+
// clone cannot lose a `refs/remotes/origin/<main>` lock race.
|
|
5223
|
+
await shared.withSerializedRemoteRefRefresh(rootDir, () =>
|
|
5224
|
+
shared.shellSafeGit(['fetch', 'origin', _mainRef], { ..._gitOpts, cwd: rootDir, timeout: 30000 }));
|
|
4829
5225
|
// -B force-creates/resets the branch so a stale local ref from a
|
|
4830
5226
|
// prior occupant does not block the checkout.
|
|
4831
5227
|
await shared.shellSafeGit(['checkout', '-B', branchName, `origin/${_mainRef}`], { ..._gitOpts, cwd: borrowed.path, timeout: 30000 });
|
|
@@ -5374,7 +5770,8 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
5374
5770
|
const _depIsSharedBranch = meta?.branchStrategy === 'shared-branch' || meta?.useExistingBranch;
|
|
5375
5771
|
if (!_failedRefCache.has(_depMainRef)) {
|
|
5376
5772
|
try {
|
|
5377
|
-
await
|
|
5773
|
+
await shared.withSerializedRemoteRefRefresh(rootDir, () =>
|
|
5774
|
+
adoGitAuth.runAdoGit(project, ['fetch', 'origin', _depMainRef], { ..._gitOpts, cwd: rootDir }));
|
|
5378
5775
|
} catch (mainFetchErr) {
|
|
5379
5776
|
log('warn', `Failed to fetch origin/${_depMainRef} before dep merge: ${mainFetchErr.message} — proceeding with stale base`);
|
|
5380
5777
|
if (adoGitAuth.isAdoAuthFailure(mainFetchErr)) _depAuthFailed = true;
|
|
@@ -5761,6 +6158,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
5761
6158
|
}
|
|
5762
6159
|
// Build actionable failReason identifying the conflicting branch and files (#958)
|
|
5763
6160
|
const mainBranch = sanitizeBranch(shared.resolveMainBranch(rootDir, project.mainBranch));
|
|
6161
|
+
const _conflictsWith = (_isInterDepConflict && _preflightConflictPrev) ? _preflightConflictPrev : mainBranch;
|
|
5764
6162
|
let failReason = 'Dependency merge failed';
|
|
5765
6163
|
if (depConflictBranch) {
|
|
5766
6164
|
if (_isInterDepConflict && _preflightConflictPrev) {
|
|
@@ -5771,15 +6169,24 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
5771
6169
|
if (depConflictFiles.length > 0) failReason += ` in ${depConflictFiles.slice(0, 5).join(', ')}`;
|
|
5772
6170
|
failReason += ' — dep branch needs updating';
|
|
5773
6171
|
}
|
|
5774
|
-
completeDispatch(id, DISPATCH_RESULT.ERROR, failReason, '', { failureClass: FAILURE_CLASS.MERGE_CONFLICT });
|
|
5775
6172
|
|
|
5776
|
-
//
|
|
5777
|
-
//
|
|
5778
|
-
//
|
|
5779
|
-
|
|
6173
|
+
// W-mrzqfozm00035365 — Recoverable dependency merge-conflict gate.
|
|
6174
|
+
// A specific dependency-branch conflict detected BEFORE the agent
|
|
6175
|
+
// process starts is NOT an execution failure of the downstream item.
|
|
6176
|
+
// When we can identify the blocked downstream WI, create/reuse the
|
|
6177
|
+
// deterministic conflict-repair WI, link the blocked item, hold the
|
|
6178
|
+
// downstream PENDING behind a `_dependencyConflict` marker, and defer
|
|
6179
|
+
// the dispatch (no retry consumed). The discovery gate then wakes the
|
|
6180
|
+
// downstream once the repair is done — instead of burning the
|
|
6181
|
+
// downstream's retry budget to terminal `failed` while the repair is
|
|
6182
|
+
// still being worked. Routes the repair through the shared-branch
|
|
6183
|
+
// dispatch path (see buildDepConflictFixItem) so commits land on the
|
|
6184
|
+
// dep's existing PR branch (W-mpcwojgr000a0244).
|
|
6185
|
+
const _canRecoverDepConflict = !!(depConflictBranch && meta?.item?.id && project);
|
|
6186
|
+
if (_canRecoverDepConflict) {
|
|
5780
6187
|
try {
|
|
5781
6188
|
const wiScope = project.name ? project : 'central';
|
|
5782
|
-
const
|
|
6189
|
+
const repairItem = buildDepConflictFixItem({
|
|
5783
6190
|
depConflictBranch,
|
|
5784
6191
|
depConflictFiles,
|
|
5785
6192
|
isInterDepConflict: _isInterDepConflict,
|
|
@@ -5788,15 +6195,61 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
5788
6195
|
blockedItem: meta.item,
|
|
5789
6196
|
projectName: project.name || null,
|
|
5790
6197
|
});
|
|
6198
|
+
let repairId = repairItem.id;
|
|
5791
6199
|
mutateWorkItems(wiScope, items => {
|
|
5792
|
-
//
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
6200
|
+
// Create-or-reuse the deterministic conflict-repair WI. Link
|
|
6201
|
+
// THIS downstream item into `_blockedItems` idempotently so
|
|
6202
|
+
// multiple downstream items can share one repair and completing
|
|
6203
|
+
// it wakes all of them.
|
|
6204
|
+
const existing = items.find(i => i.id === repairItem.id);
|
|
6205
|
+
if (!existing) {
|
|
6206
|
+
items.push(repairItem);
|
|
6207
|
+
log('info', `Auto-queued conflict-fix work item ${repairItem.id} for ${depConflictBranch} (blocked: ${meta.item.id})`);
|
|
6208
|
+
} else {
|
|
6209
|
+
repairId = existing.id;
|
|
6210
|
+
if (!Array.isArray(existing._blockedItems)) {
|
|
6211
|
+
existing._blockedItems = existing._blockedItem ? [existing._blockedItem] : [];
|
|
6212
|
+
}
|
|
6213
|
+
if (!existing._blockedItems.includes(meta.item.id)) {
|
|
6214
|
+
existing._blockedItems.push(meta.item.id);
|
|
6215
|
+
}
|
|
6216
|
+
}
|
|
6217
|
+
// Stamp the blocked downstream WI: hold PENDING with the
|
|
6218
|
+
// recoverable marker + actionable pending reason. No retry
|
|
6219
|
+
// charged, no terminal failure.
|
|
6220
|
+
const downstream = items.find(i => i.id === meta.item.id);
|
|
6221
|
+
if (downstream) {
|
|
6222
|
+
downstream.status = WI_STATUS.PENDING;
|
|
6223
|
+
downstream._dependencyConflict = buildDependencyConflictMarker({
|
|
6224
|
+
repairWiId: repairId,
|
|
6225
|
+
conflictBranch: depConflictBranch,
|
|
6226
|
+
conflictsWith: _conflictsWith,
|
|
6227
|
+
isInterDep: _isInterDepConflict,
|
|
6228
|
+
files: depConflictFiles,
|
|
6229
|
+
detectedAt: ts(),
|
|
6230
|
+
});
|
|
6231
|
+
downstream._pendingReason = DEP_CONFLICT_PENDING_REASON;
|
|
6232
|
+
delete downstream.dispatched_at;
|
|
6233
|
+
delete downstream.dispatched_to;
|
|
6234
|
+
}
|
|
6235
|
+
return items;
|
|
5797
6236
|
});
|
|
5798
|
-
|
|
6237
|
+
// Tear the dispatch down as a recoverable deferral — NOT a
|
|
6238
|
+
// completion — so no retry is consumed and the downstream WI is
|
|
6239
|
+
// free to re-dispatch once the gate clears.
|
|
6240
|
+
deferDispatchToPending(id, `dependency conflict: ${depConflictBranch} conflicts with ${_conflictsWith} (repair ${repairId})`);
|
|
6241
|
+
log('info', `Dependency conflict on ${depConflictBranch} blocks ${meta.item.id} — held pending behind repair ${repairId} (no retry consumed)`);
|
|
6242
|
+
} catch (e) {
|
|
6243
|
+
log('warn', `Recoverable dep-conflict gate failed for ${meta?.item?.id}: ${e.message} — falling back to failure`);
|
|
6244
|
+
completeDispatch(id, DISPATCH_RESULT.ERROR, failReason, '', { failureClass: FAILURE_CLASS.MERGE_CONFLICT });
|
|
6245
|
+
}
|
|
6246
|
+
cleanupTempAgent(agentId);
|
|
6247
|
+
return;
|
|
5799
6248
|
}
|
|
6249
|
+
|
|
6250
|
+
// No identifiable downstream item (central / no meta.item) — preserve
|
|
6251
|
+
// the original terminal failure behavior.
|
|
6252
|
+
completeDispatch(id, DISPATCH_RESULT.ERROR, failReason, '', { failureClass: FAILURE_CLASS.MERGE_CONFLICT });
|
|
5800
6253
|
cleanupTempAgent(agentId);
|
|
5801
6254
|
return;
|
|
5802
6255
|
}
|
|
@@ -6101,6 +6554,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
6101
6554
|
|
|
6102
6555
|
// Spawn the selected harness runtime.
|
|
6103
6556
|
const childEnv = shared.cleanChildEnv();
|
|
6557
|
+
shared.applyAgentTestLoadSafetyEnv(childEnv);
|
|
6104
6558
|
if (completionReportPath) childEnv.MINIONS_COMPLETION_REPORT = completionReportPath;
|
|
6105
6559
|
if (completionNonce) childEnv.MINIONS_COMPLETION_NONCE = completionNonce;
|
|
6106
6560
|
childEnv.MINIONS_REPO_HOST = getRepoHost(project);
|
|
@@ -6151,7 +6605,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
6151
6605
|
// W-mpg54mi2000n7b7e — inject a per-slug GitHub PAT so child `gh`/`git`
|
|
6152
6606
|
// calls authenticate as the right account without any `gh auth login`
|
|
6153
6607
|
// interactive flow. Resolution honors config.engine.ghAccounts via
|
|
6154
|
-
// engine/gh-token.js (exact owner → owner-glob → fleet default → null).
|
|
6608
|
+
// engine/providers/gh-token.js (exact owner → owner-glob → fleet default → null).
|
|
6155
6609
|
// Mirrors the MINIONS_ADO_TOKEN injection above for ADO projects.
|
|
6156
6610
|
try {
|
|
6157
6611
|
const slug = shared.getProjectOrg(project) && project?.repoName
|
|
@@ -6165,12 +6619,12 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
6165
6619
|
if (_abortStaleSpawn()) return SPAWN_ATTEMPT_ABORTED;
|
|
6166
6620
|
// Spawn via wrapper script — node directly (no bash intermediary)
|
|
6167
6621
|
// spawn-agent.js handles CLAUDECODE env cleanup and claude binary resolution
|
|
6168
|
-
const spawnScript = runtime.spawnScript || path.join(ENGINE_DIR, 'spawn-agent.js');
|
|
6622
|
+
const spawnScript = runtime.spawnScript || path.join(ENGINE_DIR, 'agents', 'spawn-agent.js');
|
|
6169
6623
|
|
|
6170
6624
|
const spawnArgs = [spawnScript, promptPath, sysPromptPath, ...args];
|
|
6171
6625
|
// P-1d8f0b93 — the pooled-dispatch branch further down (agentWorkerPool
|
|
6172
6626
|
// acquireWorker) accepts an optional `hermeticDirs` (workspace-manifest
|
|
6173
|
-
// `allowed_repos` beyond the primary cwd — see engine/agent-worker-pool.js
|
|
6627
|
+
// `allowed_repos` beyond the primary cwd — see engine/agents/agent-worker-pool.js
|
|
6174
6628
|
// header doc). Repository harness discovery (skills/commands/--add-dir) is
|
|
6175
6629
|
// now delegated entirely to the native runtime CLI (#817), so there is no
|
|
6176
6630
|
// longer an engine-computed add-dir set to reuse here; leave it empty and
|
|
@@ -6178,6 +6632,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
6178
6632
|
const addDirs = [];
|
|
6179
6633
|
|
|
6180
6634
|
let dispatchActivatedForLaunch = false;
|
|
6635
|
+
let runtimePidObservedAt = 0;
|
|
6181
6636
|
const _activateDispatchForRuntime = (phase) => {
|
|
6182
6637
|
let activated = false;
|
|
6183
6638
|
mutateDispatch((dispatch) => {
|
|
@@ -6223,6 +6678,8 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
6223
6678
|
item._spawnPhase = 'running';
|
|
6224
6679
|
if (Number.isInteger(proc?.pid) && proc.pid > 0) {
|
|
6225
6680
|
item.runtimePid = proc.pid;
|
|
6681
|
+
runtimePidObservedAt = Date.now();
|
|
6682
|
+
item.runtimePidObservedAt = runtimePidObservedAt;
|
|
6226
6683
|
}
|
|
6227
6684
|
persisted = true;
|
|
6228
6685
|
return dispatch;
|
|
@@ -6246,6 +6703,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
6246
6703
|
delete item.pooled;
|
|
6247
6704
|
delete item.executionCwd;
|
|
6248
6705
|
delete item.runtimePid;
|
|
6706
|
+
delete item.runtimePidObservedAt;
|
|
6249
6707
|
delete item._spawnPhase;
|
|
6250
6708
|
if (!dispatch.pending.some(entry => entry.id === id)) dispatch.pending.push(item);
|
|
6251
6709
|
return dispatch;
|
|
@@ -6304,7 +6762,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
6304
6762
|
}
|
|
6305
6763
|
let pidFilePath, logFd, registeredInActivityMap, registeredInActiveProcesses, proc;
|
|
6306
6764
|
// The PID file at this path is written asynchronously by the spawned
|
|
6307
|
-
// child (engine/spawn-agent.js:423) once it starts. We compute the path
|
|
6765
|
+
// child (engine/agents/spawn-agent.js:423) once it starts. We compute the path
|
|
6308
6766
|
// upfront so the catch block can unlink it whether spawn fails before
|
|
6309
6767
|
// the child wrote it (no-op) or after (real cleanup).
|
|
6310
6768
|
pidFilePath = promptPath.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid');
|
|
@@ -6396,7 +6854,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
6396
6854
|
promptText: fs.readFileSync(promptPath, 'utf8'),
|
|
6397
6855
|
});
|
|
6398
6856
|
// The child_process PID file is normally written asynchronously by
|
|
6399
|
-
// spawn-agent.js itself (engine/spawn-agent.js:606) once it starts.
|
|
6857
|
+
// spawn-agent.js itself (engine/agents/spawn-agent.js:606) once it starts.
|
|
6400
6858
|
// There is no such child process here, so the engine writes the
|
|
6401
6859
|
// equivalent file itself as soon as the pool hands back a real OS PID
|
|
6402
6860
|
// (mirrors that write: async, best-effort, same `String(pid)` shape).
|
|
@@ -6426,13 +6884,14 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
6426
6884
|
startedAt,
|
|
6427
6885
|
runtimeName,
|
|
6428
6886
|
sessionId: cachedSessionId,
|
|
6887
|
+
runtimePidObservedAt,
|
|
6429
6888
|
_completionNonce: completionNonce,
|
|
6430
6889
|
...(cachedSessionId ? {
|
|
6431
6890
|
_runtimeResumeAt: Date.now(),
|
|
6432
6891
|
_runtimeResumeAwaitingFirstOutput: true,
|
|
6433
6892
|
} : {}),
|
|
6434
6893
|
_pendingSteeringFiles: pendingSteering.entries,
|
|
6435
|
-
// P-1d8f0b93 — marks a pooled dispatch so engine/timeout.js's
|
|
6894
|
+
// P-1d8f0b93 — marks a pooled dispatch so engine/orchestration/timeout.js's
|
|
6436
6895
|
// cancel-vs-kill wiring (P-6e2a4c15, killTrackedProcess /
|
|
6437
6896
|
// _escalatePlatformKill) can route `.kill()` to cancel+release the
|
|
6438
6897
|
// ACP worker instead of SIGKILL/taskkill on the shared worker's OS pid.
|
|
@@ -6450,7 +6909,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
6450
6909
|
// banner on tab switch / agent change).
|
|
6451
6910
|
//
|
|
6452
6911
|
// ONLY emitted on fresh spawns — re-attach after engine restart goes
|
|
6453
|
-
// through engine/cli.js and does NOT pass through here, so a re-attached
|
|
6912
|
+
// through engine/operations/cli.js and does NOT pass through here, so a re-attached
|
|
6454
6913
|
// dispatch (procInfo.reattached === true) won't synthesise a spurious
|
|
6455
6914
|
// started event that could clobber a still-live previous-dispatch
|
|
6456
6915
|
// banner that the user is mid-read on.
|
|
@@ -6476,9 +6935,9 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
6476
6935
|
// kill the orphan child so it doesn't run unmonitored. shared.killImmediate
|
|
6477
6936
|
// recurses into the process tree (footgun #4) — plain proc.kill('SIGKILL')
|
|
6478
6937
|
// doesn't on Windows.
|
|
6479
|
-
try { shared.
|
|
6938
|
+
try { shared.terminateProcess('engine.spawn-partial-failure', proc); } catch { /* already exited */ }
|
|
6480
6939
|
} else if (proc && typeof proc.kill === 'function') {
|
|
6481
|
-
try {
|
|
6940
|
+
try { shared.terminateProcess('engine.spawn-partial-pooled', proc); } catch { /* already exited */ }
|
|
6482
6941
|
}
|
|
6483
6942
|
if (registeredInActiveProcesses) {
|
|
6484
6943
|
activeProcesses.delete(id);
|
|
@@ -6618,7 +7077,35 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
6618
7077
|
|
|
6619
7078
|
const procInfo = activeProcesses.get(id);
|
|
6620
7079
|
ackPendingSteeringFiles(agentId, procInfo, steeringAckStdout);
|
|
6621
|
-
|
|
7080
|
+
|
|
7081
|
+
// W-ms9tcs3o01y03383 — process exit + a valid, dispatch-owned completion
|
|
7082
|
+
// report IS the completion boundary for this prompt.
|
|
7083
|
+
//
|
|
7084
|
+
// `code` here comes from the child's `close` event, so terminal exit is
|
|
7085
|
+
// confirmed by construction, and the report is only accepted when it echoes
|
|
7086
|
+
// the per-spawn nonce (carried across steering resumes). Both halves are
|
|
7087
|
+
// required: a missing/forged report, or a still-live process, keeps every
|
|
7088
|
+
// existing failure/timeout path exactly as it was. This never kills a live
|
|
7089
|
+
// agent for silence — it only short-circuits AFTER the process is gone.
|
|
7090
|
+
//
|
|
7091
|
+
// When both hold, any queued resume bookkeeping is released rather than
|
|
7092
|
+
// spawned, so the dispatch finalizes on this same async continuation
|
|
7093
|
+
// instead of waiting out `engine.resumeHeartbeatTimeout` for a first
|
|
7094
|
+
// heartbeat that a completed turn will never produce.
|
|
7095
|
+
const terminalCompletionReport = resolveCloseCompletionBoundary(
|
|
7096
|
+
dispatchItem,
|
|
7097
|
+
code,
|
|
7098
|
+
procInfo?._completionNonce || completionNonce,
|
|
7099
|
+
);
|
|
7100
|
+
let closeCompletionPath = shared.DISPATCH_COMPLETION_PATH.PROCESS_CLOSE;
|
|
7101
|
+
if (terminalCompletionReport) {
|
|
7102
|
+
const releasedResume = releasePendingSteeringResumeForCompletedTurn(agentId, procInfo, liveOutputPath);
|
|
7103
|
+
if (releasedResume.released || releasedResume.deferred > 0) {
|
|
7104
|
+
closeCompletionPath = shared.DISPATCH_COMPLETION_PATH.REPORT_RECOVERY_IMMEDIATE;
|
|
7105
|
+
}
|
|
7106
|
+
} else {
|
|
7107
|
+
promoteCheckpointSteeringForClose(agentId, procInfo, runtime, liveOutputPath);
|
|
7108
|
+
}
|
|
6622
7109
|
|
|
6623
7110
|
// #853 — the child is gone, so its clean committed state is stable. Push
|
|
6624
7111
|
// any strictly-ahead dispatch branch before steering resume, pool return,
|
|
@@ -6670,7 +7157,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
6670
7157
|
// Live-kill flow first lands here; deferred-checkpoint flow
|
|
6671
7158
|
// also lands here from the natural-close branch above.
|
|
6672
7159
|
try {
|
|
6673
|
-
const store = require('./engine/steering-store');
|
|
7160
|
+
const store = require('./engine/persistence/steering-store');
|
|
6674
7161
|
const steerEntries = Array.isArray(steerEntry) ? steerEntry : (steerEntry ? [steerEntry] : []);
|
|
6675
7162
|
for (const entry of steerEntries) {
|
|
6676
7163
|
if (entry?.steerId) {
|
|
@@ -6726,8 +7213,9 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
6726
7213
|
return;
|
|
6727
7214
|
}
|
|
6728
7215
|
|
|
6729
|
-
const spawnScript = runtime.spawnScript || path.join(ENGINE_DIR, 'spawn-agent.js');
|
|
7216
|
+
const spawnScript = runtime.spawnScript || path.join(ENGINE_DIR, 'agents', 'spawn-agent.js');
|
|
6730
7217
|
const childEnv = shared.cleanChildEnv();
|
|
7218
|
+
shared.applyAgentTestLoadSafetyEnv(childEnv);
|
|
6731
7219
|
if (completionReportPath) childEnv.MINIONS_COMPLETION_REPORT = completionReportPath;
|
|
6732
7220
|
// P-d2a8f6c1: preserve the per-dispatch nonce across steering resume so
|
|
6733
7221
|
// the agent's completion JSON still validates after the resumed turn.
|
|
@@ -6809,18 +7297,23 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
6809
7297
|
cleanupTempAgent(agentId);
|
|
6810
7298
|
return;
|
|
6811
7299
|
}
|
|
7300
|
+
let resumePidObservedAt = 0;
|
|
6812
7301
|
const writeResumePid = () => {
|
|
6813
7302
|
if (!resumeProc.pid) return;
|
|
7303
|
+
resumePidObservedAt = Date.now();
|
|
6814
7304
|
try { fs.writeFileSync(pidFilePath, String(resumeProc.pid)); } catch { /* best-effort */ }
|
|
6815
7305
|
try {
|
|
6816
7306
|
mutateDispatch((dp) => {
|
|
6817
7307
|
const active = dp.active.find((entry) => entry.id === id);
|
|
6818
7308
|
if (active) {
|
|
6819
7309
|
active.runtimePid = resumeProc.pid;
|
|
7310
|
+
active.runtimePidObservedAt = resumePidObservedAt;
|
|
6820
7311
|
active._spawnPhase = 'running';
|
|
6821
7312
|
}
|
|
6822
7313
|
return dp;
|
|
6823
7314
|
});
|
|
7315
|
+
const tracked = activeProcesses.get(id);
|
|
7316
|
+
if (tracked?.proc === resumeProc) tracked.runtimePidObservedAt = resumePidObservedAt;
|
|
6824
7317
|
} catch (e) {
|
|
6825
7318
|
log('warn', `Steering: failed to persist resumed PID for ${agentId}: ${e.message}`);
|
|
6826
7319
|
}
|
|
@@ -6839,6 +7332,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
6839
7332
|
startedAt: procInfo.startedAt,
|
|
6840
7333
|
runtimeName,
|
|
6841
7334
|
sessionId: steerSessionId,
|
|
7335
|
+
runtimePidObservedAt: resumePidObservedAt,
|
|
6842
7336
|
// P-d2a8f6c1: keep the per-dispatch nonce alive across the steering resume.
|
|
6843
7337
|
_completionNonce: procInfo._completionNonce || completionNonce,
|
|
6844
7338
|
_runtimeResumeAt: Date.now(),
|
|
@@ -6877,7 +7371,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
6877
7371
|
if (resumeInfo && !resumeInfo._steeringDeliveredAt) {
|
|
6878
7372
|
resumeInfo._steeringDeliveredAt = Date.now();
|
|
6879
7373
|
try {
|
|
6880
|
-
const store = require('./engine/steering-store');
|
|
7374
|
+
const store = require('./engine/persistence/steering-store');
|
|
6881
7375
|
const pending = Array.isArray(resumeInfo._pendingSteeringFiles) ? resumeInfo._pendingSteeringFiles : [];
|
|
6882
7376
|
for (const pendingEntry of pending) {
|
|
6883
7377
|
if (pendingEntry?.steerId) {
|
|
@@ -6910,17 +7404,24 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
6910
7404
|
try { fs.appendFileSync(liveOutputPath, `\n[steering-failed] Resume exited with code ${resumeCode}. Your message was received but the agent could not continue the session.\n`); } catch {}
|
|
6911
7405
|
}
|
|
6912
7406
|
// Don't assume original work completed — run normal close handler to parse output and determine actual result
|
|
6913
|
-
|
|
7407
|
+
finalizeAgentClose(resumeCode);
|
|
6914
7408
|
return;
|
|
6915
7409
|
}
|
|
6916
7410
|
// Successful resume — run normal close handler
|
|
6917
|
-
|
|
7411
|
+
finalizeAgentClose(resumeCode);
|
|
6918
7412
|
});
|
|
6919
7413
|
resumeProc.on('error', (err) => {
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
7414
|
+
try {
|
|
7415
|
+
log('warn', `Steering re-spawn error for ${agentId}: ${err.message}`);
|
|
7416
|
+
try { fs.appendFileSync(liveOutputPath, `\n[steering-failed] Spawn error: ${err.message}. Your message was received but the agent could not resume.\n`); } catch {}
|
|
7417
|
+
activeProcesses.delete(id);
|
|
7418
|
+
completeDispatch(id, DISPATCH_RESULT.ERROR, `Steering re-spawn failed: ${err.message}`);
|
|
7419
|
+
} catch (handlerErr) {
|
|
7420
|
+
console.error(
|
|
7421
|
+
`[engine] Failed to contain steering re-spawn error for ${id}: ` +
|
|
7422
|
+
`${handlerErr?.stack || handlerErr}`,
|
|
7423
|
+
);
|
|
7424
|
+
}
|
|
6924
7425
|
});
|
|
6925
7426
|
|
|
6926
7427
|
// Don't run completion hooks — agent is still working
|
|
@@ -7065,7 +7566,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
7065
7566
|
|| !!dispatchItem.meta?.keep_processes_skip_workdir_check;
|
|
7066
7567
|
if (_kpEnabled) {
|
|
7067
7568
|
try {
|
|
7068
|
-
const keepProcessSweep = require('./engine/keep-process-sweep');
|
|
7569
|
+
const keepProcessSweep = require('./engine/processes/keep-process-sweep');
|
|
7069
7570
|
// First evaluate with workdir validation OFF so we always catch
|
|
7070
7571
|
// schema failures (pids-missing, ttl-too-long, etc.) regardless
|
|
7071
7572
|
// of whether requireGitWorkdir is enabled or skipped per-WI.
|
|
@@ -7168,7 +7669,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
7168
7669
|
parsedSnippet ? '## What you wrote\n\n```json\n' + parsedSnippet + '\n```\n' : '',
|
|
7169
7670
|
'## Canonical shape',
|
|
7170
7671
|
'',
|
|
7171
|
-
canonicalHint || '(see `engine/keep-process-sweep.js` `buildKeepProcessesHint` for the canonical shape.)',
|
|
7672
|
+
canonicalHint || '(see `engine/processes/keep-process-sweep.js` `buildKeepProcessesHint` for the canonical shape.)',
|
|
7172
7673
|
'',
|
|
7173
7674
|
].filter(Boolean).join('\n');
|
|
7174
7675
|
}
|
|
@@ -7215,7 +7716,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
7215
7716
|
{
|
|
7216
7717
|
if (managedSpawnRequested && lifecycleCompletionAccepted) {
|
|
7217
7718
|
try {
|
|
7218
|
-
const managedSpawn = require('./engine/managed-spawn');
|
|
7719
|
+
const managedSpawn = require('./engine/processes/managed-spawn');
|
|
7219
7720
|
const allowedCwdRoot = keepProcessesAllowedCwdRoot;
|
|
7220
7721
|
const wiId = dispatchItem.meta?.item?.id || '';
|
|
7221
7722
|
const evalResult = managedSpawn.evaluateManagedSpawnAcceptance(agentId, {
|
|
@@ -7265,7 +7766,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
7265
7766
|
parsedSnippet ? '## What you wrote\n\n```json\n' + parsedSnippet + '\n```\n' : '',
|
|
7266
7767
|
'## Canonical shape',
|
|
7267
7768
|
'',
|
|
7268
|
-
canonicalHint || '(see `engine/managed-spawn.js` `buildManagedSpawnHint` for the canonical shape.)',
|
|
7769
|
+
canonicalHint || '(see `engine/processes/managed-spawn.js` `buildManagedSpawnHint` for the canonical shape.)',
|
|
7269
7770
|
'',
|
|
7270
7771
|
].filter(Boolean).join('\n');
|
|
7271
7772
|
writeInboxAlert(`managed-spawn-${agentId}`, alertBody);
|
|
@@ -7299,7 +7800,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
7299
7800
|
const qaSessionMeta = dispatchItem.meta?.item?.meta;
|
|
7300
7801
|
if (!spawnFailureReason
|
|
7301
7802
|
&& qaSessionMeta?.sessionId) {
|
|
7302
|
-
const qaSessions = require('./engine/qa
|
|
7803
|
+
const qaSessions = require('./engine/qa/sessions');
|
|
7303
7804
|
if (qaSessionMeta.sessionPhase === qaSessions.SESSION_PHASE.SETUP) {
|
|
7304
7805
|
const qaSession = qaSessions.getSession(qaSessionMeta.sessionId);
|
|
7305
7806
|
if (!qaSession || qaSessions.TERMINAL_STATES.has(qaSession.state)) {
|
|
@@ -7410,7 +7911,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
7410
7911
|
};
|
|
7411
7912
|
}
|
|
7412
7913
|
} else if (managedSpawnRequested) {
|
|
7413
|
-
const managedSpawn = require('./engine/managed-spawn');
|
|
7914
|
+
const managedSpawn = require('./engine/processes/managed-spawn');
|
|
7414
7915
|
const staleSidecar = path.join(AGENTS_DIR, agentId, managedSpawn.MANAGED_SPAWN_FILENAME);
|
|
7415
7916
|
try { fs.unlinkSync(staleSidecar); } catch (e) {
|
|
7416
7917
|
if (e.code !== 'ENOENT') log('warn', `managed-spawn: failed to discard untrusted sidecar for ${agentId}: ${e.message}`);
|
|
@@ -7431,7 +7932,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
7431
7932
|
let managedSpawnHealthcheckFailure = null;
|
|
7432
7933
|
if (managedSpawnSpawned.length > 0) {
|
|
7433
7934
|
try {
|
|
7434
|
-
const managedSpawn = require('./engine/managed-spawn');
|
|
7935
|
+
const managedSpawn = require('./engine/processes/managed-spawn');
|
|
7435
7936
|
const items = managedSpawnSpawned;
|
|
7436
7937
|
// Re-read the specs from the state file (recordManagedBatch normalised
|
|
7437
7938
|
// them — healthcheck shape lives there now).
|
|
@@ -7595,6 +8096,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
7595
8096
|
});
|
|
7596
8097
|
const finalCompletionReportPath = structuredCompletion?._path || dispatchItem.meta?.completionReportPath || shared.dispatchCompletionReportPath(id);
|
|
7597
8098
|
const completionOpts = {
|
|
8099
|
+
completionPath: closeCompletionPath,
|
|
7598
8100
|
...(finalCompletionReportPath ? { completionReportPath: finalCompletionReportPath } : {}),
|
|
7599
8101
|
...(structuredCompletion ? { structuredCompletion } : {}),
|
|
7600
8102
|
};
|
|
@@ -7696,7 +8198,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
7696
8198
|
let processAnchorReason = null;
|
|
7697
8199
|
if (worktreePath) {
|
|
7698
8200
|
try {
|
|
7699
|
-
const anchorDecision = require('./engine/
|
|
8201
|
+
const anchorDecision = require('./engine/worktrees/gc').shouldGcDispatchWorktree({
|
|
7700
8202
|
worktreePath,
|
|
7701
8203
|
agentId,
|
|
7702
8204
|
managedSpawnSpawnedCount: managedSpawnAliveCount,
|
|
@@ -7792,7 +8294,8 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
7792
8294
|
await shared.shellSafeGit(['reset', '--hard', 'HEAD'], { ..._gitOpts, cwd: worktreePath, timeout: 30000 });
|
|
7793
8295
|
// -fd preserves gitignored files (node_modules, .vite, .next caches) — that's the whole point.
|
|
7794
8296
|
await shared.shellSafeGit(['clean', '-fd'], { ..._gitOpts, cwd: worktreePath, timeout: 30000 });
|
|
7795
|
-
await shared.
|
|
8297
|
+
await shared.withSerializedRemoteRefRefresh(rootDir, () =>
|
|
8298
|
+
shared.shellSafeGit(['fetch', 'origin', _mainRefRet], { ..._gitOpts, cwd: rootDir, timeout: 30000 }));
|
|
7796
8299
|
// Detach at origin/<main> — local main is typically checked out in
|
|
7797
8300
|
// the project root and git refuses two checkouts of the same branch.
|
|
7798
8301
|
await shared.shellSafeGit(['checkout', '--detach', `origin/${_mainRefRet}`], { ..._gitOpts, cwd: worktreePath, timeout: 30000 });
|
|
@@ -7842,7 +8345,7 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
7842
8345
|
// at …` until the 2-hour age sweep in cleanup.js eventually catches up.
|
|
7843
8346
|
if (worktreePath && rootDir && failedWorktreeProtection.allowRemoval !== false) {
|
|
7844
8347
|
try {
|
|
7845
|
-
const _wgc = require('./engine/
|
|
8348
|
+
const _wgc = require('./engine/worktrees/gc');
|
|
7846
8349
|
const _wtRoot = path.resolve(rootDir, engineConfig.worktreeRoot || ENGINE_DEFAULTS.worktreeRoot);
|
|
7847
8350
|
const _gcResult = _wgc.gcDispatchWorktreeIfOrphan({
|
|
7848
8351
|
worktreePath,
|
|
@@ -7877,19 +8380,19 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
7877
8380
|
// ── P-d9e6b2c4 — live-mode dispatch-end auto-restore + terminal-failure
|
|
7878
8381
|
// notify. Runs on EVERY terminal result for a live-mode dispatch: the
|
|
7879
8382
|
// agent ran in-place in the operator's checkout, so switch the tree back
|
|
7880
|
-
// to the ref it was on before (see engine/live-checkout.js#
|
|
8383
|
+
// to the ref it was on before (see engine/worktrees/live-checkout.js#
|
|
7881
8384
|
// restoreLiveCheckoutAtDispatchEnd). The worktree-GC block above no-ops
|
|
7882
8385
|
// in live mode (worktreePath===null); this is its live-mode counterpart.
|
|
7883
8386
|
// originalRef was captured by prepareLiveCheckout and persisted on the
|
|
7884
8387
|
// dispatch record at branch-resolution time (P-c5a1f3b8); the in-memory
|
|
7885
8388
|
// closure copy is authoritative here. The engine-restart reattach path
|
|
7886
|
-
// fires the same helper from the persisted record in engine/cli.js. The
|
|
8389
|
+
// fires the same helper from the persisted record in engine/operations/cli.js. The
|
|
7887
8390
|
// restore is a plain `git checkout <originalRef>` — never --force/reset/
|
|
7888
8391
|
// clean/stash — and is best-effort: a thrown restore never alters the
|
|
7889
8392
|
// dispatch result.
|
|
7890
8393
|
if (liveMode && branchName && shared.isLiveCheckoutDispatchRecord(dispatchItem, getConfig())) {
|
|
7891
8394
|
try {
|
|
7892
|
-
const _liveRestore = require('./engine/live-checkout');
|
|
8395
|
+
const _liveRestore = require('./engine/worktrees/live-checkout');
|
|
7893
8396
|
await _liveRestore.restoreLiveCheckoutAtDispatchEnd({
|
|
7894
8397
|
localPath: cwd,
|
|
7895
8398
|
branchName,
|
|
@@ -8003,14 +8506,52 @@ async function spawnAgent(dispatchItem, config, spawnAttemptLease = null) {
|
|
|
8003
8506
|
}
|
|
8004
8507
|
}
|
|
8005
8508
|
|
|
8006
|
-
|
|
8509
|
+
function finalizeAgentClose(code) {
|
|
8510
|
+
onAgentClose(code).catch(err => {
|
|
8511
|
+
const detail = err instanceof Error ? err.stack || err.message : String(err);
|
|
8512
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
8513
|
+
try {
|
|
8514
|
+
log('error', `Agent ${agentId} (${id}) close finalization failed: ${detail}`);
|
|
8515
|
+
activeProcesses.delete(id);
|
|
8516
|
+
realActivityMap.delete(id);
|
|
8517
|
+
const dispatchNow = getDispatch();
|
|
8518
|
+
const stillQueued = [...(dispatchNow.active || []), ...(dispatchNow.pending || [])]
|
|
8519
|
+
.some(item => item.id === id);
|
|
8520
|
+
if (stillQueued) {
|
|
8521
|
+
completeDispatch(
|
|
8522
|
+
id,
|
|
8523
|
+
DISPATCH_RESULT.ERROR,
|
|
8524
|
+
`Post-completion lifecycle error: ${reason}`,
|
|
8525
|
+
'',
|
|
8526
|
+
{ failureClass: FAILURE_CLASS.UNKNOWN, agentRetryable: true },
|
|
8527
|
+
);
|
|
8528
|
+
}
|
|
8529
|
+
} catch (recoveryErr) {
|
|
8530
|
+
console.error(
|
|
8531
|
+
`[engine] Failed to contain close finalization error for ${id}: ` +
|
|
8532
|
+
`${recoveryErr?.stack || recoveryErr}`,
|
|
8533
|
+
);
|
|
8534
|
+
} finally {
|
|
8535
|
+
try { cleanupTempAgent(agentId); } catch { /* best-effort recovery */ }
|
|
8536
|
+
}
|
|
8537
|
+
});
|
|
8538
|
+
}
|
|
8539
|
+
|
|
8540
|
+
proc.on('close', finalizeAgentClose);
|
|
8007
8541
|
|
|
8008
8542
|
proc.on('error', (err) => {
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
8543
|
+
try {
|
|
8544
|
+
log('error', `Failed to spawn agent ${agentId}: ${err.message}`);
|
|
8545
|
+
activeProcesses.delete(id);
|
|
8546
|
+
realActivityMap.delete(id);
|
|
8547
|
+
completeDispatch(id, DISPATCH_RESULT.ERROR, `Spawn error: ${err.message}`);
|
|
8548
|
+
} catch (handlerErr) {
|
|
8549
|
+
console.error(
|
|
8550
|
+
`[engine] Failed to contain spawn error for ${id}: ${handlerErr?.stack || handlerErr}`,
|
|
8551
|
+
);
|
|
8552
|
+
} finally {
|
|
8553
|
+
try { cleanupTempAgent(agentId); } catch { /* best-effort recovery */ }
|
|
8554
|
+
}
|
|
8014
8555
|
});
|
|
8015
8556
|
|
|
8016
8557
|
// Safety: if process exits immediately (within 3s), log it
|
|
@@ -8097,8 +8638,6 @@ function areDependenciesMet(item, config) {
|
|
|
8097
8638
|
const deps = item.depends_on;
|
|
8098
8639
|
if (!deps || deps.length === 0) return true;
|
|
8099
8640
|
const sourcePlan = item.sourcePlan;
|
|
8100
|
-
if (!sourcePlan) return true;
|
|
8101
|
-
const projects = getProjects(config);
|
|
8102
8641
|
|
|
8103
8642
|
// Collect work items from ALL projects (dependencies can be cross-project)
|
|
8104
8643
|
const allWorkItems = queries.getWorkItems(config);
|
|
@@ -8109,12 +8648,14 @@ function areDependenciesMet(item, config) {
|
|
|
8109
8648
|
const depItem = allWorkItems.find(w => w.id === depId);
|
|
8110
8649
|
if (!depItem) {
|
|
8111
8650
|
// Fallback: check the PRD store — plan-to-prd agents may pre-set items to done.
|
|
8112
|
-
|
|
8113
|
-
|
|
8114
|
-
|
|
8115
|
-
|
|
8116
|
-
|
|
8117
|
-
|
|
8651
|
+
if (sourcePlan) {
|
|
8652
|
+
try {
|
|
8653
|
+
const plan = prdStore.readPrd(sourcePlan);
|
|
8654
|
+
const prdItem = (plan?.missing_features || []).find(f => f.id === depId);
|
|
8655
|
+
if (prdItem && PRD_MET_STATUSES.has(prdItem.status)) continue; // PRD says done — treat as met
|
|
8656
|
+
} catch (e) { log('warn', 'check PRD dep status: ' + e.message); }
|
|
8657
|
+
}
|
|
8658
|
+
log('warn', `Dependency ${depId} not found for ${item.id} (plan: ${sourcePlan || 'none'}) — treating as unmet`);
|
|
8118
8659
|
return false;
|
|
8119
8660
|
}
|
|
8120
8661
|
if (depItem.status === WI_STATUS.FAILED) return 'failed';
|
|
@@ -8227,12 +8768,12 @@ function reconcileItemsWithPrs(items, allPrs, { onlyIds } = {}) {
|
|
|
8227
8768
|
return reconciled;
|
|
8228
8769
|
}
|
|
8229
8770
|
|
|
8230
|
-
// ─── Inbox Consolidation (extracted to engine/consolidation.js) ──────────────
|
|
8771
|
+
// ─── Inbox Consolidation (extracted to engine/memory/consolidation.js) ──────────────
|
|
8231
8772
|
|
|
8232
|
-
const { consolidateInbox } = require('./engine/consolidation');
|
|
8773
|
+
const { consolidateInbox } = require('./engine/memory/consolidation');
|
|
8233
8774
|
const { pollPrStatus, pollPrHumanComments, reconcilePrs, checkLiveReviewStatus: adoCheckLiveReview, checkLiveBuildAndConflict: adoCheckLiveBuildAndConflict, needsAdoPollRetry, getAdoToken, isAdoThrottled, getAdoThrottleState } = require('./engine/ado');
|
|
8234
|
-
const { pollPrStatus: ghPollPrStatus, pollPrHumanComments: ghPollPrHumanComments, reconcilePrs: ghReconcilePrs, checkLiveReviewStatus: ghCheckLiveReview, checkLiveBuildAndConflict: ghCheckLiveBuildAndConflict, isGhThrottled } = require('./engine/github');
|
|
8235
|
-
const { reconcileSharedBranchPrs } = require('./engine/shared-branch-pr-reconcile');
|
|
8775
|
+
const { pollPrStatus: ghPollPrStatus, pollPrHumanComments: ghPollPrHumanComments, reconcilePrs: ghReconcilePrs, checkLiveReviewStatus: ghCheckLiveReview, checkLiveBuildAndConflict: ghCheckLiveBuildAndConflict, isGhThrottled } = require('./engine/providers/github');
|
|
8776
|
+
const { reconcileSharedBranchPrs } = require('./engine/providers/shared-branch-pr-reconcile');
|
|
8236
8777
|
|
|
8237
8778
|
// ─── State Snapshot ─────────────────────────────────────────────────────────
|
|
8238
8779
|
|
|
@@ -8273,12 +8814,12 @@ function updateSnapshot(config) {
|
|
|
8273
8814
|
safeWrite(path.join(IDENTITY_DIR, 'now.md'), snapshot);
|
|
8274
8815
|
}
|
|
8275
8816
|
|
|
8276
|
-
// ─── Cooldowns (extracted to engine/cooldown.js) ─────────────────────────────
|
|
8817
|
+
// ─── Cooldowns (extracted to engine/orchestration/cooldown.js) ─────────────────────────────
|
|
8277
8818
|
|
|
8278
8819
|
const { dispatchCooldowns, loadCooldowns, saveCooldowns,
|
|
8279
8820
|
isOnCooldown, setCooldown, setCooldownWithContext, drainCoalescedContexts,
|
|
8280
8821
|
setCooldownFailure, clearCooldown, getPrReviewCooldownKey, clearLegacyPrReviewCooldown,
|
|
8281
|
-
isAlreadyDispatched, isBranchActive } = require('./engine/cooldown');
|
|
8822
|
+
isAlreadyDispatched, isBranchActive } = require('./engine/orchestration/cooldown');
|
|
8282
8823
|
|
|
8283
8824
|
// Auto-clean pending/failed work items for a PRD so they re-materialize with updated plan data
|
|
8284
8825
|
function autoCleanPrdWorkItems(prdFile, config) {
|
|
@@ -8845,7 +9386,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
8845
9386
|
}
|
|
8846
9387
|
}
|
|
8847
9388
|
|
|
8848
|
-
// buildBaseVars, selectPlaybook, buildPrDispatch extracted to engine/playbook.js
|
|
9389
|
+
// buildBaseVars, selectPlaybook, buildPrDispatch extracted to engine/agents/playbook.js
|
|
8849
9390
|
|
|
8850
9391
|
function clearPendingHumanFeedbackFlag(projectMeta, prId) {
|
|
8851
9392
|
if (!prId) return;
|
|
@@ -8865,7 +9406,7 @@ function isPrNoOpFixCauseSuppressed(pr, cause) {
|
|
|
8865
9406
|
}
|
|
8866
9407
|
|
|
8867
9408
|
// #639 — distinct suppression check for the build-fix-ineffective pause
|
|
8868
|
-
// (see shared.isBuildFixIneffectivePaused / engine/lifecycle.js
|
|
9409
|
+
// (see shared.isBuildFixIneffectivePaused / engine/orchestration/lifecycle.js
|
|
8869
9410
|
// recordBuildFixIneffective). Kept separate from isPrNoOpFixCauseSuppressed
|
|
8870
9411
|
// so the log line is unambiguous about WHY build-fix automation stopped:
|
|
8871
9412
|
// repeated attempts genuinely found nothing to change vs. repeated attempts
|
|
@@ -9341,8 +9882,8 @@ async function discoverFromPrs(config, project) {
|
|
|
9341
9882
|
if (pr.status !== PR_STATUS.ACTIVE) continue;
|
|
9342
9883
|
// Issue #384: skip tombstoned PRs (user explicitly deleted via dashboard)
|
|
9343
9884
|
if (pr.userDeleted === true) continue;
|
|
9344
|
-
if (shared.
|
|
9345
|
-
_logPrDispatchSkipOnce(pr, '
|
|
9885
|
+
if (!shared.isAutoManagedPrRecord(pr)) {
|
|
9886
|
+
_logPrDispatchSkipOnce(pr, 'not-auto-managed');
|
|
9346
9887
|
continue;
|
|
9347
9888
|
}
|
|
9348
9889
|
if (!shared.isPrCompatibleWithProject(project, pr, pr.url || '')) continue;
|
|
@@ -9368,13 +9909,6 @@ async function discoverFromPrs(config, project) {
|
|
|
9368
9909
|
log('info', `Branch mutex: skipping PR ${pr.id} dispatch — branch ${prBranchForMutex} locked by another agent`);
|
|
9369
9910
|
continue;
|
|
9370
9911
|
}
|
|
9371
|
-
// Auto-managed gate: single source of truth in engine/shared.js. Canonical
|
|
9372
|
-
// `contextOnly` wins over stale legacy flags; legacy `_contextOnly` is only
|
|
9373
|
-
// a fallback for raw pre-migration records.
|
|
9374
|
-
if (!shared.isAutoManagedPrRecord(pr)) {
|
|
9375
|
-
_logPrDispatchSkipOnce(pr, 'not-auto-managed');
|
|
9376
|
-
continue;
|
|
9377
|
-
}
|
|
9378
9912
|
// #764 — PR-level circuit breaker: a prior dispatch against this PR's
|
|
9379
9913
|
// branch hit WORKTREE_PREFLIGHT because the branch is held externally
|
|
9380
9914
|
// (project root or an unreachable sibling worktree). Every dispatch path
|
|
@@ -9389,6 +9923,8 @@ async function discoverFromPrs(config, project) {
|
|
|
9389
9923
|
// Use reviewStatus as single source of truth (synced from ADO/GitHub votes)
|
|
9390
9924
|
// minionsReview tracks metadata (reviewer, note) but not the authoritative status
|
|
9391
9925
|
const reviewStatus = pr.reviewStatus || REVIEW_STATUS.PENDING;
|
|
9926
|
+
const prAutoFixEnabled = shared.isPrAutoFixEnabled(pr);
|
|
9927
|
+
const prAutoReviewEnabled = shared.isPrAutoReviewEnabled(pr);
|
|
9392
9928
|
|
|
9393
9929
|
// Skip fix dispatch if a fix was recently submitted and awaiting re-review.
|
|
9394
9930
|
// The poller holds reviewStatus at 'waiting' until the reviewer acts on the new code.
|
|
@@ -9402,8 +9938,8 @@ async function discoverFromPrs(config, project) {
|
|
|
9402
9938
|
let fixDispatched = false;
|
|
9403
9939
|
|
|
9404
9940
|
// PRs needing review: evalLoop gates the entire review+fix cycle; pollEnabled ensures reviewStatus is fresh
|
|
9405
|
-
const reviewEnabled = evalLoopEnabled && pollEnabled && autoReviewPrs;
|
|
9406
|
-
const reReviewEnabled = evalLoopEnabled && pollEnabled && autoReReviewPrs;
|
|
9941
|
+
const reviewEnabled = prAutoReviewEnabled && evalLoopEnabled && pollEnabled && autoReviewPrs;
|
|
9942
|
+
const reReviewEnabled = prAutoReviewEnabled && evalLoopEnabled && pollEnabled && autoReReviewPrs;
|
|
9407
9943
|
const alreadyReviewed = isPrReviewCurrent(pr);
|
|
9408
9944
|
const needsReview = reviewEnabled && reviewStatus === REVIEW_STATUS.PENDING && !alreadyReviewed;
|
|
9409
9945
|
if (needsReview) {
|
|
@@ -9473,7 +10009,7 @@ async function discoverFromPrs(config, project) {
|
|
|
9473
10009
|
const humanCauseKey = getPrAutomationCauseKey('human-comment', pr);
|
|
9474
10010
|
const humanFixKey = getPrAutomationDispatchKey(humanFixBaseKey, humanCauseKey);
|
|
9475
10011
|
const hasCoalescedFeedback = (dispatchCooldowns.get(humanFixKey)?.pendingContexts || []).length > 0;
|
|
9476
|
-
if (pollEnabled && autoFixHumanComments && (pr.humanFeedback?.pendingFix || hasCoalescedFeedback) && !fixDispatched
|
|
10012
|
+
if (prAutoFixEnabled && pollEnabled && autoFixHumanComments && (pr.humanFeedback?.pendingFix || hasCoalescedFeedback) && !fixDispatched
|
|
9477
10013
|
&& !isPrNoOpFixCauseSuppressed(pr, shared.PR_FIX_CAUSE.HUMAN_FEEDBACK)) {
|
|
9478
10014
|
// W-mp3bp0ha000997ab-d: skip when the most recent HUMAN-FEEDBACK dispatch
|
|
9479
10015
|
// already noop'd against the same head SHA AND the same triggering
|
|
@@ -9703,7 +10239,7 @@ async function discoverFromPrs(config, project) {
|
|
|
9703
10239
|
|
|
9704
10240
|
// PRs with changes requested → route back to author for fix.
|
|
9705
10241
|
// Gate on evalLoopEnabled and provider polling — the review→fix cycle depends on fresh vote state.
|
|
9706
|
-
if (evalLoopEnabled && pollEnabled && autoFixReviewFeedback && reviewStatus === REVIEW_STATUS.CHANGES_REQUESTED && !awaitingReReview && !fixDispatched
|
|
10242
|
+
if (prAutoFixEnabled && evalLoopEnabled && pollEnabled && autoFixReviewFeedback && reviewStatus === REVIEW_STATUS.CHANGES_REQUESTED && !awaitingReReview && !fixDispatched
|
|
9707
10243
|
&& !isPrNoOpFixCauseSuppressed(pr, shared.PR_FIX_CAUSE.REVIEW_FEEDBACK)) {
|
|
9708
10244
|
const reviewCauseKey = getPrAutomationCauseKey('review-feedback', pr);
|
|
9709
10245
|
const key = getPrAutomationDispatchKey(`fix-${project?.name || 'default'}-${prDisplayId}`, reviewCauseKey);
|
|
@@ -9759,7 +10295,7 @@ async function discoverFromPrs(config, project) {
|
|
|
9759
10295
|
const gracePeriodMs = config.engine?.buildFixGracePeriod ?? ENGINE_DEFAULTS.buildFixGracePeriod;
|
|
9760
10296
|
if (Date.now() - new Date(pr._buildFixPushedAt).getTime() < gracePeriodMs) continue;
|
|
9761
10297
|
}
|
|
9762
|
-
if (pollEnabled && autoFixBuilds && pr.status === PR_STATUS.ACTIVE && pr.buildStatus === 'failing'
|
|
10298
|
+
if (prAutoFixEnabled && pollEnabled && autoFixBuilds && pr.status === PR_STATUS.ACTIVE && pr.buildStatus === 'failing'
|
|
9763
10299
|
&& !fixDispatched
|
|
9764
10300
|
&& !isPrNoOpFixCauseSuppressed(pr, shared.PR_FIX_CAUSE.BUILD_FAILURE)
|
|
9765
10301
|
&& !isBuildFixIneffectiveSuppressed(pr)) {
|
|
@@ -9923,7 +10459,7 @@ async function discoverFromPrs(config, project) {
|
|
|
9923
10459
|
}
|
|
9924
10460
|
|
|
9925
10461
|
// PRs with merge conflicts — dispatch fix to resolve (gated by provider polling + autoFixConflicts)
|
|
9926
|
-
if (pollEnabled && autoFixConflicts && pr.status === PR_STATUS.ACTIVE && pr._mergeConflict && !fixDispatched
|
|
10462
|
+
if (prAutoFixEnabled && pollEnabled && autoFixConflicts && pr.status === PR_STATUS.ACTIVE && pr._mergeConflict && !fixDispatched
|
|
9927
10463
|
&& !isPrNoOpFixCauseSuppressed(pr, shared.PR_FIX_CAUSE.MERGE_CONFLICT)) {
|
|
9928
10464
|
// W-mpritzcr0004afc5 (#2955): "don't fan out a parallel conflict-fix
|
|
9929
10465
|
// while the review-feedback path owns the PR" is already handled by
|
|
@@ -10022,12 +10558,188 @@ async function discoverFromPrs(config, project) {
|
|
|
10022
10558
|
return newWork;
|
|
10023
10559
|
}
|
|
10024
10560
|
|
|
10561
|
+
/**
|
|
10562
|
+
* Hard cap on the co-service rows a QA-session prompt can carry. Sessions are
|
|
10563
|
+
* already bounded at `qaSessions.LIMITS.projectsMax` (5); this is the
|
|
10564
|
+
* independent render-side bound so a hand-edited session record can never
|
|
10565
|
+
* inflate a dispatch prompt.
|
|
10566
|
+
*/
|
|
10567
|
+
const QA_SESSION_SERVICES_MAX = 8;
|
|
10568
|
+
|
|
10569
|
+
/**
|
|
10570
|
+
* W-msb9kgs402813a97-a — the live co-service inventory for a QA Session.
|
|
10571
|
+
*
|
|
10572
|
+
* A multi-project session owns ONE managed-spawn per project
|
|
10573
|
+
* (`qaSessions.managedSpawnNamesForSession`), but DRAFT/EXECUTE only ever saw
|
|
10574
|
+
* the PRIMARY spawn — so a co-service origin was unaddressable from the test
|
|
10575
|
+
* the agent drafted. This builds a bounded
|
|
10576
|
+
* `{ name, project, primary, health, baseUrl, ports }` row per spawn, which is
|
|
10577
|
+
* surfaced to the playbooks as `{{session_services_json}}` and to runner
|
|
10578
|
+
* adapters as `briefOpts.services`.
|
|
10579
|
+
*
|
|
10580
|
+
* `health` is one of `healthy` | `unhealthy` (alive but failing its
|
|
10581
|
+
* healthcheck) | `down` (registered but not alive) | `missing` (no spec
|
|
10582
|
+
* registered yet — the normal SETUP-phase state). Never throws: an unreadable
|
|
10583
|
+
* managed-process store degrades to `missing` rows, because a render must not
|
|
10584
|
+
* fail over service discovery.
|
|
10585
|
+
*
|
|
10586
|
+
* Exported for testing.
|
|
10587
|
+
*/
|
|
10588
|
+
function buildQaSessionServices(session) {
|
|
10589
|
+
if (!session || typeof session !== 'object') return [];
|
|
10590
|
+
let names = [];
|
|
10591
|
+
try {
|
|
10592
|
+
names = require('./engine/qa/sessions').managedSpawnNamesForSession(session);
|
|
10593
|
+
} catch (err) {
|
|
10594
|
+
log('warn', `qa-session render: managedSpawnNamesForSession failed: ${err.message}`);
|
|
10595
|
+
return [];
|
|
10596
|
+
}
|
|
10597
|
+
if (!Array.isArray(names) || names.length === 0) return [];
|
|
10598
|
+
|
|
10599
|
+
const specProjects = Array.isArray(session.spec && session.spec.projects)
|
|
10600
|
+
? session.spec.projects.filter(p => typeof p === 'string' && p)
|
|
10601
|
+
: [];
|
|
10602
|
+
const primaryProject = session.primaryProject
|
|
10603
|
+
|| specProjects[0]
|
|
10604
|
+
|| (session.spec && session.spec.project)
|
|
10605
|
+
|| null;
|
|
10606
|
+
|
|
10607
|
+
const byName = new Map();
|
|
10608
|
+
try {
|
|
10609
|
+
for (const spec of require('./engine/processes/managed-spawn').listManagedSpecs()) {
|
|
10610
|
+
if (spec && typeof spec.name === 'string') byName.set(spec.name, spec);
|
|
10611
|
+
}
|
|
10612
|
+
} catch (err) {
|
|
10613
|
+
log('warn', `qa-session render: managed-spawn inventory unavailable: ${err.message}`);
|
|
10614
|
+
}
|
|
10615
|
+
|
|
10616
|
+
return names.slice(0, QA_SESSION_SERVICES_MAX).map((name, index) => {
|
|
10617
|
+
// managedSpawnNamesForSession maps spec.projects positionally, so index
|
|
10618
|
+
// alignment is its contract; single/central sessions collapse to the one
|
|
10619
|
+
// primary row.
|
|
10620
|
+
const project = specProjects.length > 1
|
|
10621
|
+
? (specProjects[index] || null)
|
|
10622
|
+
: (primaryProject || null);
|
|
10623
|
+
const record = byName.get(name) || null;
|
|
10624
|
+
let health = 'missing';
|
|
10625
|
+
if (record) {
|
|
10626
|
+
if (record.healthy === true) health = 'healthy';
|
|
10627
|
+
else if (record.alive === true) health = 'unhealthy';
|
|
10628
|
+
else health = 'down';
|
|
10629
|
+
}
|
|
10630
|
+
return {
|
|
10631
|
+
name,
|
|
10632
|
+
project,
|
|
10633
|
+
primary: !project || project === primaryProject,
|
|
10634
|
+
health,
|
|
10635
|
+
baseUrl: (record && record.attrs && typeof record.attrs.base_url === 'string')
|
|
10636
|
+
? record.attrs.base_url
|
|
10637
|
+
: '',
|
|
10638
|
+
ports: (record && Array.isArray(record.ports)) ? record.ports.slice() : [],
|
|
10639
|
+
};
|
|
10640
|
+
});
|
|
10641
|
+
}
|
|
10642
|
+
|
|
10643
|
+
/**
|
|
10644
|
+
* Render-time wrapper: resolve the session behind a QA work item and encode its
|
|
10645
|
+
* service inventory. Returns '' for non-QA items and for any lookup failure, so
|
|
10646
|
+
* `{{session_services_json}}` degrades to the PLAYBOOK_OPTIONAL_VARS empty
|
|
10647
|
+
* string rather than breaking the render.
|
|
10648
|
+
*/
|
|
10649
|
+
function _qaSessionServicesJson(item) {
|
|
10650
|
+
const meta = item && item.meta;
|
|
10651
|
+
if (!meta || !meta.sessionId) return '';
|
|
10652
|
+
try {
|
|
10653
|
+
const session = require('./engine/qa/sessions').getSession(meta.sessionId);
|
|
10654
|
+
if (!session) return '';
|
|
10655
|
+
const services = buildQaSessionServices(session);
|
|
10656
|
+
return services.length ? JSON.stringify(services) : '';
|
|
10657
|
+
} catch (err) {
|
|
10658
|
+
log('warn', `qa-session render: session services lookup failed for ${meta.sessionId}: ${err.message}`);
|
|
10659
|
+
return '';
|
|
10660
|
+
}
|
|
10661
|
+
}
|
|
10662
|
+
|
|
10663
|
+
/**
|
|
10664
|
+
* Independent render-side cap on the manifest journey rows an EXECUTE prompt
|
|
10665
|
+
* can carry. The DRAFT gate already bounds an accepted manifest at
|
|
10666
|
+
* `visualJourney.LIMITS.journeysMax`, but a hand-edited session record must
|
|
10667
|
+
* never be able to inflate a dispatch prompt.
|
|
10668
|
+
*/
|
|
10669
|
+
const QA_SESSION_JOURNEYS_MAX = 20;
|
|
10670
|
+
|
|
10671
|
+
/**
|
|
10672
|
+
* W-msb9kgs402813a97-c — the DRAFT-accepted visual-journey manifest, projected
|
|
10673
|
+
* into the exact shape EXECUTE has to report back.
|
|
10674
|
+
*
|
|
10675
|
+
* `visualJourney.validateEvidenceCoverage` check (c) requires every journey the
|
|
10676
|
+
* manifest declared to come back as a PASSED `journeyCoverage` entry keyed by
|
|
10677
|
+
* the journey id VERBATIM. Those ids are free-form strings the DRAFT agent
|
|
10678
|
+
* invents (`playbooks/qa-session-draft.md` calls them "short-stable-id"), the
|
|
10679
|
+
* DRAFT sidecar that carried them is read-then-unlinked, and the accepted
|
|
10680
|
+
* manifest is persisted on the session record only. So without this projection
|
|
10681
|
+
* the EXECUTE agent is graded against a value it was never shown, and every
|
|
10682
|
+
* session that requested screenshots or video fails
|
|
10683
|
+
* `qa-session-evidence-incomplete` on a flawless run.
|
|
10684
|
+
*
|
|
10685
|
+
* Field names deliberately mirror the sidecar's coverage entry (`journeyId`,
|
|
10686
|
+
* `projects`, `services`) rather than the manifest's own (`id`, …), so the
|
|
10687
|
+
* contract the agent is handed is copy-shaped for the contract it must emit.
|
|
10688
|
+
*
|
|
10689
|
+
* Pure and never throws — exported for testing.
|
|
10690
|
+
*/
|
|
10691
|
+
function buildQaExecuteJourneyContract(session) {
|
|
10692
|
+
if (!session || typeof session !== 'object') return [];
|
|
10693
|
+
const manifest = session.visualJourneyManifest;
|
|
10694
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) return [];
|
|
10695
|
+
if (!Array.isArray(manifest.journeys)) return [];
|
|
10696
|
+
const boundedList = (value) => (Array.isArray(value)
|
|
10697
|
+
? value.filter(v => typeof v === 'string' && v).slice(0, QA_SESSION_SERVICES_MAX)
|
|
10698
|
+
: []);
|
|
10699
|
+
return manifest.journeys
|
|
10700
|
+
.filter(j => j && typeof j === 'object' && typeof j.id === 'string' && j.id)
|
|
10701
|
+
.slice(0, QA_SESSION_JOURNEYS_MAX)
|
|
10702
|
+
.map(j => ({
|
|
10703
|
+
journeyId: j.id,
|
|
10704
|
+
name: typeof j.name === 'string' ? j.name : '',
|
|
10705
|
+
kind: typeof j.kind === 'string' ? j.kind : '',
|
|
10706
|
+
projects: boundedList(j.projects),
|
|
10707
|
+
services: boundedList(j.services),
|
|
10708
|
+
}));
|
|
10709
|
+
}
|
|
10710
|
+
|
|
10711
|
+
/**
|
|
10712
|
+
* Render-time wrapper for `{{visual_journey_manifest_json}}`.
|
|
10713
|
+
*
|
|
10714
|
+
* EXECUTE-only on purpose: the manifest does not exist until DRAFT produces it,
|
|
10715
|
+
* and on the re-draft path (`editDraft` re-queues DRAFT for the same session)
|
|
10716
|
+
* showing the PREVIOUS attempt's manifest to the DRAFT agent would invite it to
|
|
10717
|
+
* copy ids it is supposed to be re-deciding.
|
|
10718
|
+
*
|
|
10719
|
+
* Returns '' for non-QA items, non-EXECUTE phases, logs-only/legacy sessions
|
|
10720
|
+
* with no persisted manifest, and any lookup failure, so the var degrades to
|
|
10721
|
+
* the PLAYBOOK_OPTIONAL_VARS empty string rather than breaking the render.
|
|
10722
|
+
*/
|
|
10723
|
+
function _qaVisualJourneyManifestJson(item) {
|
|
10724
|
+
const meta = item && item.meta;
|
|
10725
|
+
if (!meta || !meta.sessionId || meta.sessionPhase !== 'execute') return '';
|
|
10726
|
+
try {
|
|
10727
|
+
const session = require('./engine/qa/sessions').getSession(meta.sessionId);
|
|
10728
|
+
if (!session) return '';
|
|
10729
|
+
const journeys = buildQaExecuteJourneyContract(session);
|
|
10730
|
+
return journeys.length ? JSON.stringify(journeys, null, 2) : '';
|
|
10731
|
+
} catch (err) {
|
|
10732
|
+
log('warn', `qa-session render: visual-journey manifest lookup failed for ${meta.sessionId}: ${err.message}`);
|
|
10733
|
+
return '';
|
|
10734
|
+
}
|
|
10735
|
+
}
|
|
10736
|
+
|
|
10025
10737
|
/**
|
|
10026
10738
|
* P-f9a2e1b4 — Compute runner_brief / runner_execute_brief / test_file for
|
|
10027
10739
|
* QA Session DRAFT and EXECUTE dispatches.
|
|
10028
10740
|
*
|
|
10029
|
-
* Lazy-requires `./engine/qa
|
|
10030
|
-
* `./engine/managed-spawn` so non-QA dispatches don't pay the load cost
|
|
10741
|
+
* Lazy-requires `./engine/qa/sessions`, `./engine/qa-runners`, and
|
|
10742
|
+
* `./engine/processes/managed-spawn` so non-QA dispatches don't pay the load cost
|
|
10031
10743
|
* and so test isolation (createTestMinionsDir → ISOLATED_MODULES) gets a
|
|
10032
10744
|
* fresh module instance per test.
|
|
10033
10745
|
*
|
|
@@ -10049,9 +10761,9 @@ function _buildRunnerBriefVars(item, project, qaRunPaths = {}) {
|
|
|
10049
10761
|
const phase = meta.sessionPhase;
|
|
10050
10762
|
if (phase !== 'draft' && phase !== 'execute') return empty;
|
|
10051
10763
|
try {
|
|
10052
|
-
const qaSessions = require('./engine/qa
|
|
10764
|
+
const qaSessions = require('./engine/qa/sessions');
|
|
10053
10765
|
const qaRunners = require('./engine/qa-runners');
|
|
10054
|
-
const managedSpawn = require('./engine/managed-spawn');
|
|
10766
|
+
const managedSpawn = require('./engine/processes/managed-spawn');
|
|
10055
10767
|
const session = qaSessions.getSession(meta.sessionId);
|
|
10056
10768
|
if (!session) {
|
|
10057
10769
|
log('warn', `qa-session render: session ${meta.sessionId} not found — runner brief empty`);
|
|
@@ -10084,6 +10796,10 @@ function _buildRunnerBriefVars(item, project, qaRunPaths = {}) {
|
|
|
10084
10796
|
session,
|
|
10085
10797
|
sessionId: session.id,
|
|
10086
10798
|
spawnInfo,
|
|
10799
|
+
// W-msb9kgs402813a97-a — every co-service origin the session owns, not
|
|
10800
|
+
// just the primary `spawnInfo`. Runner adapters address a co-service by
|
|
10801
|
+
// its own baseUrl/ports instead of assuming one origin per session.
|
|
10802
|
+
services: buildQaSessionServices(session),
|
|
10087
10803
|
flowsRaw: (meta.qaSession && meta.qaSession.flowsRaw) || session.spec.flowsRaw || '',
|
|
10088
10804
|
capture: (meta.qaSession && meta.qaSession.capture) || session.spec.capture || {},
|
|
10089
10805
|
testFile,
|
|
@@ -10166,13 +10882,24 @@ function renderProjectWorkItemPromptForAgent(item, workType, agentId, config, pr
|
|
|
10166
10882
|
managedSpawnLookupUrl =
|
|
10167
10883
|
`http://localhost:${dashboardPort}/api/managed-processes/by-name/${encodeURIComponent(managedSpawnName)}`;
|
|
10168
10884
|
}
|
|
10169
|
-
const qaRuns = qaRunId ? require('./engine/qa
|
|
10885
|
+
const qaRuns = qaRunId ? require('./engine/qa/runs') : null;
|
|
10170
10886
|
const qaRunPaths = qaRunId
|
|
10171
10887
|
? {
|
|
10172
10888
|
artifactsDir: qaRuns.qaArtifactsDirForRun(qaRunId),
|
|
10173
10889
|
resultSidecar: qaRuns.resultSidecarPath(String(agentId)),
|
|
10174
10890
|
}
|
|
10175
10891
|
: { artifactsDir: '', resultSidecar: '' };
|
|
10892
|
+
// W-msb9kgs402813a97-b — absolute path of the DRAFT visual-journey manifest
|
|
10893
|
+
// sidecar. Only the DRAFT phase writes one; every other dispatch renders ''
|
|
10894
|
+
// (filtered via PLAYBOOK_OPTIONAL_VARS).
|
|
10895
|
+
let qaDraftResultSidecar = '';
|
|
10896
|
+
if (item.meta && item.meta.sessionId && item.meta.sessionPhase === 'draft') {
|
|
10897
|
+
try {
|
|
10898
|
+
qaDraftResultSidecar = require('./engine/qa/visual-journey').draftResultSidecarPath(String(agentId));
|
|
10899
|
+
} catch (err) {
|
|
10900
|
+
log('warn', `qa-session render: draft result sidecar path for ${item.meta.sessionId}: ${err.message}`);
|
|
10901
|
+
}
|
|
10902
|
+
}
|
|
10176
10903
|
const vars = {
|
|
10177
10904
|
...buildBaseVars(agentId, config, project),
|
|
10178
10905
|
item_id: item.id,
|
|
@@ -10199,7 +10926,7 @@ function renderProjectWorkItemPromptForAgent(item, workType, agentId, config, pr
|
|
|
10199
10926
|
// suppress their own premature PR-creation step the same way via
|
|
10200
10927
|
// {{#shared_branch}} / {{^shared_branch}} conditional blocks. Truthy only
|
|
10201
10928
|
// when the item is genuinely on a shared branch (mirrors selectPlaybook's
|
|
10202
|
-
// implement→implement-shared gate at engine/playbook.js). Default '' for
|
|
10929
|
+
// implement→implement-shared gate at engine/agents/playbook.js). Default '' for
|
|
10203
10930
|
// parallel/PR-targeted dispatches keeps existing per-type PR behavior.
|
|
10204
10931
|
shared_branch: (item.branchStrategy === 'shared-branch' && item.featureBranch) ? '1' : '',
|
|
10205
10932
|
// W-mrtdmtwq000kf391 — resolved PR contract flag. Truthy when the work item
|
|
@@ -10236,6 +10963,12 @@ function renderProjectWorkItemPromptForAgent(item, workType, agentId, config, pr
|
|
|
10236
10963
|
managed_spawn_ttl_minutes: item.meta && Number.isFinite(Number(item.meta.managed_spawn_ttl_minutes))
|
|
10237
10964
|
? Math.floor(Number(item.meta.managed_spawn_ttl_minutes))
|
|
10238
10965
|
: '',
|
|
10966
|
+
// W-ms5tb6ha016fda10 — declared execution surface. Resolved fail-closed so
|
|
10967
|
+
// only a valid `control-plane` opt-in on an eligible type reaches the
|
|
10968
|
+
// playbook; everything else renders as a normal repo dispatch.
|
|
10969
|
+
execution_surface: shared.isControlPlaneExecution(item, workType)
|
|
10970
|
+
? shared.EXECUTION_SURFACE.CONTROL_PLANE
|
|
10971
|
+
: shared.EXECUTION_SURFACE.REPO,
|
|
10239
10972
|
// W-mq5l3f2u000i9045 — opt-out flag for the inlined PR description audit
|
|
10240
10973
|
// section in fix.md / implement.md. When the dispatcher sets
|
|
10241
10974
|
// item.meta.skipDescriptionAudit (skill-meta updates, doc-only fixes,
|
|
@@ -10254,8 +10987,12 @@ function renderProjectWorkItemPromptForAgent(item, workType, agentId, config, pr
|
|
|
10254
10987
|
qa_target: (item.meta && item.meta.qaTarget) || null,
|
|
10255
10988
|
qa_artifacts_dir: qaRunPaths.artifactsDir,
|
|
10256
10989
|
qa_result_sidecar: qaRunPaths.resultSidecar,
|
|
10990
|
+
// W-msb9kgs402813a97-b — where the DRAFT agent writes its visual-journey
|
|
10991
|
+
// manifest. The engine consumes it exactly once in
|
|
10992
|
+
// engine/orchestration/lifecycle.js and gates EXECUTE on it.
|
|
10993
|
+
qa_draft_result_sidecar: qaDraftResultSidecar,
|
|
10257
10994
|
// P-e6b3c2d8 — QA Session template vars. The qa-sessions chain helpers
|
|
10258
|
-
// (engine/qa
|
|
10995
|
+
// (engine/qa/sessions.js#_baseWorkItem) stamp meta.sessionId,
|
|
10259
10996
|
// meta.sessionPhase, and meta.qaSession.{target,flowsRaw,mode,capture,runner}
|
|
10260
10997
|
// on each SETUP/DRAFT/EXECUTE WI; renderProjectWorkItemPromptForAgent
|
|
10261
10998
|
// surfaces them as named template vars so the qa-session-* playbooks
|
|
@@ -10269,6 +11006,18 @@ function renderProjectWorkItemPromptForAgent(item, workType, agentId, config, pr
|
|
|
10269
11006
|
? 'qa-session-' + String(item.meta.sessionId)
|
|
10270
11007
|
: '',
|
|
10271
11008
|
managed_spawn_lookup_url: managedSpawnLookupUrl,
|
|
11009
|
+
// W-msb9kgs402813a97-a — every managed-spawn the session owns, as
|
|
11010
|
+
// `[{name, project, primary, health, baseUrl, ports}]`. `managed_spawn_name`
|
|
11011
|
+
// stays the PRIMARY (back-compat); this var is what lets DRAFT/EXECUTE
|
|
11012
|
+
// address a co-service origin. Empty string when the item is not a QA
|
|
11013
|
+
// session (filtered via PLAYBOOK_OPTIONAL_VARS).
|
|
11014
|
+
session_services_json: _qaSessionServicesJson(item),
|
|
11015
|
+
// W-msb9kgs402813a97-c — the DRAFT-accepted visual-journey manifest,
|
|
11016
|
+
// projected into the sidecar's coverage-entry shape. EXECUTE's evidence
|
|
11017
|
+
// gate grades the run against these journey ids verbatim, so the agent has
|
|
11018
|
+
// to be shown them; the DRAFT sidecar that carried them is already
|
|
11019
|
+
// consumed. Empty for every non-EXECUTE and non-visual dispatch.
|
|
11020
|
+
visual_journey_manifest_json: _qaVisualJourneyManifestJson(item),
|
|
10272
11021
|
target_kind: (item.meta && item.meta.qaSession && item.meta.qaSession.target && item.meta.qaSession.target.kind) || '',
|
|
10273
11022
|
target_pr_id: (item.meta && item.meta.qaSession && item.meta.qaSession.target && item.meta.qaSession.target.kind === 'pr'
|
|
10274
11023
|
? String(item.meta.qaSession.target.prId || '')
|
|
@@ -10543,12 +11292,56 @@ function discoverFromWorkItems(config, project) {
|
|
|
10543
11292
|
}
|
|
10544
11293
|
}
|
|
10545
11294
|
|
|
11295
|
+
// W-mrzqfozm00035365 (requirement 7) — safe reconciliation: revive items
|
|
11296
|
+
// that terminally FAILED solely because a pre-execution dependency merge
|
|
11297
|
+
// conflict exhausted their retries, when the deterministic conflict-repair
|
|
11298
|
+
// WI exists and is still live (pending/dispatched) or done. Guarded by
|
|
11299
|
+
// shouldReviveFailedDependencyConflict so unrelated failures are never
|
|
11300
|
+
// revived. After revival the recoverable gate below governs redispatch, so
|
|
11301
|
+
// there is no immediate retry loop and no retry charge.
|
|
11302
|
+
if (item.status === WI_STATUS.FAILED && !isItemCompleted(item)) {
|
|
11303
|
+
const repair = findConflictRepairForBlockedItem(item.id, items, item._dependencyConflict?.repairWiId);
|
|
11304
|
+
if (shouldReviveFailedDependencyConflict(item, repair)) {
|
|
11305
|
+
// Ensure the repair links back to this item so the gate can find it on
|
|
11306
|
+
// later ticks (legacy repairs only tracked the first blocked item).
|
|
11307
|
+
if (!Array.isArray(repair._blockedItems)) {
|
|
11308
|
+
repair._blockedItems = repair._blockedItem ? [repair._blockedItem] : [];
|
|
11309
|
+
}
|
|
11310
|
+
if (!repair._blockedItems.includes(item.id)) repair._blockedItems.push(item.id);
|
|
11311
|
+
// Re-stamp / preserve the recoverable marker so the discovery gate
|
|
11312
|
+
// governs redispatch instead of an immediate dispatch attempt.
|
|
11313
|
+
if (!item._dependencyConflict) {
|
|
11314
|
+
item._dependencyConflict = buildDependencyConflictMarker({
|
|
11315
|
+
repairWiId: repair.id,
|
|
11316
|
+
conflictBranch: repair.featureBranch || repair._branch || null,
|
|
11317
|
+
conflictsWith: null,
|
|
11318
|
+
isInterDep: !!repair._isInterDepConflict,
|
|
11319
|
+
files: [],
|
|
11320
|
+
detectedAt: item.failedAt || ts(),
|
|
11321
|
+
});
|
|
11322
|
+
} else if (!item._dependencyConflict.repairWiId) {
|
|
11323
|
+
item._dependencyConflict.repairWiId = repair.id;
|
|
11324
|
+
}
|
|
11325
|
+
const prevReason = item.failReason;
|
|
11326
|
+
item.status = WI_STATUS.PENDING;
|
|
11327
|
+
delete item.failReason;
|
|
11328
|
+
delete item.failedAt;
|
|
11329
|
+
delete item._failureClass;
|
|
11330
|
+
delete item._lastDispatchResult;
|
|
11331
|
+
delete item.dispatched_at;
|
|
11332
|
+
delete item.dispatched_to;
|
|
11333
|
+
item._pendingReason = DEP_CONFLICT_PENDING_REASON;
|
|
11334
|
+
needsWrite = true;
|
|
11335
|
+
log('info', `Dependency-conflict reconciliation: revived ${item.id} from failed → pending behind repair ${repair.id} (was: ${prevReason})`);
|
|
11336
|
+
}
|
|
11337
|
+
}
|
|
11338
|
+
|
|
10546
11339
|
// #2996 follow-up: auto-recover from WORKTREE_DIRTY / WORKTREE_DIVERGENT quarantines.
|
|
10547
11340
|
// The quarantine is self-healing (dir renamed away, ref backed up, branch reset to
|
|
10548
11341
|
// origin), so the next dispatch starts from a clean worktree. Auto-flip to pending up
|
|
10549
11342
|
// to ENGINE_DEFAULTS.quarantineAutoRecoveryMax times so WIs without downstream deps and
|
|
10550
11343
|
// no human watching don't rot in `failed`. _failureClass is stamped on the WI only by
|
|
10551
|
-
// the force-demote path (engine/dispatch.js); the standard non-retryable path leaves
|
|
11344
|
+
// the force-demote path (engine/orchestration/dispatch.js); the standard non-retryable path leaves
|
|
10552
11345
|
// it on the dispatch record but embeds the class name in failReason — so detection
|
|
10553
11346
|
// covers both shapes.
|
|
10554
11347
|
if (item.status === WI_STATUS.FAILED && !isItemCompleted(item)) {
|
|
@@ -10629,6 +11422,37 @@ function discoverFromWorkItems(config, project) {
|
|
|
10629
11422
|
}
|
|
10630
11423
|
}
|
|
10631
11424
|
|
|
11425
|
+
// W-mrzqfozm00035365 — recoverable dependency merge-conflict gate. A
|
|
11426
|
+
// downstream item whose dependency branches conflicted during pre-execution
|
|
11427
|
+
// integration carries a `_dependencyConflict` marker instead of a terminal
|
|
11428
|
+
// failure. Decide wait/wake/block from the linked conflict-repair WI's
|
|
11429
|
+
// current status:
|
|
11430
|
+
// wait → repair still in progress; keep pending, do NOT redispatch into
|
|
11431
|
+
// the same conflict (no retry charged).
|
|
11432
|
+
// wake → repair done after detection; clear the marker and fall through
|
|
11433
|
+
// so re-dispatch re-fetches dep refs and recomputes integration.
|
|
11434
|
+
// block → repair failed/cancelled, or completed before re-detection and
|
|
11435
|
+
// branches still conflict; keep pending with an actionable
|
|
11436
|
+
// unresolved reason and DO NOT loop.
|
|
11437
|
+
if (item._dependencyConflict) {
|
|
11438
|
+
const repair = findConflictRepairForBlockedItem(item.id, items, item._dependencyConflict.repairWiId);
|
|
11439
|
+
const gate = evaluateDependencyConflictGate(item, repair);
|
|
11440
|
+
if (gate.action === 'wake') {
|
|
11441
|
+
delete item._dependencyConflict;
|
|
11442
|
+
if (item._pendingReason === DEP_CONFLICT_PENDING_REASON
|
|
11443
|
+
|| item._pendingReason === DEP_CONFLICT_UNRESOLVED_PENDING_REASON) {
|
|
11444
|
+
delete item._pendingReason;
|
|
11445
|
+
}
|
|
11446
|
+
needsWrite = true;
|
|
11447
|
+
log('info', `Dependency-conflict gate cleared for ${item.id}: ${gate.reason} — re-running dependency integration on fresh refs`);
|
|
11448
|
+
// fall through to the normal dispatch path.
|
|
11449
|
+
} else {
|
|
11450
|
+
if (item._pendingReason !== gate.pendingReason) { item._pendingReason = gate.pendingReason; needsWrite = true; }
|
|
11451
|
+
skipped.gated++;
|
|
11452
|
+
continue;
|
|
11453
|
+
}
|
|
11454
|
+
}
|
|
11455
|
+
|
|
10632
11456
|
const key = `work-${project?.name || 'default'}-${item.id}`;
|
|
10633
11457
|
// Self-heal: collect keys for batched dispatch cleanup.
|
|
10634
11458
|
selfHealKeys.add(key);
|
|
@@ -10795,7 +11619,7 @@ function discoverFromWorkItems(config, project) {
|
|
|
10795
11619
|
// extractWorkItemPrRef (title/first-paragraph-of-description text scan),
|
|
10796
11620
|
// which is fine for prompt-context enrichment (promptItem, above) that
|
|
10797
11621
|
// downgrades gracefully on a miss. But `dispatch.meta.pr` is a hard signal:
|
|
10798
|
-
// `_isPrBackedDispatch`/`getStalePrDispatchReason` (engine/dispatch.js)
|
|
11622
|
+
// `_isPrBackedDispatch`/`getStalePrDispatchReason` (engine/orchestration/dispatch.js)
|
|
10799
11623
|
// treat ANY dispatch carrying it as PR-backed and will cancel the source
|
|
10800
11624
|
// work item outright if the tracked PR turns out to be merged/abandoned/
|
|
10801
11625
|
// context-only or branch-mismatched. A loose text match against an
|
|
@@ -10810,7 +11634,7 @@ function discoverFromWorkItems(config, project) {
|
|
|
10810
11634
|
// refactor item whose prose mentioned "PR #3015 for context") and trip
|
|
10811
11635
|
// the gate even though the item has no real PR target. The dashboard's
|
|
10812
11636
|
// create-time stamp path keeps using the loose form — gate uses
|
|
10813
|
-
// structured-only; stamp uses loose. See engine/shared.js
|
|
11637
|
+
// structured-only; stamp uses loose. See engine/core/shared.js
|
|
10814
11638
|
// extractStructuredWorkItemPrRef for the structured-source list.
|
|
10815
11639
|
if (!linkedPr && getStructuredWorkItemPrRef(item) && (shared.isFixLikeWorkType(workType) || workType === WORK_TYPE.REVIEW || workType === WORK_TYPE.TEST)) {
|
|
10816
11640
|
if (item._pendingReason !== 'pr_not_found') { item._pendingReason = 'pr_not_found'; needsWrite = true; }
|
|
@@ -11362,6 +12186,45 @@ function discoverCentralWorkItems(config) {
|
|
|
11362
12186
|
continue;
|
|
11363
12187
|
}
|
|
11364
12188
|
|
|
12189
|
+
// W-ms9tcry701xz0389 — pre-spawn PRD result recovery. A previous
|
|
12190
|
+
// plan-to-prd attempt may have produced a perfectly valid sidecar that was
|
|
12191
|
+
// never imported (the dispatch was interrupted, timed out, or its
|
|
12192
|
+
// completion was not accepted). Because `_prdFilename` is now retry-stable
|
|
12193
|
+
// (see the plan-to-prd var block below), that sidecar still declares THIS
|
|
12194
|
+
// work item's expected identity — so import it and finish, instead of
|
|
12195
|
+
// burning another 10-minute model run recreating identical content.
|
|
12196
|
+
// Runs before agent resolution so a recovered item never claims an agent.
|
|
12197
|
+
// consumePrdResultSidecar fails closed: a sidecar belonging to another
|
|
12198
|
+
// work item or source plan is rejected and left untouched.
|
|
12199
|
+
if (workType === WORK_TYPE.PLAN_TO_PRD && item.planFile && item._prdFilename && item.dispatched_to) {
|
|
12200
|
+
const recovery = prdResultSidecar.consumePrdResultSidecar({
|
|
12201
|
+
agentsDir: AGENTS_DIR,
|
|
12202
|
+
agentId: item.dispatched_to,
|
|
12203
|
+
expectedFilename: item._prdFilename,
|
|
12204
|
+
planFile: item.planFile,
|
|
12205
|
+
workItemId: item.id,
|
|
12206
|
+
// W-msa0mrus00mh466f — a recovered revision must normalize exactly like
|
|
12207
|
+
// a completed one, or the sidecar rescue would resurrect the PRD still
|
|
12208
|
+
// flagged revision-requested/stale with its pointers dangling.
|
|
12209
|
+
revisionForPrd: item._revisionForPrd,
|
|
12210
|
+
plansDir: PLANS_DIR,
|
|
12211
|
+
});
|
|
12212
|
+
if (recovery.imported) {
|
|
12213
|
+
mutations.set(item.id, Object.assign(mutations.get(item.id) || {}, {
|
|
12214
|
+
status: WI_STATUS.DONE,
|
|
12215
|
+
completedAt: ts(),
|
|
12216
|
+
_prdFilename: recovery.filename,
|
|
12217
|
+
_artifacts: { ...(item._artifacts || {}), prd: recovery.filename, plan: item.planFile },
|
|
12218
|
+
resultSummary: `Imported the PRD result sidecar left by a prior dispatch attempt (${recovery.filename}) — no re-run needed.`,
|
|
12219
|
+
}));
|
|
12220
|
+
log('info', `plan-to-prd ${item.id}: recovered PRD "${recovery.filename}" from a prior attempt's sidecar — skipping re-dispatch`);
|
|
12221
|
+
continue;
|
|
12222
|
+
}
|
|
12223
|
+
if (recovery.code !== prdResultSidecar.PRD_RESULT_OUTCOME.ABSENT) {
|
|
12224
|
+
log('info', `plan-to-prd ${item.id}: prior sidecar not usable (${recovery.reason}) — dispatching a fresh attempt`);
|
|
12225
|
+
}
|
|
12226
|
+
}
|
|
12227
|
+
|
|
11365
12228
|
if (isFanOut) {
|
|
11366
12229
|
// ─── Fan-out: dispatch to ALL idle agents ───────────────────────
|
|
11367
12230
|
const idleAgents = Object.entries(config.agents)
|
|
@@ -11646,13 +12509,29 @@ function discoverCentralWorkItems(config) {
|
|
|
11646
12509
|
// `plans/` prefix or separator difference between item.planFile and a
|
|
11647
12510
|
// PRD's source_plan can't fork a duplicate PRD (#415 class).
|
|
11648
12511
|
let prdFilename = null;
|
|
12512
|
+
let resolvedPrd = null;
|
|
11649
12513
|
const planKey = path.basename(String(item.planFile || ''));
|
|
11650
12514
|
const prdRows = prdStore.listPrdRows();
|
|
11651
12515
|
const prdFiles = prdRows.filter(row => !row.archived).map(row => row.filename);
|
|
12516
|
+
// Explicit PRD pin (W-msa0mrus00mh466f): a revision / diff-aware regen
|
|
12517
|
+
// work item names the PRD it must rewrite. Honor it before the
|
|
12518
|
+
// source_plan scan so the operation lands on that exact PRD even when
|
|
12519
|
+
// the PRD has no source markdown to match on.
|
|
12520
|
+
if (item._existingPrdFile && prdFiles.includes(item._existingPrdFile)) {
|
|
12521
|
+
const pinned = prdStore.readPrd(item._existingPrdFile);
|
|
12522
|
+
if (pinned && !shared.isPrdArchived(pinned)) {
|
|
12523
|
+
prdFilename = item._existingPrdFile;
|
|
12524
|
+
resolvedPrd = pinned;
|
|
12525
|
+
vars.existing_prd_json = JSON.stringify(pinned, null, 2);
|
|
12526
|
+
log('info', `plan-to-prd: targeting pinned PRD "${prdFilename}" for ${item.id} (_existingPrdFile)`);
|
|
12527
|
+
}
|
|
12528
|
+
}
|
|
11652
12529
|
for (const pf of prdFiles) {
|
|
12530
|
+
if (prdFilename) break;
|
|
11653
12531
|
const prd = prdStore.readPrd(pf);
|
|
11654
12532
|
if (planKey && prd && !shared.isPrdArchived(prd) && shared.prdMatchesSourcePlan(prd.source_plan, planKey)) {
|
|
11655
12533
|
prdFilename = pf;
|
|
12534
|
+
resolvedPrd = prd;
|
|
11656
12535
|
vars.existing_prd_json = JSON.stringify(prd, null, 2);
|
|
11657
12536
|
log('info', `plan-to-prd: reusing existing PRD "${pf}" for plan "${item.planFile}" (#884)`);
|
|
11658
12537
|
break;
|
|
@@ -11664,6 +12543,20 @@ function discoverCentralWorkItems(config) {
|
|
|
11664
12543
|
// prepared this same tick — REUSE it so concurrent/repeat plan-to-prd
|
|
11665
12544
|
// runs for one plan converge to a single PRD instead of splitting into
|
|
11666
12545
|
// <slug>-<date>.json / -2.json / -3.json. Match by basename.
|
|
12546
|
+
if (!prdFilename && planKey) {
|
|
12547
|
+
// W-ms9tcry701xz0389 — RETRY STABILITY comes first: reuse the
|
|
12548
|
+
// filename THIS item already pinned for THIS plan. The externally
|
|
12549
|
+
// visible result identity (the name the agent is told to declare in
|
|
12550
|
+
// its sidecar) must never be re-derived from transient per-dispatch
|
|
12551
|
+
// state. It used to be: the cross-repo slug is `cross-<uid>` with a
|
|
12552
|
+
// fresh uid per dispatch, so every retry invented a new expected
|
|
12553
|
+
// filename, instantly orphaning the previous attempt's otherwise
|
|
12554
|
+
// valid sidecar and forcing a full regeneration.
|
|
12555
|
+
if (item._prdFilename && (!item._planKey || item._planKey === planKey)) {
|
|
12556
|
+
prdFilename = item._prdFilename;
|
|
12557
|
+
log('info', `plan-to-prd: reusing pinned PRD "${prdFilename}" for ${item.id} (retry-stable identity)`);
|
|
12558
|
+
}
|
|
12559
|
+
}
|
|
11667
12560
|
if (!prdFilename && planKey) {
|
|
11668
12561
|
for (const otherItem of items) {
|
|
11669
12562
|
if (!otherItem || otherItem.id === item.id || !otherItem._prdFilename) continue;
|
|
@@ -11715,6 +12608,12 @@ function discoverCentralWorkItems(config) {
|
|
|
11715
12608
|
while (prdExisting.has(prdFilename)) { prdFilename = prdBase + '-' + prdCounter + '.json'; prdCounter++; }
|
|
11716
12609
|
}
|
|
11717
12610
|
vars.prd_filename = prdFilename;
|
|
12611
|
+
// W-msa0mrus00mh466f — a revision dispatch's ONLY distinguishing input is
|
|
12612
|
+
// the operator's feedback. `planFile` names the source markdown, so
|
|
12613
|
+
// {{plan_content}} renders the plan and the work-item description (which
|
|
12614
|
+
// embeds the feedback) is never spliced into this playbook. Carry the
|
|
12615
|
+
// feedback explicitly, read from the PRD that owns it.
|
|
12616
|
+
vars.revision_feedback = shared.resolvePrdRevisionFeedback(item, resolvedPrd, prdFilename);
|
|
11718
12617
|
// Pin _planKey alongside _prdFilename so the same-plan in-flight reuse
|
|
11719
12618
|
// above can recognize a sibling prepared earlier THIS tick by plan.
|
|
11720
12619
|
mutations.set(item.id, Object.assign(mutations.get(item.id) || {}, { _prdFilename: prdFilename, _planKey: planKey }));
|
|
@@ -11921,10 +12820,10 @@ async function discoverWork(config) {
|
|
|
11921
12820
|
// Gated by config.engine?.scheduledWorkDiscoveryEnabled !== false (P-d6f0a2b5).
|
|
11922
12821
|
if (config.engine?.scheduledWorkDiscoveryEnabled !== false) {
|
|
11923
12822
|
try {
|
|
11924
|
-
const { discoverScheduledWork } = require('./engine/scheduler');
|
|
12823
|
+
const { discoverScheduledWork } = require('./engine/orchestration/scheduler');
|
|
11925
12824
|
const scheduledWork = discoverScheduledWork(config);
|
|
11926
12825
|
if (scheduledWork.length > 0) {
|
|
11927
|
-
const { createMeeting, getMeetings } = require('./engine/meeting');
|
|
12826
|
+
const { createMeeting, getMeetings } = require('./engine/orchestration/meeting');
|
|
11928
12827
|
// Separate meetings (no work-items write) from task items
|
|
11929
12828
|
const taskItems = [];
|
|
11930
12829
|
for (const item of scheduledWork) {
|
|
@@ -11982,7 +12881,7 @@ async function discoverWork(config) {
|
|
|
11982
12881
|
|
|
11983
12882
|
// Meeting work (multi-round team discussions)
|
|
11984
12883
|
try {
|
|
11985
|
-
const { discoverMeetingWork } = require('./engine/meeting');
|
|
12884
|
+
const { discoverMeetingWork } = require('./engine/orchestration/meeting');
|
|
11986
12885
|
const meetingWork = discoverMeetingWork(config);
|
|
11987
12886
|
// #768 — persist immediately, same rationale as the per-project loop above.
|
|
11988
12887
|
await persistBatch(meetingWork);
|
|
@@ -11990,7 +12889,7 @@ async function discoverWork(config) {
|
|
|
11990
12889
|
|
|
11991
12890
|
// Pipeline orchestration — check stage completions and start ready stages
|
|
11992
12891
|
try {
|
|
11993
|
-
const { discoverPipelineWork } = require('./engine/pipeline');
|
|
12892
|
+
const { discoverPipelineWork } = require('./engine/orchestration/pipeline');
|
|
11994
12893
|
await discoverPipelineWork(config);
|
|
11995
12894
|
} catch (e) { log('warn', 'discover pipeline work: ' + e.message); }
|
|
11996
12895
|
|
|
@@ -11999,7 +12898,7 @@ async function discoverWork(config) {
|
|
|
11999
12898
|
// Throttled to ~10 min (P3 decision) — cadence in ENGINE_DEFAULTS.planCompletionScanEvery
|
|
12000
12899
|
if (tickCount % (ENGINE_DEFAULTS.planCompletionScanEvery || 60) === 0) {
|
|
12001
12900
|
try {
|
|
12002
|
-
const lifecycle = require('./engine/lifecycle');
|
|
12901
|
+
const lifecycle = require('./engine/orchestration/lifecycle');
|
|
12003
12902
|
for (const f of prdStore.listPrdRows().filter(row => !row.archived).map(row => row.filename)) {
|
|
12004
12903
|
if (completedPlanCache.has(f)) continue;
|
|
12005
12904
|
const plan = prdStore.readPrd(f);
|
|
@@ -12322,7 +13221,7 @@ function _resolveTickHardTimeoutMs() {
|
|
|
12322
13221
|
// W-mrktrokd — retry default-schedule startup reconciliation until it completes
|
|
12323
13222
|
// while enabled, then keep the steady-state tick free of config.json work.
|
|
12324
13223
|
let _scheduleBootstrapDone = false;
|
|
12325
|
-
function _runScheduleBootstrapOnce(config, scheduleBootstrap = require('./engine/schedule-bootstrap')) {
|
|
13224
|
+
function _runScheduleBootstrapOnce(config, scheduleBootstrap = require('./engine/orchestration/schedule-bootstrap')) {
|
|
12326
13225
|
if (_scheduleBootstrapDone || !scheduleBootstrap.bootstrapEnabled(config)) return [];
|
|
12327
13226
|
const reconciled = scheduleBootstrap.ensureBootstrapSchedules();
|
|
12328
13227
|
if (reconciled === null) return [];
|
|
@@ -12343,7 +13242,7 @@ async function tickInner() {
|
|
|
12343
13242
|
}
|
|
12344
13243
|
|
|
12345
13244
|
// W-mpcyvff6000pf828 (#2653) — control.heartbeat is written by a dedicated
|
|
12346
|
-
// 15s interval in engine/cli.js (createHeartbeatWriter), decoupled from
|
|
13245
|
+
// 15s interval in engine/operations/cli.js (createHeartbeatWriter), decoupled from
|
|
12347
13246
|
// tickInner so a slow tick (cold runtime spawn, sequential PR polls, slow
|
|
12348
13247
|
// worktree create) cannot starve heartbeats and flip the dashboard to STALE
|
|
12349
13248
|
// on an otherwise healthy engine.
|
|
@@ -12357,7 +13256,7 @@ async function tickInner() {
|
|
|
12357
13256
|
// dashboard's #engine-quick-stats "Next tick in Xs" countdown has a reliable
|
|
12358
13257
|
// anchor (control.json carries no separate tick-counter field). Synchronous
|
|
12359
13258
|
// mutator, no awaits inside the lock — mirrors the writer contract used by
|
|
12360
|
-
// engine/cli.js#writeHeartbeatNow. Distinct from control.heartbeat: this
|
|
13259
|
+
// engine/operations/cli.js#writeHeartbeatNow. Distinct from control.heartbeat: this
|
|
12361
13260
|
// advances once per tick cadence, not on the 15s heartbeat cadence, so the
|
|
12362
13261
|
// client can render an accurate "next tick" countdown without coupling to
|
|
12363
13262
|
// the staleness signal.
|
|
@@ -12380,13 +13279,13 @@ async function tickInner() {
|
|
|
12380
13279
|
if (_isTickStale(myGeneration)) return;
|
|
12381
13280
|
safe('checkSteering', () => checkSteering(config));
|
|
12382
13281
|
safe('checkSpawnPhaseStalls', () => {
|
|
12383
|
-
const { checkSpawnPhaseStalls } = require('./engine/spawn-phase-watchdog');
|
|
13282
|
+
const { checkSpawnPhaseStalls } = require('./engine/agents/spawn-phase-watchdog');
|
|
12384
13283
|
checkSpawnPhaseStalls(activeProcesses, config);
|
|
12385
13284
|
});
|
|
12386
13285
|
safe('checkIdleThreshold', () => checkIdleThreshold(config));
|
|
12387
13286
|
|
|
12388
13287
|
// 1b. Check for meeting round timeouts
|
|
12389
|
-
safe('meetingTimeouts', () => { const { checkMeetingTimeouts } = require('./engine/meeting'); checkMeetingTimeouts(config); });
|
|
13288
|
+
safe('meetingTimeouts', () => { const { checkMeetingTimeouts } = require('./engine/orchestration/meeting'); checkMeetingTimeouts(config); });
|
|
12390
13289
|
|
|
12391
13290
|
// In stopping state, only track agent completions — skip discovery and dispatch
|
|
12392
13291
|
if (control.state === 'stopping') {
|
|
@@ -12418,7 +13317,7 @@ async function tickInner() {
|
|
|
12418
13317
|
// disables it; 4h cadence is enforced inside shouldAutoSweep().
|
|
12419
13318
|
if ((config.engine?.autoConsolidateMemory ?? ENGINE_DEFAULTS.autoConsolidateMemory) === true) {
|
|
12420
13319
|
safe('autoSweepKb', () => {
|
|
12421
|
-
const { shouldAutoSweep, spawnSweepRunnerDetached } = require('./engine/kb-sweep');
|
|
13320
|
+
const { shouldAutoSweep, spawnSweepRunnerDetached } = require('./engine/memory/kb-sweep');
|
|
12422
13321
|
const decision = shouldAutoSweep();
|
|
12423
13322
|
if (!decision.shouldSpawn) return;
|
|
12424
13323
|
const result = spawnSweepRunnerDetached({
|
|
@@ -12447,7 +13346,7 @@ async function tickInner() {
|
|
|
12447
13346
|
const keepSweepEvery = Math.max(1, ENGINE_DEFAULTS.keepProcesses?.sweepEvery || 180);
|
|
12448
13347
|
if (ENGINE_DEFAULTS.keepProcesses?.enabled !== false && tickCount % keepSweepEvery === 0) {
|
|
12449
13348
|
safe('sweepKeepProcesses', () => {
|
|
12450
|
-
const { sweepKeepProcesses } = require('./engine/keep-process-sweep');
|
|
13349
|
+
const { sweepKeepProcesses } = require('./engine/processes/keep-process-sweep');
|
|
12451
13350
|
const stats = sweepKeepProcesses();
|
|
12452
13351
|
if (stats.scanned > 0 && (stats.expiredFiles || stats.deadFiles || stats.malformed)) {
|
|
12453
13352
|
log('info', `keep-processes sweep: scanned=${stats.scanned} expired=${stats.expiredFiles} dead=${stats.deadFiles} malformed=${stats.malformed} killed=${stats.killedPids}`);
|
|
@@ -12465,7 +13364,7 @@ async function tickInner() {
|
|
|
12465
13364
|
const managedSweepEvery = Math.max(1, ENGINE_DEFAULTS.managedSpawn?.sweepEvery || 180);
|
|
12466
13365
|
if (ENGINE_DEFAULTS.managedSpawn?.enabled !== false && tickCount % managedSweepEvery === 0) {
|
|
12467
13366
|
safe('sweepManagedSpawn', () => {
|
|
12468
|
-
const { sweepManagedSpawn } = require('./engine/managed-spawn');
|
|
13367
|
+
const { sweepManagedSpawn } = require('./engine/processes/managed-spawn');
|
|
12469
13368
|
const stats = sweepManagedSpawn();
|
|
12470
13369
|
if (stats.scanned > 0 && (stats.ttlExpired || stats.deadDropped || stats.rotatedLogs || stats.malformed)) {
|
|
12471
13370
|
log('info', `managed-spawn sweep: scanned=${stats.scanned} ttl=${stats.ttlExpired} dead=${stats.deadDropped} killed=${stats.killedPids} rotated=${stats.rotatedLogs} malformed=${stats.malformed}`);
|
|
@@ -12478,7 +13377,7 @@ async function tickInner() {
|
|
|
12478
13377
|
// dispatch-end gcDispatchWorktreeIfOrphan couldn't remove (typically Windows
|
|
12479
13378
|
// file locks: EPERM/EBUSY on lingering spawn-agent.exe handles, AV scans,
|
|
12480
13379
|
// node_modules\.bin shims). Delegates to `cleanup.runPeriodicWorktreeSweep`,
|
|
12481
|
-
// which runs the two pruners in `engine/
|
|
13380
|
+
// which runs the two pruners in `engine/worktrees/gc.js` against a single
|
|
12482
13381
|
// shared `dispatchSnap`: (1) in-root `pruneOrphanWorktrees` and (2)
|
|
12483
13382
|
// out-of-root `pruneOrphanWorktreesFromGitRegistry` (catches `git worktree
|
|
12484
13383
|
// list` entries outside `worktreeRoot`). Runs at worktreePruneIntervalTicks
|
|
@@ -12486,7 +13385,7 @@ async function tickInner() {
|
|
|
12486
13385
|
const wtPruneEvery = Math.max(1, ENGINE_DEFAULTS.worktreePruneIntervalTicks || 30);
|
|
12487
13386
|
if (tickCount % wtPruneEvery === 0) {
|
|
12488
13387
|
safe('pruneWorktreesPeriodic', () => {
|
|
12489
|
-
const { runPeriodicWorktreeSweep } = require('./engine/cleanup');
|
|
13388
|
+
const { runPeriodicWorktreeSweep } = require('./engine/orchestration/cleanup');
|
|
12490
13389
|
const stats = runPeriodicWorktreeSweep(config);
|
|
12491
13390
|
const reportStats = (finalStats) => {
|
|
12492
13391
|
const removed = finalStats.removed
|
|
@@ -12512,8 +13411,8 @@ async function tickInner() {
|
|
|
12512
13411
|
safe('checkWatches', () => {
|
|
12513
13412
|
const { checkWatches } = require('./engine/watches');
|
|
12514
13413
|
const projects = getProjects(config);
|
|
12515
|
-
const pullRequestStore = require('./engine/pull-requests-store');
|
|
12516
|
-
const workItemStore = require('./engine/work-items-store');
|
|
13414
|
+
const pullRequestStore = require('./engine/persistence/pull-requests-store');
|
|
13415
|
+
const workItemStore = require('./engine/planning/work-items-store');
|
|
12517
13416
|
const pullRequests = projects.flatMap(p => pullRequestStore.readPullRequestsForScope(p.name));
|
|
12518
13417
|
const workItems = projects.flatMap(p => workItemStore.readWorkItemsForScope(p.name));
|
|
12519
13418
|
// Also include central work items
|
|
@@ -12523,16 +13422,16 @@ async function tickInner() {
|
|
|
12523
13422
|
// best-effort — if a module/file is missing the watch evaluator will
|
|
12524
13423
|
// simply report "not found" rather than crashing the tick.
|
|
12525
13424
|
let meetings = [];
|
|
12526
|
-
try { meetings = require('./engine/meeting').getMeetings(); } catch { /* optional */ }
|
|
13425
|
+
try { meetings = require('./engine/orchestration/meeting').getMeetings(); } catch { /* optional */ }
|
|
12527
13426
|
|
|
12528
|
-
const plans = require('./engine/prd-store').listPrdRows()
|
|
13427
|
+
const plans = require('./engine/planning/prd-store').listPrdRows()
|
|
12529
13428
|
.map(({ filename, plan }) => ({ ...plan, _source: filename, _sourcePlan: plan.source_plan || '' }));
|
|
12530
13429
|
|
|
12531
13430
|
let scheduleRuns = {};
|
|
12532
|
-
try { scheduleRuns = require('./engine/small-state-store').readScheduleRuns(); } catch { /* optional */ }
|
|
13431
|
+
try { scheduleRuns = require('./engine/persistence/small-state-store').readScheduleRuns(); } catch { /* optional */ }
|
|
12533
13432
|
|
|
12534
13433
|
let pipelineRuns = {};
|
|
12535
|
-
try { pipelineRuns = require('./engine/pipeline').getPipelineRuns(); } catch { /* optional */ }
|
|
13434
|
+
try { pipelineRuns = require('./engine/orchestration/pipeline').getPipelineRuns(); } catch { /* optional */ }
|
|
12536
13435
|
|
|
12537
13436
|
let dispatch = { pending: [], active: [], completed: [] };
|
|
12538
13437
|
try { dispatch = queries.getDispatch() || dispatch; } catch { /* optional */ }
|
|
@@ -12776,7 +13675,7 @@ async function tickInner() {
|
|
|
12776
13675
|
// W-mrdykpv60005d50d: don't resurrect items whose failure class is explicitly
|
|
12777
13676
|
// non-retryable (AUTH, INJECTION_FLAGGED, PRE_DISPATCH_EVAL_STUCK, etc.) — mirrors
|
|
12778
13677
|
// the same isRetryableFailureReason() gate used by the dispatch-completion retry path
|
|
12779
|
-
// (engine/dispatch.js:764) and the quarantine auto-recovery block above (engine.js:8804-8811).
|
|
13678
|
+
// (engine/orchestration/dispatch.js:764) and the quarantine auto-recovery block above (engine.js:8804-8811).
|
|
12780
13679
|
if (!isRetryableFailureReason(item.failReason, item._failureClass)) {
|
|
12781
13680
|
log('info', `Stall recovery: skipping ${item.id} — failure class '${item._failureClass || 'unknown'}' is non-retryable`);
|
|
12782
13681
|
continue;
|
|
@@ -13103,8 +14002,28 @@ async function tickInner() {
|
|
|
13103
14002
|
// item itself resolves to live mode — worktree-mode coding items on
|
|
13104
14003
|
// the same project remain uncapped.
|
|
13105
14004
|
const itemProjName = item.project || item.meta?.project?.name || null;
|
|
14005
|
+
// W-ms5tb6ha016fda10: resolved fail-closed from the item's own declaration.
|
|
14006
|
+
// An item that allocates NO local checkout — control-plane (API/CLI only)
|
|
14007
|
+
// or cloud (a remote agent owns the workspace) — cannot legitimately be
|
|
14008
|
+
// blocked by any of the checkout-allocation gates below: the live-mode
|
|
14009
|
+
// concurrency cap, the operator branch hold, the worktree free-space
|
|
14010
|
+
// preflight, or the live-checkout dirty/stale-base precheck. Gating on the
|
|
14011
|
+
// resolved SURFACE rather than on control-plane specifically is what keeps
|
|
14012
|
+
// a second no-checkout surface from silently inheriting repo-shaped gates
|
|
14013
|
+
// (and pending as `worktree_low_disk_space` instead of its own honest
|
|
14014
|
+
// reason). Both predicates fail closed, so anything malformed resolves to
|
|
14015
|
+
// `repo` and keeps today's behavior.
|
|
14016
|
+
const _itemIsControlPlane = shared.isControlPlaneExecution(item, item.type);
|
|
14017
|
+
const _itemExecutionSurface = _itemIsControlPlane
|
|
14018
|
+
? shared.EXECUTION_SURFACE.CONTROL_PLANE
|
|
14019
|
+
: (shared.isCloudExecution(item, item.type)
|
|
14020
|
+
? shared.EXECUTION_SURFACE.CLOUD
|
|
14021
|
+
: shared.EXECUTION_SURFACE.REPO);
|
|
14022
|
+
const _itemSkipsCheckoutAllocation =
|
|
14023
|
+
_itemExecutionSurface !== shared.EXECUTION_SURFACE.REPO;
|
|
13106
14024
|
if (
|
|
13107
14025
|
itemProjName
|
|
14026
|
+
&& !_itemSkipsCheckoutAllocation
|
|
13108
14027
|
&& !READ_ONLY_ROOT_TASK_TYPES.has(item.type)
|
|
13109
14028
|
&& liveProjectsInUse.has(itemProjName)
|
|
13110
14029
|
) {
|
|
@@ -13122,7 +14041,7 @@ async function tickInner() {
|
|
|
13122
14041
|
const _branchHoldProjCfg = shared.findProjectByName(shared.getProjects(config), itemProjName);
|
|
13123
14042
|
const _allocationBranchHold = await _checkWorktreeBranchHoldForDispatch(
|
|
13124
14043
|
_branchHoldProjCfg, item.type, itemBranch, config,
|
|
13125
|
-
{ listWorktrees: listAllocationWorktrees },
|
|
14044
|
+
{ listWorktrees: listAllocationWorktrees, executionSurface: _itemExecutionSurface },
|
|
13126
14045
|
);
|
|
13127
14046
|
if (_isTickStale(myGeneration)) return;
|
|
13128
14047
|
if (_allocationBranchHold?.held) {
|
|
@@ -13142,7 +14061,9 @@ async function tickInner() {
|
|
|
13142
14061
|
// filesystem has the minimum free-space reserve.
|
|
13143
14062
|
if (itemProjName) {
|
|
13144
14063
|
const _diskProjCfg = shared.findProjectByName(shared.getProjects(config), itemProjName);
|
|
13145
|
-
const _allocationDiskPreflight = _checkWorktreeDiskSpaceForDispatch(
|
|
14064
|
+
const _allocationDiskPreflight = _checkWorktreeDiskSpaceForDispatch(
|
|
14065
|
+
_diskProjCfg, item.type, config, { executionSurface: _itemExecutionSurface },
|
|
14066
|
+
);
|
|
13146
14067
|
if (_allocationDiskPreflight && !_allocationDiskPreflight.ok) {
|
|
13147
14068
|
const pendingReason = _persistWorktreeDiskPreflight(item, _allocationDiskPreflight);
|
|
13148
14069
|
_writeWorktreeDiskPreflightAlert(item, _diskProjCfg, _allocationDiskPreflight);
|
|
@@ -13168,12 +14089,13 @@ async function tickInner() {
|
|
|
13168
14089
|
// fire rarely, as the exception path rather than the common path.
|
|
13169
14090
|
if (
|
|
13170
14091
|
itemProjName
|
|
14092
|
+
&& !_itemSkipsCheckoutAllocation
|
|
13171
14093
|
&& !READ_ONLY_ROOT_TASK_TYPES.has(item.type)
|
|
13172
14094
|
) {
|
|
13173
14095
|
const _precheckProjCfg = shared.findProjectByName(shared.getProjects(config), itemProjName);
|
|
13174
14096
|
if (_precheckProjCfg && shared.resolveCheckoutMode(_precheckProjCfg, item.type) === 'live' && _precheckProjCfg.localPath) {
|
|
13175
14097
|
const _precheckLocalPath = path.resolve(String(_precheckProjCfg.localPath));
|
|
13176
|
-
const _liveCheckoutPrecheck = require('./engine/live-checkout');
|
|
14098
|
+
const _liveCheckoutPrecheck = require('./engine/worktrees/live-checkout');
|
|
13177
14099
|
const _persistPendingReason = (reason) => {
|
|
13178
14100
|
item._pendingReason = reason;
|
|
13179
14101
|
try {
|
|
@@ -13303,7 +14225,7 @@ async function tickInner() {
|
|
|
13303
14225
|
// rather than awaiting each spawnAgent serially. `spawnAgent` does real
|
|
13304
14226
|
// wall-clock work per item (prompt build + `git worktree add`, which on a
|
|
13305
14227
|
// ~250k-file monorepo can take multiple seconds), so the old sequential loop
|
|
13306
|
-
// made eager routing (engine/routing.js already fans work across every idle
|
|
14228
|
+
// made eager routing (engine/orchestration/routing.js already fans work across every idle
|
|
13307
14229
|
// agent in a single allocation pass) visibly "trickle in" one agent at a time,
|
|
13308
14230
|
// contradicting the stated eager-dispatch design. `toDispatch` is already
|
|
13309
14231
|
// bounded by `slotsAvailable` (≤ maxConcurrent — the seed loop above decrements
|
|
@@ -13594,7 +14516,7 @@ function emitMemoryBaseline(tickN) {
|
|
|
13594
14516
|
`gcPauses=${sample.gcCount}/${sample.gcPausesTotalMs.toFixed(2)}ms tickN=${tickN}`);
|
|
13595
14517
|
}
|
|
13596
14518
|
|
|
13597
|
-
// ─── Exports (for engine/cli.js and other modules) ──────────────────────────
|
|
14519
|
+
// ─── Exports (for engine/operations/cli.js and other modules) ──────────────────────────
|
|
13598
14520
|
|
|
13599
14521
|
module.exports = {
|
|
13600
14522
|
// Paths
|
|
@@ -13605,13 +14527,15 @@ module.exports = {
|
|
|
13605
14527
|
// Utilities
|
|
13606
14528
|
ts, logTs, dateStamp, log,
|
|
13607
14529
|
safeJson, safeRead, safeWrite,
|
|
14530
|
+
buildQaSessionServices, // exported for testing (W-msb9kgs402813a97-a)
|
|
14531
|
+
buildQaExecuteJourneyContract, // exported for testing (W-msb9kgs402813a97-c)
|
|
13608
14532
|
|
|
13609
14533
|
// State readers/writers
|
|
13610
14534
|
getConfig, getControl, getDispatch, getRouting, getNotes,
|
|
13611
14535
|
getAgentStatus, getInboxFiles, getPrs,
|
|
13612
14536
|
validateConfig,
|
|
13613
14537
|
|
|
13614
|
-
// Dispatch management (re-exported from engine/dispatch.js)
|
|
14538
|
+
// Dispatch management (re-exported from engine/orchestration/dispatch.js)
|
|
13615
14539
|
mutateDispatch, addToDispatch, addToDispatchWithValidation, isRetryableFailureReason, completeDispatch, writeInboxAlert, updateAgentStatus, pruneStalePrDispatches, pruneStalePrDispatchesAsync,
|
|
13616
14540
|
activeProcesses, realActivityMap, engineRestartGraceExempt,
|
|
13617
14541
|
get engineRestartGraceUntil() { return engineRestartGraceUntil; },
|
|
@@ -13634,6 +14558,9 @@ module.exports = {
|
|
|
13634
14558
|
resolveDependencyBranches, buildCrossRepoDepsSection, // exported for testing (P-faea3206)
|
|
13635
14559
|
gitOutputToString, gitErrorOutput, classifyDepMergeFailureOutput, listUnmergedFiles, // exported for testing
|
|
13636
14560
|
buildDepConflictFixItem, deriveConflictFixKey, // exported for testing (W-mpcwojgr000a0244)
|
|
14561
|
+
buildDependencyConflictMarker, findConflictRepairForBlockedItem, // exported for testing (W-mrzqfozm00035365)
|
|
14562
|
+
evaluateDependencyConflictGate, shouldReviveFailedDependencyConflict, // exported for testing (W-mrzqfozm00035365)
|
|
14563
|
+
DEP_CONFLICT_PENDING_REASON, DEP_CONFLICT_UNRESOLVED_PENDING_REASON, // exported for testing (W-mrzqfozm00035365)
|
|
13637
14564
|
runWorktreeAdd, // exported for testing (W-mqvaxv65000m76f2 — GVFS partial-checkout behavioral test)
|
|
13638
14565
|
_fetchWithTransientRetry, _classifyFreshBaseFetchFailure, _probeBranchLocally, // exported for testing
|
|
13639
14566
|
isWorktreeRetryableError, syncReusedWorktree, pushCleanAheadBranch, assertCleanSharedWorktree, _quarantineDirtyWorktree, // exported for testing
|
|
@@ -13650,6 +14577,8 @@ module.exports = {
|
|
|
13650
14577
|
_tryAutoResolveLiveCheckoutWorktreeConflict, // exported for testing (W-mr28h2j2000y0de1 review — auto-resolve worktree conflicts)
|
|
13651
14578
|
_isOutputTruncated, AGENT_OUTPUT_CAP_BYTES, // P-8e4c2a17: exported for testing (OUTPUT_TRUNCATED detection)
|
|
13652
14579
|
promoteCheckpointSteeringForClose, // exported for testing
|
|
14580
|
+
resolveCloseCompletionBoundary, // exported for testing (W-ms9tcs3o01y03383)
|
|
14581
|
+
releasePendingSteeringResumeForCompletedTurn, // exported for testing (W-ms9tcs3o01y03383)
|
|
13653
14582
|
normalizePrBranch, resolvePrBranch, prCausePart, getPrCauseHead, getPrCauseBase, getPrAutomationCauseKey, getPrAutomationDispatchKey, // exported for testing
|
|
13654
14583
|
isPrReviewCurrent, isPrFixedAfterReview, // exported for testing (W-mrej8p740006b0bf — SHA-based review freshness)
|
|
13655
14584
|
ensurePrBranchForDispatch, isWithinLinkGraceWindow, PR_LINK_GRACE_WINDOW_MS, // exported for testing (W-mphm0kt0000cebc3)
|
|
@@ -13661,10 +14590,10 @@ module.exports = {
|
|
|
13661
14590
|
resolvePriorExploreContext, // exported for testing (P-mqyp0008v022w3x4 SHERLOC)
|
|
13662
14591
|
renderProjectWorkItemPromptForAgent, // exported for testing
|
|
13663
14592
|
|
|
13664
|
-
// Timeout / Steering / Idle (re-exported from engine/timeout.js)
|
|
14593
|
+
// Timeout / Steering / Idle (re-exported from engine/orchestration/timeout.js)
|
|
13665
14594
|
checkTimeouts, checkSteering, checkIdleThreshold,
|
|
13666
14595
|
|
|
13667
|
-
// Cleanup (re-exported from engine/cleanup.js)
|
|
14596
|
+
// Cleanup (re-exported from engine/orchestration/cleanup.js)
|
|
13668
14597
|
runCleanup,
|
|
13669
14598
|
|
|
13670
14599
|
// Post-completion / lifecycle
|
|
@@ -13750,7 +14679,7 @@ function ensureGitHooksInstalled() {
|
|
|
13750
14679
|
|
|
13751
14680
|
if (require.main === module) {
|
|
13752
14681
|
try { ensureGitHooksInstalled(); } catch { /* best-effort */ }
|
|
13753
|
-
const { handleCommand } = require('./engine/cli');
|
|
14682
|
+
const { handleCommand } = require('./engine/operations/cli');
|
|
13754
14683
|
const [cmd, ...args] = process.argv.slice(2);
|
|
13755
14684
|
handleCommand(cmd, args);
|
|
13756
14685
|
}
|