@yemi33/minions 0.1.2448 → 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 +58 -4
- package/dashboard/js/detail-panel.js +36 -0
- package/dashboard/js/memory-panel.js +59 -12
- package/dashboard/js/qa.js +179 -20
- 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 +13 -2
- 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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* engine/shared.js — Shared utilities for Minions engine, dashboard, and LLM modules.
|
|
2
|
+
* engine/core/shared.js — Shared utilities for Minions engine, dashboard, and LLM modules.
|
|
3
3
|
* Extracted from engine.js and dashboard.js to eliminate duplication.
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -11,11 +11,29 @@ const crypto = require('crypto');
|
|
|
11
11
|
// enumeration, cwd-holder detection) live in process-utils.js
|
|
12
12
|
// (P-mrb8yg9x001k1401-b). Re-exported below via `...processUtils` so no
|
|
13
13
|
// caller of require('./shared') needs to change.
|
|
14
|
-
const processUtils = require('
|
|
14
|
+
const processUtils = require('../processes/process-utils');
|
|
15
15
|
|
|
16
|
-
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
16
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..', '..');
|
|
17
17
|
const DEFAULT_MINIONS_HOME = path.join(os.homedir(), '.minions');
|
|
18
|
-
|
|
18
|
+
|
|
19
|
+
function _resolveRootPointerPath() {
|
|
20
|
+
const processRoot = process.env.MINIONS_TEST_PROCESS_ROOT;
|
|
21
|
+
const requested = process.env.MINIONS_TEST_ROOT_POINTER_PATH;
|
|
22
|
+
if (processRoot && requested) {
|
|
23
|
+
try {
|
|
24
|
+
const root = path.resolve(processRoot);
|
|
25
|
+
const target = path.resolve(requested);
|
|
26
|
+
const relative = path.relative(root, target);
|
|
27
|
+
if (fs.existsSync(path.join(root, '.minions-test-process'))
|
|
28
|
+
&& relative && !relative.startsWith('..') && !path.isAbsolute(relative)) {
|
|
29
|
+
return target;
|
|
30
|
+
}
|
|
31
|
+
} catch { /* fall through to the operator home */ }
|
|
32
|
+
}
|
|
33
|
+
return path.join(os.homedir(), '.minions-root');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const ROOT_POINTER_PATH = _resolveRootPointerPath();
|
|
19
37
|
|
|
20
38
|
function isInstalledRoot(dir) {
|
|
21
39
|
if (!dir) return false;
|
|
@@ -40,6 +58,11 @@ function saveMinionsRootPointer(root) {
|
|
|
40
58
|
}
|
|
41
59
|
|
|
42
60
|
function resolveMinionsHome(forInit = false, options = {}) {
|
|
61
|
+
const testHome = process.env.MINIONS_TEST_DIR
|
|
62
|
+
? path.resolve(process.env.MINIONS_TEST_DIR)
|
|
63
|
+
: null;
|
|
64
|
+
if (testHome) return testHome;
|
|
65
|
+
|
|
43
66
|
const envHome = process.env.MINIONS_HOME ? path.resolve(process.env.MINIONS_HOME) : null;
|
|
44
67
|
if (envHome) return envHome;
|
|
45
68
|
|
|
@@ -63,6 +86,40 @@ const MINIONS_DIR = process.env.MINIONS_TEST_DIR || resolveMinionsHome(false, {
|
|
|
63
86
|
preferSourceCheckout: true,
|
|
64
87
|
});
|
|
65
88
|
const ENGINE_DIR = path.join(MINIONS_DIR, 'engine');
|
|
89
|
+
|
|
90
|
+
// W-ms6wdokc00051a14 — Definition directories (playbooks/, prompts/, dashboard/)
|
|
91
|
+
// belong to the runtime root. The ONE exception is the process-isolation
|
|
92
|
+
// baseline runtime root created by test/process-isolation.js: it is seeded with
|
|
93
|
+
// runtime-state directories only, so it opts into a package-root fallback by
|
|
94
|
+
// carrying this marker. The fallback is deliberately scoped to that single
|
|
95
|
+
// directory — a test that swaps in its own MINIONS_TEST_DIR sandbox must stay
|
|
96
|
+
// sandboxed, or "this definition file is absent" assertions would silently read
|
|
97
|
+
// the real repository copy instead.
|
|
98
|
+
const TEST_PROCESS_RUNTIME_MARKER = '.minions-test-process-runtime';
|
|
99
|
+
|
|
100
|
+
function _definitionFallbackRootFor(runtimeRoot) {
|
|
101
|
+
const processRoot = process.env.MINIONS_TEST_PROCESS_ROOT;
|
|
102
|
+
if (!processRoot) return null;
|
|
103
|
+
try {
|
|
104
|
+
if (!fs.existsSync(path.join(path.resolve(processRoot), '.minions-test-process'))) return null;
|
|
105
|
+
if (!fs.existsSync(path.join(runtimeRoot, TEST_PROCESS_RUNTIME_MARKER))) return null;
|
|
106
|
+
} catch { return null; }
|
|
107
|
+
return PACKAGE_ROOT;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function resolveDefinitionDir(name) {
|
|
111
|
+
if (!/^[A-Za-z0-9._-]+$/.test(String(name || ''))) {
|
|
112
|
+
throw new Error(`Invalid definition directory: ${name}`);
|
|
113
|
+
}
|
|
114
|
+
const runtimeDir = path.join(MINIONS_DIR, name);
|
|
115
|
+
if (fs.existsSync(runtimeDir)) return runtimeDir;
|
|
116
|
+
const fallbackRoot = _definitionFallbackRootFor(MINIONS_DIR);
|
|
117
|
+
if (fallbackRoot) {
|
|
118
|
+
const packageDir = path.join(fallbackRoot, name);
|
|
119
|
+
if (fs.existsSync(packageDir)) return packageDir;
|
|
120
|
+
}
|
|
121
|
+
return runtimeDir;
|
|
122
|
+
}
|
|
66
123
|
// W-mr2c4i8m0004da94: Node's built-in `--diagnostic-dir` target for the
|
|
67
124
|
// engine.js process's crash-diagnostics report (see
|
|
68
125
|
// getEngineCrashDiagnosticsEnv / pruneCrashDiagnosticsReports below and
|
|
@@ -150,22 +207,36 @@ function writeDashboardPortFile({ port, pid, minionsHome } = {}) {
|
|
|
150
207
|
boundAt: new Date().toISOString(),
|
|
151
208
|
};
|
|
152
209
|
try {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
210
|
+
withFileLock(`${fp}.lock`, () => {
|
|
211
|
+
fs.mkdirSync(path.dirname(fp), { recursive: true });
|
|
212
|
+
const tmp = fp + '.tmp';
|
|
213
|
+
fs.writeFileSync(tmp, JSON.stringify(payload));
|
|
214
|
+
fs.renameSync(tmp, fp);
|
|
215
|
+
});
|
|
157
216
|
return { ok: true, path: fp };
|
|
158
217
|
} catch (err) {
|
|
159
218
|
return { ok: false, error: err };
|
|
160
219
|
}
|
|
161
220
|
}
|
|
162
221
|
|
|
163
|
-
/** Delete the runtime dashboard-port.json beacon.
|
|
164
|
-
*
|
|
165
|
-
|
|
166
|
-
function clearDashboardPortFile(minionsHome) {
|
|
222
|
+
/** Delete the runtime dashboard-port.json beacon. An expected PID makes stale
|
|
223
|
+
* dashboard shutdowns preserve a newer process's beacon. Best-effort. */
|
|
224
|
+
function clearDashboardPortFile(minionsHome, { expectedPid = null } = {}) {
|
|
167
225
|
const fp = _dashboardPortPath(minionsHome);
|
|
168
|
-
try {
|
|
226
|
+
try {
|
|
227
|
+
return withFileLock(`${fp}.lock`, () => {
|
|
228
|
+
if (Number.isInteger(expectedPid)) {
|
|
229
|
+
const current = readDashboardPortFile(minionsHome);
|
|
230
|
+
if (!current || current.pid !== expectedPid) {
|
|
231
|
+
return { ok: false, reason: 'owner-changed' };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
try { fs.unlinkSync(fp); return { ok: true }; }
|
|
235
|
+
catch { return { ok: false, reason: 'missing' }; }
|
|
236
|
+
});
|
|
237
|
+
} catch (error) {
|
|
238
|
+
return { ok: false, error };
|
|
239
|
+
}
|
|
169
240
|
}
|
|
170
241
|
|
|
171
242
|
/**
|
|
@@ -244,7 +315,7 @@ function nodeSqliteRemediationLine(version = process.versions.node) {
|
|
|
244
315
|
}
|
|
245
316
|
|
|
246
317
|
// ── F6 (P-f6commentedit): Comment-edit dedup helpers ────────────────────────
|
|
247
|
-
// Shared by engine/github.js + engine/ado.js pollPrHumanComments so a comment
|
|
318
|
+
// Shared by engine/providers/github.js + engine/ado/index.js pollPrHumanComments so a comment
|
|
248
319
|
// EDITED after first observation triggers a single re-dispatch (and only one).
|
|
249
320
|
// MANDATORY TZ NORMALIZATION: GH `updated_at` is always UTC (Z-suffix), but
|
|
250
321
|
// ADO `lastUpdatedDate` is inconsistent — sometimes `+00:00`, sometimes naive
|
|
@@ -465,7 +536,7 @@ function log(level, msg, meta = {}) {
|
|
|
465
536
|
// Capture the SQL state.db path too — same write-time semantics so a
|
|
466
537
|
// deferred buffer flush after the test ends still routes the entry to
|
|
467
538
|
// the test's state.db (not the production one). Path resolution mirrors
|
|
468
|
-
// engine/db.js but inlined to avoid the require cycle that loading the
|
|
539
|
+
// engine/db/index.js but inlined to avoid the require cycle that loading the
|
|
469
540
|
// db module here would create on module init.
|
|
470
541
|
Object.defineProperty(entry, '_dbPath', {
|
|
471
542
|
value: _currentLogDbPath(),
|
|
@@ -496,7 +567,7 @@ function _currentLogDbPath() {
|
|
|
496
567
|
if (process.env.MINIONS_TEST_DIR) {
|
|
497
568
|
return path.join(path.resolve(process.env.MINIONS_TEST_DIR), 'engine', 'state.db');
|
|
498
569
|
}
|
|
499
|
-
const root = process.env.MINIONS_HOME ? path.resolve(process.env.MINIONS_HOME) :
|
|
570
|
+
const root = process.env.MINIONS_HOME ? path.resolve(process.env.MINIONS_HOME) : PACKAGE_ROOT;
|
|
500
571
|
return path.join(root, 'engine', 'state.db');
|
|
501
572
|
}
|
|
502
573
|
|
|
@@ -632,7 +703,7 @@ function _flushLogBuffer() {
|
|
|
632
703
|
// state.db files stay isolated; logging must never crash the caller, so
|
|
633
704
|
// appendLogsToDbFile already swallows its own errors.
|
|
634
705
|
try {
|
|
635
|
-
const store = require('
|
|
706
|
+
const store = require('../observability/logs-store');
|
|
636
707
|
for (const [dbPath, entries] of byDbPath) {
|
|
637
708
|
store.appendLogsToDbFile(dbPath, entries);
|
|
638
709
|
}
|
|
@@ -1240,10 +1311,10 @@ function deleteDispatchPromptSidecar(item) {
|
|
|
1240
1311
|
|
|
1241
1312
|
// ── Stop-intent flag (engine/stop-intent.json) ──────────────────────────────
|
|
1242
1313
|
//
|
|
1243
|
-
// File-presence signal used by the external supervisor (engine/supervisor.js)
|
|
1314
|
+
// File-presence signal used by the external supervisor (engine/recovery/supervisor.js)
|
|
1244
1315
|
// to know when a user explicitly asked Minions to be down (`minions stop`,
|
|
1245
1316
|
// `minions uninstall`, `engine.js stop`, mid-`minions restart`). Centralized
|
|
1246
|
-
// here so dashboard.js, bin/minions.js, engine/cli.js, and supervisor.js all
|
|
1317
|
+
// here so dashboard.js, bin/minions.js, engine/operations/cli.js, and supervisor.js all
|
|
1247
1318
|
// honor the same contract:
|
|
1248
1319
|
//
|
|
1249
1320
|
// - Set by every stop/kill code path BEFORE killing.
|
|
@@ -1295,7 +1366,7 @@ const LOCK_STALE_MS = 300000; // 5 minutes — force-remove locks older than thi
|
|
|
1295
1366
|
|
|
1296
1367
|
// P-8a4d6f29 — single helper for detached-process stdio capture with
|
|
1297
1368
|
// rotate-on-open. Used by bin/minions.js (engine + dashboard stdio logs) and
|
|
1298
|
-
// engine/managed-spawn.js openManagedLog. Centralising replaces the previous
|
|
1369
|
+
// engine/processes/managed-spawn.js openManagedLog. Centralising replaces the previous
|
|
1299
1370
|
// _openStdioLog in bin/minions.js so rotation is uniform across every long-
|
|
1300
1371
|
// running log Minions writes.
|
|
1301
1372
|
//
|
|
@@ -1364,7 +1435,7 @@ function openAppendLogFd(name, dir, opts) {
|
|
|
1364
1435
|
// Windows) without installing anything (e.g. Sysinternals ProcDump).
|
|
1365
1436
|
//
|
|
1366
1437
|
// Callers: bin/minions.js (CLI `start`/`restart` spawn) and
|
|
1367
|
-
// engine/supervisor.js (respawn-on-death). Both spawn engine.js as a fresh
|
|
1438
|
+
// engine/recovery/supervisor.js (respawn-on-death). Both spawn engine.js as a fresh
|
|
1368
1439
|
// process before any config is loaded into memory, so this reads config.json
|
|
1369
1440
|
// directly rather than expecting an already-parsed config object — pass the
|
|
1370
1441
|
// parsed object in if the caller already has it (e.g. from its own
|
|
@@ -1392,7 +1463,7 @@ function getEngineCrashDiagnosticsEnv(config, baseEnv) {
|
|
|
1392
1463
|
// mtime, mirroring dashboard.js's `_heapSnapshotPrune` retention pattern so
|
|
1393
1464
|
// a leak-hunt session (or a genuine crash-loop) doesn't fill the disk with
|
|
1394
1465
|
// reports unbounded. Best-effort: missing dir / unreadable stat / failed
|
|
1395
|
-
// unlink are swallowed. Called from engine/cleanup.js#runCleanup (fires
|
|
1466
|
+
// unlink are swallowed. Called from engine/orchestration/cleanup.js#runCleanup (fires
|
|
1396
1467
|
// every ENGINE_DEFAULTS.cleanupEvery ticks, ~10 min at default tick
|
|
1397
1468
|
// interval) so retention happens automatically without a dedicated sweep.
|
|
1398
1469
|
function pruneCrashDiagnosticsReports(config) {
|
|
@@ -1762,9 +1833,19 @@ function mutateJsonFileLocked(filePath, mutateFn, {
|
|
|
1762
1833
|
}
|
|
1763
1834
|
cleanupClaim();
|
|
1764
1835
|
} else {
|
|
1836
|
+
// W-ms811bme000n475a — refresh the `.backup` sidecar AFTER the primary
|
|
1837
|
+
// write, from the content we just committed. safeWrite is atomic
|
|
1838
|
+
// (tmp file + rename), so a pre-write snapshot bought no torn-write
|
|
1839
|
+
// protection; all it did was leave the sidecar one revision stale.
|
|
1840
|
+
// safeJson's restore-on-missing/corrupt path then silently REVERTS
|
|
1841
|
+
// the last committed change and rewrites it to the primary — which is
|
|
1842
|
+
// how a project removed via removeProject() reappeared in config.json
|
|
1843
|
+
// and survived dashboard refreshes, engine ticks, and restart.
|
|
1844
|
+
// Copying after the write also stops a corrupt primary from being
|
|
1845
|
+
// copied over the last known-good backup.
|
|
1765
1846
|
const backupPath = filePath + '.backup';
|
|
1766
|
-
try { if (fileExists) fs.copyFileSync(filePath, backupPath); } catch { /* backup is best-effort */ }
|
|
1767
1847
|
safeWrite(filePath, finalData);
|
|
1848
|
+
try { if (fileExists) fs.copyFileSync(filePath, backupPath); } catch { /* backup is best-effort */ }
|
|
1768
1849
|
}
|
|
1769
1850
|
// Side-effect hook fired only when an actual write happened. Callers
|
|
1770
1851
|
// use this to emit cache-invalidation signals (events table row) so
|
|
@@ -1811,7 +1892,7 @@ function mutateControl(mutator) {
|
|
|
1811
1892
|
|
|
1812
1893
|
// ── Crash-loop detection (W-mr2c46590003e3ee) ──────────────────────────────
|
|
1813
1894
|
// Three independent mechanisms can respawn a dead/frozen engine process:
|
|
1814
|
-
// engine/supervisor.js#checkEngine / #checkEngineHung, engine/watchdog.js
|
|
1895
|
+
// engine/recovery/supervisor.js#checkEngine / #checkEngineHung, engine/recovery/watchdog.js
|
|
1815
1896
|
// (OS-scheduled external recovery), and dashboard.js's in-process 30s
|
|
1816
1897
|
// watchdog. None of them counted or throttled repeated crashes, so a genuine
|
|
1817
1898
|
// crash-loop (engine dying every few minutes) silently self-healed forever
|
|
@@ -1890,7 +1971,7 @@ function recordEngineRespawn(source) {
|
|
|
1890
1971
|
}
|
|
1891
1972
|
|
|
1892
1973
|
function mutateEngineState(mutator) {
|
|
1893
|
-
const store = require('
|
|
1974
|
+
const store = require('../persistence/small-state-store');
|
|
1894
1975
|
const { result } = store.applyEngineStateMutation((data) => {
|
|
1895
1976
|
if (!data || typeof data !== 'object' || Array.isArray(data)) data = {};
|
|
1896
1977
|
return mutator(data) || data;
|
|
@@ -1899,19 +1980,19 @@ function mutateEngineState(mutator) {
|
|
|
1899
1980
|
}
|
|
1900
1981
|
|
|
1901
1982
|
function readEngineState() {
|
|
1902
|
-
const data = require('
|
|
1983
|
+
const data = require('../persistence/small-state-store').readEngineState();
|
|
1903
1984
|
if (!data || typeof data !== 'object' || Array.isArray(data)) return {};
|
|
1904
1985
|
return data;
|
|
1905
1986
|
}
|
|
1906
1987
|
|
|
1907
1988
|
function mutateCooldowns(mutator) {
|
|
1908
|
-
const store = require('
|
|
1989
|
+
const store = require('../persistence/small-state-store');
|
|
1909
1990
|
const { wrote, result } = store.applyCooldownsMutation((data) => {
|
|
1910
1991
|
if (!data || typeof data !== 'object' || Array.isArray(data)) data = {};
|
|
1911
1992
|
return mutator(data) || data;
|
|
1912
1993
|
});
|
|
1913
1994
|
if (wrote) {
|
|
1914
|
-
try { require('
|
|
1995
|
+
try { require('../persistence/db-events').emitStateEvent('cooldowns'); } catch { /* optional */ }
|
|
1915
1996
|
}
|
|
1916
1997
|
return result;
|
|
1917
1998
|
}
|
|
@@ -2229,7 +2310,7 @@ function validateGhSlug(slug) {
|
|
|
2229
2310
|
}
|
|
2230
2311
|
|
|
2231
2312
|
// P-f2-gh-shell (F2): validators for `gh api` endpoint paths and numeric IDs.
|
|
2232
|
-
// Used by engine/github.js to gate argv-form shell-out calls (shellSafeGh)
|
|
2313
|
+
// Used by engine/providers/github.js to gate argv-form shell-out calls (shellSafeGh)
|
|
2233
2314
|
// against poisoned PR JSON that could carry shell metacharacters or non-numeric
|
|
2234
2315
|
// IDs through to child_process. All validators throw a structured Error on
|
|
2235
2316
|
// rejection — callers must let it propagate so the shell-out never happens.
|
|
@@ -2418,7 +2499,7 @@ function shellSafeGh(args, opts = {}) {
|
|
|
2418
2499
|
|
|
2419
2500
|
// W-mr3tayu4 — shared, age-gated stale `.git/index.lock` remover. Previously
|
|
2420
2501
|
// lived only in engine.js and was called from the worktree-mode `git worktree
|
|
2421
|
-
// add` retry paths; it is now shared so engine/live-checkout.js#prepareLiveCheckout
|
|
2502
|
+
// add` retry paths; it is now shared so engine/worktrees/live-checkout.js#prepareLiveCheckout
|
|
2422
2503
|
// (checkoutMode:'live' projects) can self-heal the same failure without a
|
|
2423
2504
|
// circular import. Removes `<rootDir>/.git/index.lock` ONLY when it is older
|
|
2424
2505
|
// than STALE_INDEX_LOCK_MAX_AGE_MS (5 min) — deliberately conservative so a lock
|
|
@@ -2590,11 +2671,117 @@ function cleanChildEnv() {
|
|
|
2590
2671
|
return { ..._cleanEnvCache };
|
|
2591
2672
|
}
|
|
2592
2673
|
|
|
2674
|
+
// Engine-owned defaults also protect PR branches that predate runner safeguards.
|
|
2675
|
+
function applyAgentTestLoadSafetyEnv(env) {
|
|
2676
|
+
if (!env || typeof env !== 'object') return env;
|
|
2677
|
+
env.MINIONS_TEST_HOST_RUNTIME_PRESENT = '1';
|
|
2678
|
+
if (!(Number(env.MINIONS_TEST_CONCURRENCY) > 0)) {
|
|
2679
|
+
env.MINIONS_TEST_CONCURRENCY = '1';
|
|
2680
|
+
}
|
|
2681
|
+
if (!(Number(env.MINIONS_TEST_GLOBAL_CONCURRENCY_CAP) > 0)) {
|
|
2682
|
+
env.MINIONS_TEST_GLOBAL_CONCURRENCY_CAP = '3';
|
|
2683
|
+
}
|
|
2684
|
+
return env;
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2593
2687
|
// Environment for git commands — prevents credential manager from opening browser
|
|
2594
2688
|
function gitEnv() {
|
|
2595
2689
|
return { ...process.env, GIT_TERMINAL_PROMPT: '0', GCM_INTERACTIVE: 'never' };
|
|
2596
2690
|
}
|
|
2597
2691
|
|
|
2692
|
+
// ── Shared remote-ref refresh serialization (W-ms5tb6ha016fda10) ─────────────
|
|
2693
|
+
//
|
|
2694
|
+
// Two dispatches preparing FRESH worktrees in the SAME clone during the same
|
|
2695
|
+
// tick both run `git fetch origin <mainBranch>`, and both target the SAME
|
|
2696
|
+
// `refs/remotes/origin/<mainBranch>`. Git takes a `<ref>.lock` for the update,
|
|
2697
|
+
// so the loser dies with `cannot lock ref 'refs/remotes/origin/main': is at
|
|
2698
|
+
// <sha> but expected <sha>`. That is a purely LOCAL serialization defect: it
|
|
2699
|
+
// has nothing to do with connectivity, and laundering it into
|
|
2700
|
+
// FAILURE_CLASS.NETWORK_ERROR hid the real cause behind a "flaky network"
|
|
2701
|
+
// story while burning dispatch retries.
|
|
2702
|
+
//
|
|
2703
|
+
// Two complementary mitigations live here:
|
|
2704
|
+
//
|
|
2705
|
+
// 1. `withSerializedRemoteRefRefresh` — an in-process, per-repository async
|
|
2706
|
+
// mutex. The engine daemon is the process that actually issues the
|
|
2707
|
+
// concurrent fresh-worktree fetches, so chaining them per resolved repo
|
|
2708
|
+
// path removes the race at its source. It is deliberately NOT a file
|
|
2709
|
+
// lock: a file lock here would be held across a network round-trip,
|
|
2710
|
+
// which the concurrency rules in CLAUDE.md forbid. The chain is keyed on
|
|
2711
|
+
// the repo, so unrelated projects still fetch in parallel and no
|
|
2712
|
+
// unrelated work is serialized.
|
|
2713
|
+
//
|
|
2714
|
+
// 2. `isGitRefLockError` — classification for the residual cross-process
|
|
2715
|
+
// case (an operator's own `git fetch`, the dashboard, or the CLI racing
|
|
2716
|
+
// the daemon), which maps to the retryable FAILURE_CLASS.GIT_REF_LOCK.
|
|
2717
|
+
const GIT_REF_LOCK_PATTERNS = [
|
|
2718
|
+
/cannot lock ref/i,
|
|
2719
|
+
/unable to lock ref/i,
|
|
2720
|
+
/cannot lock the ref/i,
|
|
2721
|
+
/unable to update local ref/i,
|
|
2722
|
+
/unable to create '[^']*\.lock'/i,
|
|
2723
|
+
/ref\b[^\n]*\bis at\b[^\n]*\bbut expected\b/i,
|
|
2724
|
+
];
|
|
2725
|
+
|
|
2726
|
+
/**
|
|
2727
|
+
* True when a git failure is a LOCAL ref-lock contention loss rather than a
|
|
2728
|
+
* network/auth failure. Accepts an Error, a string, or anything stringifiable;
|
|
2729
|
+
* inspects `stderr` and `stdout` too because `child_process.exec` rejections
|
|
2730
|
+
* put the actionable git text there rather than in `message`.
|
|
2731
|
+
*/
|
|
2732
|
+
function isGitRefLockError(err) {
|
|
2733
|
+
if (!err) return false;
|
|
2734
|
+
const parts = [];
|
|
2735
|
+
if (typeof err === 'string') parts.push(err);
|
|
2736
|
+
else {
|
|
2737
|
+
if (err.message) parts.push(String(err.message));
|
|
2738
|
+
if (err.stderr) parts.push(String(err.stderr));
|
|
2739
|
+
if (err.stdout) parts.push(String(err.stdout));
|
|
2740
|
+
if (parts.length === 0) parts.push(String(err));
|
|
2741
|
+
}
|
|
2742
|
+
const text = parts.join('\n');
|
|
2743
|
+
return GIT_REF_LOCK_PATTERNS.some(re => re.test(text));
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
// resolved repo key -> tail promise of the currently queued refresh chain
|
|
2747
|
+
const _remoteRefRefreshChains = new Map();
|
|
2748
|
+
|
|
2749
|
+
/**
|
|
2750
|
+
* Serialize `fn` against every other remote-ref refresh for the same
|
|
2751
|
+
* repository within this process. Returns whatever `fn` resolves/rejects with
|
|
2752
|
+
* — the queue never swallows errors, and a rejected refresh does not poison
|
|
2753
|
+
* the chain for the next caller.
|
|
2754
|
+
*
|
|
2755
|
+
* Keep `fn` scoped to the ref refresh itself. Do NOT wrap worktree creation,
|
|
2756
|
+
* checkout, or merge work in it: those are unrelated expensive operations and
|
|
2757
|
+
* serializing them would convert a narrow correctness fix into a throughput
|
|
2758
|
+
* regression.
|
|
2759
|
+
*/
|
|
2760
|
+
function withSerializedRemoteRefRefresh(repoKey, fn) {
|
|
2761
|
+
if (typeof fn !== 'function') {
|
|
2762
|
+
return Promise.reject(new TypeError('withSerializedRemoteRefRefresh requires a function'));
|
|
2763
|
+
}
|
|
2764
|
+
let key;
|
|
2765
|
+
try { key = _normalizeWorktreePath(path.resolve(String(repoKey || ''))); }
|
|
2766
|
+
catch { key = null; }
|
|
2767
|
+
if (!key) key = '<no-repo>';
|
|
2768
|
+
const previous = _remoteRefRefreshChains.get(key) || Promise.resolve();
|
|
2769
|
+
// `previous` is already error-swallowed (see `tail`), but guard both settle
|
|
2770
|
+
// paths so a future refactor cannot deadlock the chain on a rejection.
|
|
2771
|
+
const run = previous.then(() => fn(), () => fn());
|
|
2772
|
+
const tail = run.then(() => {}, () => {});
|
|
2773
|
+
_remoteRefRefreshChains.set(key, tail);
|
|
2774
|
+
tail.then(() => {
|
|
2775
|
+
if (_remoteRefRefreshChains.get(key) === tail) _remoteRefRefreshChains.delete(key);
|
|
2776
|
+
});
|
|
2777
|
+
return run;
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
/** Test-only: drop every queued refresh chain. */
|
|
2781
|
+
function _resetRemoteRefRefreshChainsForTesting() {
|
|
2782
|
+
_remoteRefRefreshChains.clear();
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2598
2785
|
// ── Stream-JSON Output Parsing (runtime-aware delegator) ────────────────────
|
|
2599
2786
|
|
|
2600
2787
|
/**
|
|
@@ -2607,7 +2794,7 @@ function gitEnv() {
|
|
|
2607
2794
|
*
|
|
2608
2795
|
* Signatures supported:
|
|
2609
2796
|
* parseStreamJsonOutput(raw)
|
|
2610
|
-
* parseStreamJsonOutput(raw, optsObj) ← legacy form (engine/llm.js still uses this)
|
|
2797
|
+
* parseStreamJsonOutput(raw, optsObj) ← legacy form (engine/agents/llm.js still uses this)
|
|
2611
2798
|
* parseStreamJsonOutput(raw, runtimeName)
|
|
2612
2799
|
* parseStreamJsonOutput(raw, runtimeName, optsObj)
|
|
2613
2800
|
*
|
|
@@ -2622,7 +2809,7 @@ function parseStreamJsonOutput(raw, runtimeName, opts) {
|
|
|
2622
2809
|
}
|
|
2623
2810
|
// Lazy require to avoid a circular dep at module init (runtimes/claude.js
|
|
2624
2811
|
// doesn't import shared, but downstream adapters might).
|
|
2625
|
-
const { resolveRuntime } = require('
|
|
2812
|
+
const { resolveRuntime } = require('../runtimes');
|
|
2626
2813
|
return resolveRuntime(runtimeName).parseOutput(raw, opts || {});
|
|
2627
2814
|
}
|
|
2628
2815
|
|
|
@@ -2702,7 +2889,7 @@ const KB_CATEGORIES = ['architecture', 'conventions', 'project-notes', 'build-re
|
|
|
2702
2889
|
// authoring populate but that aren't destinations for inbox classification:
|
|
2703
2890
|
//
|
|
2704
2891
|
// agents — per-agent personal memory (knowledge/agents/<id>.md),
|
|
2705
|
-
// loaded by engine/consolidation.js + engine/playbook.js.
|
|
2892
|
+
// loaded by engine/memory/consolidation.js + engine/agents/playbook.js.
|
|
2706
2893
|
// consolidated /
|
|
2707
2894
|
// consolidation /
|
|
2708
2895
|
// consolidations — three historical names the consolidation pipeline has
|
|
@@ -2740,7 +2927,7 @@ const KB_READABLE_CATEGORIES = Object.freeze([
|
|
|
2740
2927
|
* duplicate-titled knowledge/project-notes/*.md entries on a single day) and
|
|
2741
2928
|
* the sibling W-mr3lokxs fix for live-checkout-dirty alerts.
|
|
2742
2929
|
*
|
|
2743
|
-
* `engine/dispatch.js#writeInboxAlert` names every file `engine-alert-<slug>-
|
|
2930
|
+
* `engine/orchestration/dispatch.js#writeInboxAlert` names every file `engine-alert-<slug>-
|
|
2744
2931
|
* <date>.md`, so the filename prefix is one signal. `shared.js#_writeWorktree
|
|
2745
2932
|
* SkipLiveInboxNote` (and any other engine-authored note) stamps frontmatter
|
|
2746
2933
|
* `agent: engine`, accepted as a second signal so alerts written under a
|
|
@@ -2980,7 +3167,7 @@ function isDispatchExecutionPathSafe(item, config) {
|
|
|
2980
3167
|
// otherwise `false`. Only an explicit boolean counts as "set" at either level —
|
|
2981
3168
|
// anything else (undefined / null / string) is treated as unset so the next
|
|
2982
3169
|
// tier (or the `false` default) decides. When ON and a live-checkout tree is
|
|
2983
|
-
// dirty, `engine/live-checkout.js#prepareLiveCheckout` runs
|
|
3170
|
+
// dirty, `engine/worktrees/live-checkout.js#prepareLiveCheckout` runs
|
|
2984
3171
|
// `git reset --hard HEAD` + `git clean -fd` instead of refusing with
|
|
2985
3172
|
// LIVE_CHECKOUT_DIRTY. This discards uncommitted tracked and untracked work but
|
|
2986
3173
|
// preserves the current branch and committed history, so it is opt-in. Sibling
|
|
@@ -3116,6 +3303,39 @@ function resolveCodexCcSandbox(engine) {
|
|
|
3116
3303
|
return validateCodexSandbox(configured);
|
|
3117
3304
|
}
|
|
3118
3305
|
|
|
3306
|
+
// W-ms87lj8j018j2572 — SINGLE SOURCE OF TRUTH for the quarantine recovery
|
|
3307
|
+
// window. Quarantine recovery REFS (`refs/minions/quarantine/*` and
|
|
3308
|
+
// `refs/minions/quarantine-wip/*`, pruned by
|
|
3309
|
+
// `engine/worktrees/quarantine-refs.js`) and PROTECTED quarantine DIRECTORIES
|
|
3310
|
+
// (swept by `engine/worktrees/gc.js` at
|
|
3311
|
+
// `ENGINE_DEFAULTS.worktreeProtectedQuarantineRetentionMs`) govern the SAME
|
|
3312
|
+
// operator recovery story, so they MUST expire together. They previously drifted
|
|
3313
|
+
// (10d vs 30d) because each side owned its own literal. The constant lives here —
|
|
3314
|
+
// the lowest dependency-free layer — because `engine/worktrees/quarantine-refs.js`
|
|
3315
|
+
// already `require`s this module, so shared.js cannot import from it at load time.
|
|
3316
|
+
// Consumers must reference these bindings; never re-derive the number.
|
|
3317
|
+
const QUARANTINE_REF_RETENTION_DAYS = 30;
|
|
3318
|
+
const QUARANTINE_REF_RETENTION_MS = QUARANTINE_REF_RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
|
3319
|
+
|
|
3320
|
+
/**
|
|
3321
|
+
* Resolve the operator-visible quarantine recovery window in ms.
|
|
3322
|
+
*
|
|
3323
|
+
* W-ms87rbe601a66d13 — aligning the two DEFAULTS (above) is only half the
|
|
3324
|
+
* story: `engine.worktreeProtectedQuarantineRetentionMs` is an operator knob,
|
|
3325
|
+
* and until both sweeps read it the directory sweep alone honored an override
|
|
3326
|
+
* while the REF sweep stayed pinned to the built-in constant. That reintroduces
|
|
3327
|
+
* the exact divergence at runtime that the shared constant removes at build
|
|
3328
|
+
* time. This resolver is the single seam both sweeps go through, so lowering,
|
|
3329
|
+
* raising, or disabling retention always moves them together.
|
|
3330
|
+
*
|
|
3331
|
+
* A non-positive value means INDEFINITE retention on both sides (the documented
|
|
3332
|
+
* `0` opt-out) — never "everything is already expired".
|
|
3333
|
+
*/
|
|
3334
|
+
function resolveQuarantineRetentionMs(config) {
|
|
3335
|
+
const configured = Number(config?.engine?.worktreeProtectedQuarantineRetentionMs);
|
|
3336
|
+
return Number.isFinite(configured) ? Math.max(0, configured) : QUARANTINE_REF_RETENTION_MS;
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3119
3339
|
const ENGINE_DEFAULTS = {
|
|
3120
3340
|
// W-mpxpckey000laa4f: tick interval lowered 60s → 10s so the shipped default
|
|
3121
3341
|
// matches what operators already run at (live boxes have had `tickInterval:
|
|
@@ -3165,7 +3385,7 @@ const ENGINE_DEFAULTS = {
|
|
|
3165
3385
|
// [1000, 120000] in dashboard.js POST /api/settings.
|
|
3166
3386
|
assertCleanStatusTimeoutMs: 10000,
|
|
3167
3387
|
// W-mqpx0tpi — cold-start health budget for `minions restart` / post-update
|
|
3168
|
-
// restart verification (engine/restart-health.js#waitForRestartHealth). The
|
|
3388
|
+
// restart verification (engine/recovery/restart-health.js#waitForRestartHealth). The
|
|
3169
3389
|
// dashboard runs node:sqlite migrations + SPA HTML assembly + initial state
|
|
3170
3390
|
// reads SYNCHRONOUSLY before server.listen(), so right after `npm install`
|
|
3171
3391
|
// on a slow/loaded box the PID is alive but port 7331 is unbound well past
|
|
@@ -3190,6 +3410,16 @@ const ENGINE_DEFAULTS = {
|
|
|
3190
3410
|
decomposeZeroChildrenStaleMinutes: 30,
|
|
3191
3411
|
autoApprovePlans: false, // auto-approve PRDs without waiting for human approval
|
|
3192
3412
|
autoArchive: false, // opt-in: auto-archive plans after verify completes (false = mark ready, user archives manually)
|
|
3413
|
+
// W-msal3ser01q4ccb3 — opt-in: after a PRD's verify work item(s) complete
|
|
3414
|
+
// SUCCESSFULLY and the canonical prd/guides/verify-<plan>.md has been
|
|
3415
|
+
// persisted, auto-create a PRD-driven QA Session (mode 'auto', so
|
|
3416
|
+
// SETUP→DRAFT→EXECUTE runs without approval) through the SAME
|
|
3417
|
+
// buildQaSessionSpecFromPrd + createSession + queueSetup path that backs
|
|
3418
|
+
// POST /api/qa/from-prd. Deliberately NOT keyed off the PRD's top-level
|
|
3419
|
+
// `status: completed` flip — that happens before verification finishes.
|
|
3420
|
+
// Resolved via shared.resolveAutoQaCompletedPrds(engine). Default false =
|
|
3421
|
+
// behavior-identical to today.
|
|
3422
|
+
autoQaCompletedPrds: false,
|
|
3193
3423
|
autoFixConflicts: true, // auto-dispatch fix agents when a PR has merge conflicts
|
|
3194
3424
|
autoFixBuilds: true, // auto-dispatch fix agents when a PR build fails
|
|
3195
3425
|
autoReviewPrs: true, // auto-dispatch review agents for newly opened agent PRs
|
|
@@ -3222,7 +3452,7 @@ const ENGINE_DEFAULTS = {
|
|
|
3222
3452
|
// to before this knob existed. Flip via Dashboard → Settings → Auto-fix
|
|
3223
3453
|
// & Review Loop, or set `engine.autoFixPaused: true` in config.json.
|
|
3224
3454
|
autoFixPaused: false, // hard-stop kill-switch / master override (see comment above)
|
|
3225
|
-
autoConsolidateMemory: true, // default-on: periodically spawn engine/kb-sweep-runner.js from the tick loop (4h cadence). Explicit false disables it; inbox→notes consolidation is independent.
|
|
3455
|
+
autoConsolidateMemory: true, // default-on: periodically spawn engine/memory/kb-sweep-runner.js from the tick loop (4h cadence). Explicit false disables it; inbox→notes consolidation is independent.
|
|
3226
3456
|
// W-mrktrokd — batteries-included default schedules. When true (default),
|
|
3227
3457
|
// adding a project auto-creates its per-project git-fetch-freshness schedule
|
|
3228
3458
|
// (`keep-branch-fresh-git-fetch-origin-<slug>`, cron `0 */12 *`, explore) and
|
|
@@ -3232,7 +3462,7 @@ const ENGINE_DEFAULTS = {
|
|
|
3232
3462
|
// Bootstrap is idempotent + non-destructive — it never duplicates or
|
|
3233
3463
|
// overwrites a schedule an operator has already created/customized (matched
|
|
3234
3464
|
// by schedule id). Set false to opt every install out. See
|
|
3235
|
-
// engine/schedule-bootstrap.js and Dashboard → Settings → Workflow Defaults.
|
|
3465
|
+
// engine/orchestration/schedule-bootstrap.js and Dashboard → Settings → Workflow Defaults.
|
|
3236
3466
|
bootstrapDefaultSchedules: true,
|
|
3237
3467
|
// W-mr48yth5 — per-category age-based TTL (in days) for the KB sweep's expiry
|
|
3238
3468
|
// pass. During runKbSweep, an entry in a listed category whose authored date
|
|
@@ -3361,8 +3591,8 @@ const ENGINE_DEFAULTS = {
|
|
|
3361
3591
|
evalMaxCost: null, // USD ceiling per work item across all eval iterations; null = no limit (gather baseline data first)
|
|
3362
3592
|
maxRetries: 3, // max dispatch retries before marking work item as failed
|
|
3363
3593
|
maxRetriesPerAgent: 2, // W-mpmwxn1j — per-agent retry cap. When the SAME agent fails the same WI this many times, the next retry MUST reassign to a different eligible agent (consults routing.md + agent availability). Falls back to the same agent only when no alternate is available. Counted separately from `maxRetries` (which caps total retries across all agents) and tracked on the WI as `_retriesByAgent: { agentId: count }`. Hard-pinned agents bypass reassignment (operator intent wins).
|
|
3364
|
-
maxPhantomRetries: 3, // max retries for "phantom completion" (runtime crashed before emitting type:"result"); tracked separately from _retryCount so phantom retries don't pollute the normal PR-attachment retry budget. See engine/lifecycle.markMissingPrAttachment + detectNonTerminalResultSummary.
|
|
3365
|
-
maxReviewVerdictMissingRetries: 2, // W-mr9sy3j700076967 — bounded retry budget for a review dispatch that completes ("success") without posting a VERDICT: comment. Below the budget, engine/lifecycle.updatePrAfterReview leaves `lastReviewedAt` unset so the normal needsReview gate re-dispatches a fresh review; at/above budget it stamps `lastReviewedAt` (stops auto-retry) but keeps `_reviewVerdictMissing: true` so the PR is dashboard-flagged for a human instead of looking like an ordinary still-pending review.
|
|
3594
|
+
maxPhantomRetries: 3, // max retries for "phantom completion" (runtime crashed before emitting type:"result"); tracked separately from _retryCount so phantom retries don't pollute the normal PR-attachment retry budget. See engine/orchestration/lifecycle.markMissingPrAttachment + detectNonTerminalResultSummary.
|
|
3595
|
+
maxReviewVerdictMissingRetries: 2, // W-mr9sy3j700076967 — bounded retry budget for a review dispatch that completes ("success") without posting a VERDICT: comment. Below the budget, engine/orchestration/lifecycle.updatePrAfterReview leaves `lastReviewedAt` unset so the normal needsReview gate re-dispatches a fresh review; at/above budget it stamps `lastReviewedAt` (stops auto-retry) but keeps `_reviewVerdictMissing: true` so the PR is dashboard-flagged for a human instead of looking like an ordinary still-pending review.
|
|
3366
3596
|
minRetryGapMs: 120000, // 2min — minimum gap between retry dispatches for the same work item; prevents tight retry loops when an idempotent agent (e.g. review bailing out on a duplicate) cannot produce the expected output (#1770)
|
|
3367
3597
|
pipelineApiRetries: 2, // max attempts for pipeline API calls
|
|
3368
3598
|
pipelineApiRetryDelay: 2000, // ms delay between pipeline API retries
|
|
@@ -3446,7 +3676,7 @@ const ENGINE_DEFAULTS = {
|
|
|
3446
3676
|
memoryBaselineEveryTicks: 6,
|
|
3447
3677
|
// W-mr2c4i8m0004da94: proactive crash diagnostics for the engine.js
|
|
3448
3678
|
// node.exe process. When enabled (default), every spawn of engine.js
|
|
3449
|
-
// (bin/minions.js `start`/`restart` AND engine/supervisor.js respawn-on-
|
|
3679
|
+
// (bin/minions.js `start`/`restart` AND engine/recovery/supervisor.js respawn-on-
|
|
3450
3680
|
// death) gets NODE_OPTIONS += `--report-on-fatalerror --report-on-signal
|
|
3451
3681
|
// --diagnostic-dir=<CRASH_REPORTS_DIR>` so a fatal V8 error (OOM, stack
|
|
3452
3682
|
// overflow, native-addon crash) or an operator-sent signal
|
|
@@ -3466,6 +3696,19 @@ const ENGINE_DEFAULTS = {
|
|
|
3466
3696
|
// → no flip), so the constant is GitHub-only today but lives in shared defaults so a future
|
|
3467
3697
|
// ADO change can adopt the same semantics.
|
|
3468
3698
|
prAbandonConfirmCount: 3,
|
|
3699
|
+
// W-msav5edk00g81710: bounded re-probe window for `abandoned` PR records. A PR can
|
|
3700
|
+
// legitimately leave `abandoned` — GitHub and ADO both allow reopening a closed PR —
|
|
3701
|
+
// but `PR_POLLABLE_STATUSES` excludes terminal statuses, so before this constant the
|
|
3702
|
+
// abandoned stamp was a one-way door and the poller could never observe the reopen
|
|
3703
|
+
// (incident: github:opg-microsoft/minions#1154, closed 16:07:07Z and reopened
|
|
3704
|
+
// 16:10:07Z, stuck `abandoned` for hours while OPEN and one hourly-sweep run away
|
|
3705
|
+
// from a permanent `userDeleted` tombstone). `isPrPollable` keeps an abandoned record
|
|
3706
|
+
// in the STATUS poll for this long after it was abandoned, then drops it again. The
|
|
3707
|
+
// window is what keeps the cost bounded: re-probing every terminal record forever
|
|
3708
|
+
// would burn API quota linearly in tracker size. Records with no abandonment anchor
|
|
3709
|
+
// fail CLOSED (never re-probed), and a host-confirmed deletion
|
|
3710
|
+
// (`_reconciliation404Confirmed`) closes the window early. `0` disables re-probing.
|
|
3711
|
+
abandonedPrRepollWindowMs: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
3469
3712
|
// W-mp60tw0u000j3931: One-shot startup reconciliation pass for `abandoned` PRs runs
|
|
3470
3713
|
// exactly once per bump of this constant. The engine compares this value against
|
|
3471
3714
|
// the persisted lastAbandonedReconciliationVersion at boot; if the version
|
|
@@ -3474,24 +3717,43 @@ const ENGINE_DEFAULTS = {
|
|
|
3474
3717
|
// hardening in W-mp5trwh60008386d shipped) are flipped back to active/merged/closed
|
|
3475
3718
|
// based on their live API state. Bump this when reconciliation logic itself changes
|
|
3476
3719
|
// and we want it to re-run once on every install.
|
|
3477
|
-
|
|
3720
|
+
//
|
|
3721
|
+
// Bumped to 2 by W-msav5edk00g81710: that WI changed abandonment bookkeeping
|
|
3722
|
+
// (records now carry an `_abandonedAt` anchor) and added the bounded re-probe
|
|
3723
|
+
// window. Records abandoned BEFORE the bump have no anchor beyond `closedAt`,
|
|
3724
|
+
// and anything older than the window is out of the poller's reach entirely —
|
|
3725
|
+
// this boot pass is their one-off repair, including the #1154 incident record.
|
|
3726
|
+
abandonedReconciliationVersion: 2,
|
|
3478
3727
|
watchesIncludeBehindBy: false, // opt-in: when true, GitHub PR poll calls /compare/{base}...{head} once per pr per pollPrStatusEvery cadence to populate pr.behindBy (powers the `behind-master` watch predicate). Off by default to avoid the extra API call. ADO PRs always get null (no commit-graph walk yet).
|
|
3479
3728
|
autoCompletePrs: false, // auto-merge PRs when builds green + review approved (opt-in)
|
|
3729
|
+
// W-ms7kklp8002zad24 — ADO-only: proactively queue the required PR-validation
|
|
3730
|
+
// (branch-policy) builds for DRAFT pull requests. On Azure DevOps, branch build
|
|
3731
|
+
// policies do NOT run automatically while a PR is a draft, so a Minions-managed
|
|
3732
|
+
// draft PR never gets a build status and the build-fix loop has nothing to act
|
|
3733
|
+
// on. When ON (default), pollPrStatus discovers the required Build policies from
|
|
3734
|
+
// the target branch's policy configurations and queues a fresh build per distinct
|
|
3735
|
+
// definition against refs/pull/<n>/merge (rate-limited via _lastBuildRequeueAt).
|
|
3736
|
+
// GitHub already fires CI on drafts, so this is inert for GitHub PRs.
|
|
3737
|
+
adoQueueDraftPrBuilds: true,
|
|
3480
3738
|
prMergeMethod: 'squash', // merge method: squash, merge, rebase
|
|
3481
3739
|
ignoredCommentAuthors: [], // comments from these authors are auto-closed and never trigger fixes
|
|
3482
3740
|
// P-f23classifier (F3, security note): ADO Minions identity allow-list for
|
|
3483
|
-
// the marker spoof guard in engine/ado.js pollPrHumanComments. ADO has no
|
|
3741
|
+
// the marker spoof guard in engine/ado/index.js pollPrHumanComments. ADO has no
|
|
3484
3742
|
// viewerDidAuthor equivalent, so a human pasting the <!-- minions:agent=... -->
|
|
3485
3743
|
// marker would otherwise be misclassified as agent-authored. The poller
|
|
3486
3744
|
// combines marker-presence with comment.author.{uniqueName|displayName|id|
|
|
3487
3745
|
// descriptor} matching one of these (case-insensitive) before treating a
|
|
3488
|
-
// body as agent-authored.
|
|
3489
|
-
//
|
|
3490
|
-
//
|
|
3746
|
+
// body as agent-authored.
|
|
3747
|
+
// W-msbf1kf6033f46cb: an empty array (default) means the identity signal is
|
|
3748
|
+
// UNAVAILABLE, not mismatched, so the marker alone classifies as agent
|
|
3749
|
+
// (fail-safe) and the poller logs a one-time warning that the spoof guard is
|
|
3750
|
+
// inactive. Populating this list re-enables the cross-check. The previous
|
|
3751
|
+
// fail-OPEN reading gave the marker zero filtering power on ADO and let every
|
|
3752
|
+
// agent fix-summary re-trigger a fix dispatch on the same PR.
|
|
3491
3753
|
minionsAdoIdentities: [],
|
|
3492
3754
|
botCommentLogins: [], // P-a3f9b2c1: opt-in shared-minions GH login list — comments from these logins are suppressed ONLY when body matches positive-signal markers (Verification SUCCESS / VERDICT:APPROVE / noop:true). Narrower than ignoredCommentAuthors which suppresses all comments by login.
|
|
3493
3755
|
// W-mp76pw7a001da7c1 — Per-slug GitHub PAT routing. Map of `<owner>` (or `<owner>/*`,
|
|
3494
|
-
// or `*` for fleet default) to a `gh auth` account name. `engine/gh-token.js`
|
|
3756
|
+
// or `*` for fleet default) to a `gh auth` account name. `engine/providers/gh-token.js`
|
|
3495
3757
|
// resolves the right token via `gh auth token --user <account> --hostname github.com`
|
|
3496
3758
|
// and threads it as `GH_TOKEN` for that one shell-out, so the engine never depends
|
|
3497
3759
|
// on which gh account is globally active. Empty `{}` (default) preserves legacy
|
|
@@ -3500,7 +3762,7 @@ const ENGINE_DEFAULTS = {
|
|
|
3500
3762
|
ghAccounts: {},
|
|
3501
3763
|
agentBusyReassignMs: 600000, // 10min — reassign work item to another agent if preferred agent is busy beyond this threshold
|
|
3502
3764
|
ccEffort: null, // effort level for CC/doc-chat (null, 'low', 'medium', 'high')
|
|
3503
|
-
enablePreDispatchEval: true, // P-d2a9f6e5: cheap LLM gate before queueing — on by default. See engine/pre-dispatch-eval.js (Ripley §3 recommendation, 2026-05-11 architecture review). Validates from acceptance_criteria when present, falls back to description when criteria are absent but description is rich (≥80 chars). Fail-open on any validator error.
|
|
3765
|
+
enablePreDispatchEval: true, // P-d2a9f6e5: cheap LLM gate before queueing — on by default. See engine/orchestration/pre-dispatch-eval.js (Ripley §3 recommendation, 2026-05-11 architecture review). Validates from acceptance_criteria when present, falls back to description when criteria are absent but description is rich (≥80 chars). Fail-open on any validator error.
|
|
3504
3766
|
// W-mq9acoo800177bcb — bounded-concurrency for the pre-dispatch validator. The
|
|
3505
3767
|
// discoverWork loop in engine.js used to run validateAcceptanceCriteria
|
|
3506
3768
|
// sequentially, costing ~25-30s per item × N items per tick (an 11-item
|
|
@@ -3533,7 +3795,7 @@ const ENGINE_DEFAULTS = {
|
|
|
3533
3795
|
preDispatchEvalMaxRejections: 5,
|
|
3534
3796
|
completionNonceRequired: false, // P-d2a8f6c1 (agent trust boundary F8): when true, a missing `nonce` field in the completion JSON hard-fails the dispatch with failure_class:'completion-nonce-mismatch'. Default false for one release so older agents/runtime caches that haven't picked up the prompt change degrade with a warning instead of breaking. Mismatched nonces hard-fail regardless of this flag. See docs/completion-reports.md → "Trust boundary".
|
|
3535
3797
|
autoApplyReviewVote: false, // Master gate for platform vote mutations; local verdicts remain informational when false.
|
|
3536
|
-
createPrsAsDraft: false, // W-mrtdy7xv — when true, the playbook PR-creation guidance (engine/playbook.js#getPrCreateInstructions) tells agents to open normal Minions-created PRs as DRAFT (GitHub `gh pr create --draft`; ADO `az repos pr create --draft true`). Default false = behave exactly as today. Per-project-overridable via `project.createPrsAsDraft` (explicit boolean), resolved through `shared.resolveCreatePrsAsDraft(project, engine)` — a project can opt IN or OUT independent of this fleet-wide default. Aggregate/E2E verify PRs already open as draft via verify.md regardless of this flag; the engine-opened shared-branch aggregate PR is outside the playbook seam and unaffected.
|
|
3798
|
+
createPrsAsDraft: false, // W-mrtdy7xv — when true, the playbook PR-creation guidance (engine/agents/playbook.js#getPrCreateInstructions) tells agents to open normal Minions-created PRs as DRAFT (GitHub `gh pr create --draft`; ADO `az repos pr create --draft true`). Default false = behave exactly as today. Per-project-overridable via `project.createPrsAsDraft` (explicit boolean), resolved through `shared.resolveCreatePrsAsDraft(project, engine)` — a project can opt IN or OUT independent of this fleet-wide default. Aggregate/E2E verify PRs already open as draft via verify.md regardless of this flag; the engine-opened shared-branch aggregate PR is outside the playbook seam and unaffected.
|
|
3537
3799
|
|
|
3538
3800
|
// ── Runtime fleet (P-3b8e5f1d) ──────────────────────────────────────────────
|
|
3539
3801
|
// Single source of truth for which CLI runtime + model every spawn uses.
|
|
@@ -3556,13 +3818,13 @@ const ENGINE_DEFAULTS = {
|
|
|
3556
3818
|
// projects.<worktreePath>.enabledMcpjsonServers (the field Claude Code actually reads — see live ~/.claude.json
|
|
3557
3819
|
// shape) means the workspace MCPs are connected on first call. Best-effort: failures NEVER block dispatch.
|
|
3558
3820
|
// Engine-scoped per the CLAUDE.md rule against `runtime.name === ...` branches at call sites — the runtime check
|
|
3559
|
-
// lives inside the helper (engine/spawn-agent.js#preApproveWorkspaceMcps) which no-ops for non-Claude runtimes.
|
|
3821
|
+
// lives inside the helper (engine/agents/spawn-agent.js#preApproveWorkspaceMcps) which no-ops for non-Claude runtimes.
|
|
3560
3822
|
claudePreApproveWorkspaceMcps: true,
|
|
3561
3823
|
// W-mrdon0pe000l045a — propagate repo-authored CLAUDE.md instructions to
|
|
3562
3824
|
// runtimes that do NOT auto-discover CLAUDE.md natively (Copilot, Codex).
|
|
3563
3825
|
// When TRUE (default), the engine bounded-discovers the nearest applicable
|
|
3564
3826
|
// CLAUDE.md files for a dispatch and injects them as a "Project instructions
|
|
3565
|
-
// (CLAUDE.md)" context layer (engine/playbook.js → engine/claude-md-context.js).
|
|
3827
|
+
// (CLAUDE.md)" context layer (engine/agents/playbook.js → engine/agents/claude-md-context.js).
|
|
3566
3828
|
// Claude is always skipped — its own CLI already reads CLAUDE.md, so injecting
|
|
3567
3829
|
// it again would double the instructions. CLAUDE.md carries no split-brain
|
|
3568
3830
|
// risk for Copilot/Codex because those runtimes never read it themselves, so
|
|
@@ -3573,12 +3835,12 @@ const ENGINE_DEFAULTS = {
|
|
|
3573
3835
|
propagateClaudeMdForNonClaudeRuntimes: true,
|
|
3574
3836
|
// W-mrtdmtwq000kf391 — surface the repo HARNESS assets in effect for a
|
|
3575
3837
|
// dispatch (runtime + project) in the Minions-authored PR body. When TRUE
|
|
3576
|
-
// (default), engine/playbook.js injects a PR-body appendix instructing the
|
|
3838
|
+
// (default), engine/agents/playbook.js injects a PR-body appendix instructing the
|
|
3577
3839
|
// agent to append a fenced "## Repo harnesses used" section listing the
|
|
3578
3840
|
// dispatch's runtime, injected CLAUDE.md files, AGENTS.md discovery, MCP
|
|
3579
3841
|
// servers, and skills. Pure reporting via the read-only harness-diagnostics
|
|
3580
|
-
// source (engine/harness-context.js → engine/preflight._runtimeHarnessRows +
|
|
3581
|
-
// engine/claude-md-context.js); it never propagates or attests any asset.
|
|
3842
|
+
// source (engine/agents/harness-context.js → engine/agents/preflight._runtimeHarnessRows +
|
|
3843
|
+
// engine/agents/claude-md-context.js); it never propagates or attests any asset.
|
|
3582
3844
|
includeHarnessSectionInPrBody: true,
|
|
3583
3845
|
copilotStreamMode: 'on', // Copilot --stream <on|off>: 'on' streams assistant.message_delta events live; 'off' batches them
|
|
3584
3846
|
copilotReasoningSummaries: false, // Copilot --enable-reasoning-summaries (Anthropic-family models only)
|
|
@@ -3586,7 +3848,7 @@ const ENGINE_DEFAULTS = {
|
|
|
3586
3848
|
// P-9b3d5f61 (Pool copilot agent dispatches through a shared ACP worker pool):
|
|
3587
3849
|
// when true AND the resolved runtime declares `capabilities.acpWorkerPool`,
|
|
3588
3850
|
// engine.js spawnAgent routes per-agent dispatches through
|
|
3589
|
-
// engine/agent-worker-pool.js (persistent `copilot --acp` workers) instead of
|
|
3851
|
+
// engine/agents/agent-worker-pool.js (persistent `copilot --acp` workers) instead of
|
|
3590
3852
|
// cold-spawning a fresh CLI per dispatch. Opt-in while fleet-pool lifecycle
|
|
3591
3853
|
// hardening remains newer than the established cold-spawn path. Structurally
|
|
3592
3854
|
// Copilot-only, same as
|
|
@@ -3594,7 +3856,7 @@ const ENGINE_DEFAULTS = {
|
|
|
3594
3856
|
// resolved runtime's capability flag isn't true, regardless of this override.
|
|
3595
3857
|
agentUseWorkerPool: false,
|
|
3596
3858
|
// P-9b3d5f61 — max concurrent ACP workers the per-agent pool
|
|
3597
|
-
// (engine/agent-worker-pool.js) will keep warm. `undefined` = fall back to
|
|
3859
|
+
// (engine/agents/agent-worker-pool.js) will keep warm. `undefined` = fall back to
|
|
3598
3860
|
// the fleet's `maxConcurrent` dispatch cap via resolveAgentAcpPoolSize, so
|
|
3599
3861
|
// the pool never queues more than the engine would have dispatched anyway.
|
|
3600
3862
|
agentAcpPoolSize: undefined,
|
|
@@ -3686,7 +3948,8 @@ const ENGINE_DEFAULTS = {
|
|
|
3686
3948
|
liveCheckoutAutoFreshen: false,
|
|
3687
3949
|
orphanHolderScanTimeoutMs: 5000, // 5s ceiling for the cross-platform holder scan (PowerShell / /proc walk / lsof)
|
|
3688
3950
|
ccMaxTurns: 50, // max tool-use turns per CC/doc-chat call before CLI stops (per response, not per session)
|
|
3689
|
-
ccWorkerIdleTimeoutMs: 30 * 60 * 1000, // W-mr0qs0vw: idle-reaper window for the persistent `copilot --acp` worker pool (engine/cc-worker-pool.js). After this much inactivity with no in-flight turn the warm ACP process is killed; the next message cold-spawns a fresh session with NO memory of prior turns (CC shows a "context cleared after inactivity" notice). Tradeoff: shorter = less idle memory/process footprint, longer = more context durability across gaps between messages. Wired into the pool via ccWorkerPool.setIdleTimeoutMs() on every reloadConfig(); clamped to [60000, 28800000] (1min–8h) in the settings POST handler. W-mr1qbj90000d30bd: `0` is an explicit "never idle-reap" sentinel that bypasses the clamp — cc-worker-pool.js maps it to an Infinity window so only an explicit closeTab kills the warm worker; Settings page exposes this as the "Never idle-reap CC workers" toggle.
|
|
3951
|
+
ccWorkerIdleTimeoutMs: 30 * 60 * 1000, // W-mr0qs0vw: idle-reaper window for the persistent `copilot --acp` worker pool (engine/agents/cc-worker-pool.js). After this much inactivity with no in-flight turn the warm ACP process is killed; the next message cold-spawns a fresh session with NO memory of prior turns (CC shows a "context cleared after inactivity" notice). Tradeoff: shorter = less idle memory/process footprint, longer = more context durability across gaps between messages. Wired into the pool via ccWorkerPool.setIdleTimeoutMs() on every reloadConfig(); clamped to [60000, 28800000] (1min–8h) in the settings POST handler. W-mr1qbj90000d30bd: `0` is an explicit "never idle-reap" sentinel that bypasses the clamp — cc-worker-pool.js maps it to an Infinity window so only an explicit closeTab kills the warm worker; Settings page exposes this as the "Never idle-reap CC workers" toggle.
|
|
3952
|
+
acpHandshakeTimeoutMs: 180000, // issue #1105: budget for the cold ACP worker handshake (`initialize` + `session/new` / `session/load`) in engine/agents/acp-transport.js, shared by the CC worker pool (engine/agents/cc-worker-pool.js) and the agent worker pool (engine/agents/agent-worker-pool.js). A cold `copilot --acp` start that loads the user's MCP configuration routinely exceeds the old hardcoded 60s, which surfaced as "Command Center returned no output / handshake timed out after 60000ms". Pushed into each pool via setHandshakeTimeoutMs() on config load; clamped to [10000, 900000] (10s–15min) in the settings POST handler. Timeout still raises the typed, retriable ACP_HANDSHAKE_FAILED error.
|
|
3690
3953
|
ccTurnTimeoutMs: 300000, // W-mpmwxni2000c25c7-b/-d: 5min per-turn no-progress watchdog. The window resets on every liveness signal — token chunk, tool-call notification, tool-update — so an actively-streaming CC/doc-chat turn (long shell command, deep search, sub-agent loop) survives indefinitely up to the outer CC_CALL_TIMEOUT_MS (~1h) ceiling. Only true silence past this window with no progress fires the cancel: the in-flight LLM call is aborted and the handler surfaces `{code:'cc-turn-timeout', retryable:true}` via the typed error envelope so the UI can stop the spinner and offer Retry. Clamped to [10000, 3600000] in the settings POST handler. Independent of CC_CALL_TIMEOUT_MS. Non-streaming doc-chat is the lone wall-clock exception (no progress hooks); see _raceCcDocChatTimeout in dashboard.js for the dual factory/promise shape.
|
|
3691
3954
|
docSessionMaxEntries: 200, // cap doc-chat session map/disk store by least-recent activity (LRU; sessions are non-expiring otherwise)
|
|
3692
3955
|
ccLiveStreamMaxAgeMs: 30 * 60 * 1000, // hard cap reconnect buffers if abort/cleanup stalls
|
|
@@ -3718,7 +3981,7 @@ const ENGINE_DEFAULTS = {
|
|
|
3718
3981
|
memoryRetrievalMaxBytes: 8 * 1024, // hard byte budget for the Relevant Memory prompt appendix
|
|
3719
3982
|
memoryRetrievalCandidateLimit: 50, // bounded FTS5 candidate pool before deterministic re-ranking
|
|
3720
3983
|
memoryEpisodicCapture: false, // opt-in capture of compact task outcomes; never stores transcripts or chain-of-thought
|
|
3721
|
-
untrustedFenceMaxBytes: 64 * 1024, // F5 (W-mpeklod3000we69c): per-block cap for `<UNTRUSTED-INPUT>` fences in engine/untrusted-fence.js. 64KB is long enough for realistic PR comments / pinned notes / agent memory sections, short enough that a megabyte-bomb comment cannot blow up the prompt. Content above the cap is truncated INSIDE the fence with a `[truncated N more bytes]` marker so the agent still sees the provenance attribute.
|
|
3984
|
+
untrustedFenceMaxBytes: 64 * 1024, // F5 (W-mpeklod3000we69c): per-block cap for `<UNTRUSTED-INPUT>` fences in engine/core/untrusted-fence.js. 64KB is long enough for realistic PR comments / pinned notes / agent memory sections, short enough that a megabyte-bomb comment cannot blow up the prompt. Content above the cap is truncated INSIDE the fence with a `[truncated N more bytes]` marker so the agent still sees the provenance attribute.
|
|
3722
3985
|
maxMeetingPromptBytes: 16 * 1024, // cap meeting findings/debate context injected into prompts
|
|
3723
3986
|
maxMeetingHumanNotesBytes: 2 * 1024, // cap human note bullet lists injected into meeting prompts
|
|
3724
3987
|
maxPipelineMeetingContextBytes: 16 * 1024, // cap aggregated meeting/dependency context for pipeline plan generation
|
|
@@ -3730,10 +3993,24 @@ const ENGINE_DEFAULTS = {
|
|
|
3730
3993
|
// for the same project. Saves the cold install/build cost on heavy projects
|
|
3731
3994
|
// (constellation: bun install + Vite warmup; minions: npm install + test cache).
|
|
3732
3995
|
// Default off — opt-in fleet-wide via engine.worktreePoolSize or per-project
|
|
3733
|
-
// via projects[].worktreePoolSize. See engine/
|
|
3996
|
+
// via projects[].worktreePoolSize. See engine/worktrees/pool.js + CLAUDE.md
|
|
3734
3997
|
// "Worktree pool" section for the lifecycle and edge cases.
|
|
3735
3998
|
worktreePoolSize: 0, // 0 = disabled (default); per-project override beats this
|
|
3736
3999
|
worktreePoolIdleTtlMs: 6 * 3600 * 1000, // 6h — idle entries past TTL are evicted by cleanup
|
|
4000
|
+
// W-ms5tb6ha016fda10 — PROTECTED quarantine directories (those whose dirty
|
|
4001
|
+
// WIP could NOT be snapshotted to a durable `refs/minions/quarantine-wip/*`
|
|
4002
|
+
// ref) were previously kept FOREVER, so a busy install accumulated
|
|
4003
|
+
// quarantine residue without bound (20 such dirs observed on 2026-07-29).
|
|
4004
|
+
// They remain the longest-lived worktree artifact, but retention is now
|
|
4005
|
+
// finite and is DERIVED from the single shared `QUARANTINE_REF_RETENTION_MS`
|
|
4006
|
+
// constant above (30 days) — the same window
|
|
4007
|
+
// `engine/worktrees/quarantine-refs.js` prunes recovery refs at — so the
|
|
4008
|
+
// operator recovery window for quarantine DIRECTORIES and quarantine REFS
|
|
4009
|
+
// expires together instead of one outliving the other. Do NOT replace this
|
|
4010
|
+
// with a literal: the two windows drifted (10d vs 30d) exactly because each
|
|
4011
|
+
// side owned its own number.
|
|
4012
|
+
// Set to 0 to restore indefinite retention.
|
|
4013
|
+
worktreeProtectedQuarantineRetentionMs: QUARANTINE_REF_RETENTION_MS,
|
|
3737
4014
|
// ── keep_processes (W-mp68q6ke0010de68) ────────────────────────────────────
|
|
3738
4015
|
// Opt-in per-WI (`meta.keep_processes: true`) feature that lets an agent
|
|
3739
4016
|
// declare specific descendant PIDs the engine MUST NOT reap on close. The
|
|
@@ -3759,7 +4036,7 @@ const ENGINE_DEFAULTS = {
|
|
|
3759
4036
|
// P-7a3b1c92 / plan W-mp7k1r760003b5dd — managed-spawn primitive: agents
|
|
3760
4037
|
// describe long-running services in agents/<id>/managed-spawn.json and the
|
|
3761
4038
|
// engine owns the spawn + healthcheck lifecycle. These defaults gate the
|
|
3762
|
-
// validator (engine/managed-spawn.js) and later the engine-side spawn loop,
|
|
4039
|
+
// validator (engine/processes/managed-spawn.js) and later the engine-side spawn loop,
|
|
3763
4040
|
// per-tick sweep, playbook hint, and dashboard endpoints. Mirrors the
|
|
3764
4041
|
// `keepProcesses` block intentionally — symmetry with the keep-pids
|
|
3765
4042
|
// primitive is the documented design convention.
|
|
@@ -3857,10 +4134,35 @@ const ENGINE_DEFAULTS = {
|
|
|
3857
4134
|
constellationBridge: {
|
|
3858
4135
|
enabled: false,
|
|
3859
4136
|
},
|
|
4137
|
+
// ── Constellation companion probe (P-e93c85a1) ──────────────────────────────
|
|
4138
|
+
// Read-only detection of whether a COMPATIBLE Constellation companion is
|
|
4139
|
+
// present on this host. Reporting only: Minions never downloads, executes, or
|
|
4140
|
+
// installs Constellation, and companion absence is a visible degraded state,
|
|
4141
|
+
// never fatal. Surfaced by `minions doctor`, `minions bridge health`, and as
|
|
4142
|
+
// informational output from `minions init` / `minions update`.
|
|
4143
|
+
//
|
|
4144
|
+
// probeEnabled Documented opt-out for managed deployments. `false`
|
|
4145
|
+
// suppresses the probe ENTIRELY (no marker read, no health
|
|
4146
|
+
// request). `MINIONS_DISABLE_COMPANION_PROBE=1` does the
|
|
4147
|
+
// same from the environment and wins over config.
|
|
4148
|
+
// probeTimeoutMs Hard bound on the optional loopback health request, so a
|
|
4149
|
+
// hung companion can never stall doctor/init/update/start.
|
|
4150
|
+
// Clamped to [250, 15000] ms at read time.
|
|
4151
|
+
// healthUrl Operator override for the companion's health endpoint.
|
|
4152
|
+
// `null` (default) means "use whatever the companion's own
|
|
4153
|
+
// marker declares, if anything". LOOPBACK-ONLY: a remote
|
|
4154
|
+
// host is refused without being requested.
|
|
4155
|
+
//
|
|
4156
|
+
// See engine/api/companion.js and docs/constellation-bridge.md.
|
|
4157
|
+
constellationCompanion: {
|
|
4158
|
+
probeEnabled: true,
|
|
4159
|
+
probeTimeoutMs: 2000,
|
|
4160
|
+
healthUrl: null,
|
|
4161
|
+
},
|
|
3860
4162
|
// ── Operator identity (W-mpejf0fq000e84d6) ──────────────────────────────────
|
|
3861
4163
|
// Explicit override for the human operator's platform login used in branch
|
|
3862
4164
|
// names (see `deriveWorkItemBranchName`). `null` (default) means auto-resolve
|
|
3863
|
-
// via `engine/operator-identity.js` (gh → git email localpart → os user).
|
|
4165
|
+
// via `engine/core/operator-identity.js` (gh → git email localpart → os user).
|
|
3864
4166
|
// Settings UI exposes this as a free-text input; clearing the field deletes
|
|
3865
4167
|
// the override and falls back to auto-resolution.
|
|
3866
4168
|
operatorLogin: null,
|
|
@@ -3963,7 +4265,7 @@ function resolveCcCli(engine) {
|
|
|
3963
4265
|
function resolveCcUseWorkerPool(engine) {
|
|
3964
4266
|
let runtime;
|
|
3965
4267
|
try {
|
|
3966
|
-
runtime = require('
|
|
4268
|
+
runtime = require('../runtimes').resolveRuntime(resolveCcCli(engine));
|
|
3967
4269
|
} catch {
|
|
3968
4270
|
return false;
|
|
3969
4271
|
}
|
|
@@ -3976,7 +4278,7 @@ function resolveCcUseWorkerPool(engine) {
|
|
|
3976
4278
|
|
|
3977
4279
|
/**
|
|
3978
4280
|
* P-9b3d5f61 — Resolve whether a per-agent spawn should route through the
|
|
3979
|
-
* persistent ACP worker pool (engine/agent-worker-pool.js). Mirrors
|
|
4281
|
+
* persistent ACP worker pool (engine/agents/agent-worker-pool.js). Mirrors
|
|
3980
4282
|
* resolveCcUseWorkerPool's shape but takes the already-resolved runtime
|
|
3981
4283
|
* adapter (as returned by `resolveRuntime(resolveAgentCli(agent, engine))`)
|
|
3982
4284
|
* rather than re-deriving it, since callers on the agent-spawn path already
|
|
@@ -3998,9 +4300,33 @@ function resolveAgentUseWorkerPool(engine, runtime) {
|
|
|
3998
4300
|
return ENGINE_DEFAULTS.agentUseWorkerPool;
|
|
3999
4301
|
}
|
|
4000
4302
|
|
|
4303
|
+
// Issue #1105 — clamp bounds for engine.acpHandshakeTimeoutMs. Mirrored by the
|
|
4304
|
+
// settings POST validator so the UI and the resolver agree on the legal range.
|
|
4305
|
+
const ACP_HANDSHAKE_TIMEOUT_MIN_MS = 10000;
|
|
4306
|
+
const ACP_HANDSHAKE_TIMEOUT_MAX_MS = 900000;
|
|
4307
|
+
|
|
4308
|
+
/**
|
|
4309
|
+
* Issue #1105 — Resolve the ACP worker handshake budget (`initialize` plus
|
|
4310
|
+
* `session/new` / `session/load`) used by both ACP worker pools. Priority:
|
|
4311
|
+
* 1. `engine.acpHandshakeTimeoutMs` — operator override
|
|
4312
|
+
* 2. `ENGINE_DEFAULTS.acpHandshakeTimeoutMs` — 180s default
|
|
4313
|
+
*
|
|
4314
|
+
* Non-numeric or non-positive overrides fall through to the default; valid
|
|
4315
|
+
* values are clamped to [10s, 15min] so a bad Settings/env value can neither
|
|
4316
|
+
* make the handshake un-cancellable nor fire before a cold start can finish.
|
|
4317
|
+
*/
|
|
4318
|
+
function resolveAcpHandshakeTimeoutMs(engine) {
|
|
4319
|
+
const raw = engine ? engine.acpHandshakeTimeoutMs : undefined;
|
|
4320
|
+
const n = typeof raw === 'number' ? raw : Number(raw);
|
|
4321
|
+
if (raw === undefined || raw === null || raw === '' || !Number.isFinite(n) || n <= 0) {
|
|
4322
|
+
return ENGINE_DEFAULTS.acpHandshakeTimeoutMs;
|
|
4323
|
+
}
|
|
4324
|
+
return Math.min(ACP_HANDSHAKE_TIMEOUT_MAX_MS, Math.max(ACP_HANDSHAKE_TIMEOUT_MIN_MS, Math.round(n)));
|
|
4325
|
+
}
|
|
4326
|
+
|
|
4001
4327
|
/**
|
|
4002
4328
|
* P-9b3d5f61 — Resolve the max concurrent ACP workers the per-agent pool
|
|
4003
|
-
* (engine/agent-worker-pool.js) should keep warm. Priority:
|
|
4329
|
+
* (engine/agents/agent-worker-pool.js) should keep warm. Priority:
|
|
4004
4330
|
* 1. `engine.agentAcpPoolSize` — explicit operator override
|
|
4005
4331
|
* 2. `engine.maxConcurrent` — fleet dispatch concurrency cap (so the
|
|
4006
4332
|
* pool never queues more than the engine would have dispatched anyway)
|
|
@@ -4133,7 +4459,7 @@ function resolvePropagateClaudeMdForNonClaudeRuntimes(project, engine) {
|
|
|
4133
4459
|
// "set" at either level — undefined/null/absent falls through to the next tier
|
|
4134
4460
|
// so operators can enable draft PRs fleet-wide while opting a specific project
|
|
4135
4461
|
// OUT (or vice-versa). Pure, no I/O. Consumed by
|
|
4136
|
-
// engine/playbook.js#getPrCreateInstructions.
|
|
4462
|
+
// engine/agents/playbook.js#getPrCreateInstructions.
|
|
4137
4463
|
function resolveCreatePrsAsDraft(project, engine) {
|
|
4138
4464
|
if (project && typeof project === 'object' && typeof project.createPrsAsDraft === 'boolean') {
|
|
4139
4465
|
return project.createPrsAsDraft;
|
|
@@ -4144,6 +4470,18 @@ function resolveCreatePrsAsDraft(project, engine) {
|
|
|
4144
4470
|
return ENGINE_DEFAULTS.createPrsAsDraft;
|
|
4145
4471
|
}
|
|
4146
4472
|
|
|
4473
|
+
// W-msal3ser01q4ccb3 — canonical resolution seam for the auto-QA-after-verify
|
|
4474
|
+
// flag. Only an explicit boolean on `engine` counts as "set"; anything else
|
|
4475
|
+
// (undefined/null/'true'/0) falls through to ENGINE_DEFAULTS so a malformed
|
|
4476
|
+
// config.json cannot silently enable an automatic dispatch. Pure, no I/O.
|
|
4477
|
+
// Consumed by engine/qa/auto-prd-qa.js.
|
|
4478
|
+
function resolveAutoQaCompletedPrds(engine) {
|
|
4479
|
+
if (engine && typeof engine === 'object' && typeof engine.autoQaCompletedPrds === 'boolean') {
|
|
4480
|
+
return engine.autoQaCompletedPrds;
|
|
4481
|
+
}
|
|
4482
|
+
return ENGINE_DEFAULTS.autoQaCompletedPrds;
|
|
4483
|
+
}
|
|
4484
|
+
|
|
4147
4485
|
// ─── Legacy ccModel → defaultModel Migration ─────────────────────────────────
|
|
4148
4486
|
//
|
|
4149
4487
|
// Pre-P-3b8e5f1d, `engine.ccModel` was the single fleet-wide model knob (it
|
|
@@ -4194,7 +4532,7 @@ function _resetLegacyCcModelMigrationFlag() {
|
|
|
4194
4532
|
// ─── Runtime Config Preflight Warnings ──────────────────────────────────────
|
|
4195
4533
|
//
|
|
4196
4534
|
// Emit non-fatal warnings about runtime/CLI configuration drift. Consumed by
|
|
4197
|
-
// engine/preflight.js (which converts the entries to `{ name, ok: 'warn',
|
|
4535
|
+
// engine/agents/preflight.js (which converts the entries to `{ name, ok: 'warn',
|
|
4198
4536
|
// message }` shape) and surfaced via `minions doctor`.
|
|
4199
4537
|
//
|
|
4200
4538
|
// The function is pure: takes the config and the list of registered runtime
|
|
@@ -4271,7 +4609,7 @@ function runtimeConfigWarnings(config, registeredRuntimes) {
|
|
|
4271
4609
|
if (engine.claudeBareMode === true) {
|
|
4272
4610
|
const ccCli = resolveCcCli(engine);
|
|
4273
4611
|
let ccRuntime = null;
|
|
4274
|
-
try { ccRuntime = require('
|
|
4612
|
+
try { ccRuntime = require('../runtimes').resolveRuntime(ccCli); } catch { /* unknown runtime — skip */ }
|
|
4275
4613
|
if (ccRuntime?.capabilities?.bareMode === true && !(engine.ccSystemPrompt !== undefined && engine.ccSystemPrompt !== null && engine.ccSystemPrompt !== '')) {
|
|
4276
4614
|
warnings.push({
|
|
4277
4615
|
id: 'bare-mode-misconfig',
|
|
@@ -4526,6 +4864,182 @@ const WORKTREE_REQUIRING_TYPES = new Set([
|
|
|
4526
4864
|
WORK_TYPE.DOCS,
|
|
4527
4865
|
]);
|
|
4528
4866
|
|
|
4867
|
+
// ── Execution surface (W-ms5tb6ha016fda10) ──────────────────────────────────
|
|
4868
|
+
//
|
|
4869
|
+
// Some scheduled/maintenance work has NOTHING to do with a repository
|
|
4870
|
+
// checkout: "merge approved PRs", "sweep the PR tracker", "one-time review
|
|
4871
|
+
// external PRs", "CI health check". Its entire contract is Minions dashboard
|
|
4872
|
+
// API calls plus external CLIs (`gh`, `az`). Historically those still ran as
|
|
4873
|
+
// `setup`/`explore` dispatches, so the engine allocated a full project
|
|
4874
|
+
// worktree for them — pure cost, and worse: concurrent fresh-worktree
|
|
4875
|
+
// preparation raced on `refs/remotes/origin/<main>` and failed jobs that never
|
|
4876
|
+
// needed a checkout at all.
|
|
4877
|
+
//
|
|
4878
|
+
// `meta.executionSurface` makes that EXPLICIT rather than inferred. There is
|
|
4879
|
+
// deliberately no prose sniffing ("does the description mention curl?") — the
|
|
4880
|
+
// operator/schedule declares the surface as structured data and the engine
|
|
4881
|
+
// validates it fail-closed:
|
|
4882
|
+
//
|
|
4883
|
+
// 'repo' (default) — today's behavior: worktree / live checkout.
|
|
4884
|
+
// 'control-plane' — no repository checkout is allocated at all.
|
|
4885
|
+
// cwd is MINIONS_DIR; project context is still
|
|
4886
|
+
// carried in the prompt.
|
|
4887
|
+
// 'cloud' — the work is handed to a REMOTE coding agent
|
|
4888
|
+
// (Agency / Copilot cloud). No local checkout,
|
|
4889
|
+
// no local process: the remote agent owns its
|
|
4890
|
+
// own workspace and produces a PR that Minions
|
|
4891
|
+
// then adopts into its normal PR record and
|
|
4892
|
+
// review/fix loops.
|
|
4893
|
+
//
|
|
4894
|
+
// Fail-closed by construction: the opt-in is an ALLOWLIST of work types
|
|
4895
|
+
// (`CONTROL_PLANE_ELIGIBLE_TYPES`), not a denylist. A repo-mutating type
|
|
4896
|
+
// (fix/implement/review/test/verify/decompose/docs/build-fix-complex) can
|
|
4897
|
+
// never declare control-plane, and an unknown surface string is rejected
|
|
4898
|
+
// rather than defaulted. `cloud` carries its own, separate allowlist
|
|
4899
|
+
// (`CLOUD_ELIGIBLE_TYPES`) because its eligibility question is the inverse
|
|
4900
|
+
// one — not "can this run without a checkout?" but "does this produce a PR a
|
|
4901
|
+
// remote agent can author?".
|
|
4902
|
+
const EXECUTION_SURFACE = {
|
|
4903
|
+
REPO: 'repo',
|
|
4904
|
+
CONTROL_PLANE: 'control-plane',
|
|
4905
|
+
CLOUD: 'cloud',
|
|
4906
|
+
};
|
|
4907
|
+
const EXECUTION_SURFACE_VALUES = Object.freeze(Object.values(EXECUTION_SURFACE));
|
|
4908
|
+
|
|
4909
|
+
// Only these types may declare `control-plane`. Every one of them is a type
|
|
4910
|
+
// that never creates a branch, never commits, and never opens a PR, so
|
|
4911
|
+
// removing its checkout cannot silently change what the dispatch produces.
|
|
4912
|
+
// `setup` is the important entry: the built-in maintenance schedules use it.
|
|
4913
|
+
const CONTROL_PLANE_ELIGIBLE_TYPES = new Set([
|
|
4914
|
+
WORK_TYPE.SETUP,
|
|
4915
|
+
WORK_TYPE.ASK,
|
|
4916
|
+
WORK_TYPE.EXPLORE,
|
|
4917
|
+
WORK_TYPE.MEETING,
|
|
4918
|
+
]);
|
|
4919
|
+
|
|
4920
|
+
function isControlPlaneEligibleType(type) {
|
|
4921
|
+
return CONTROL_PLANE_ELIGIBLE_TYPES.has(String(type || '').trim().toLowerCase());
|
|
4922
|
+
}
|
|
4923
|
+
|
|
4924
|
+
// Only these types may declare `cloud`. A cloud dispatch is a hand-off: Minions
|
|
4925
|
+
// renders the same playbook prompt it would have given a local agent, ships it
|
|
4926
|
+
// to a remote coding agent, and adopts the PR that comes back. That only makes
|
|
4927
|
+
// sense for types whose deliverable IS a pull request against a single repo.
|
|
4928
|
+
//
|
|
4929
|
+
// Deliberately excluded, and why:
|
|
4930
|
+
// review / verify — their job is to build, run tests, and inspect a
|
|
4931
|
+
// checkout; a remote agent that cannot run the local
|
|
4932
|
+
// validation build produces a worse review, not a
|
|
4933
|
+
// cheaper one.
|
|
4934
|
+
// build-fix-complex — needs live CI logs and the PR's own branch.
|
|
4935
|
+
// decompose — pure planning; it mutates Minions state, not a repo.
|
|
4936
|
+
// implement:large — decomposed into child items first; the CHILDREN are
|
|
4937
|
+
// what may go to the cloud.
|
|
4938
|
+
// setup — produces no PR and usually drives local processes
|
|
4939
|
+
// (managed_spawn), which a remote agent cannot leave
|
|
4940
|
+
// running on this machine.
|
|
4941
|
+
// ask / explore / meeting — read-only; their value is the output text, which
|
|
4942
|
+
// the cloud surface does not stream back.
|
|
4943
|
+
const CLOUD_ELIGIBLE_TYPES = new Set([
|
|
4944
|
+
WORK_TYPE.IMPLEMENT,
|
|
4945
|
+
WORK_TYPE.FIX,
|
|
4946
|
+
WORK_TYPE.TEST,
|
|
4947
|
+
WORK_TYPE.DOCS,
|
|
4948
|
+
]);
|
|
4949
|
+
|
|
4950
|
+
function isCloudEligibleType(type) {
|
|
4951
|
+
return CLOUD_ELIGIBLE_TYPES.has(String(type || '').trim().toLowerCase());
|
|
4952
|
+
}
|
|
4953
|
+
|
|
4954
|
+
/**
|
|
4955
|
+
* Validate a declared execution surface against the work type.
|
|
4956
|
+
*
|
|
4957
|
+
* @returns {{valid: true, value: string}|{valid: false, error: string, code: string}}
|
|
4958
|
+
* Unset/empty normalizes to EXECUTION_SURFACE.REPO (back-compat default).
|
|
4959
|
+
*/
|
|
4960
|
+
function validateExecutionSurface(value, type) {
|
|
4961
|
+
if (value === undefined || value === null) return { valid: true, value: EXECUTION_SURFACE.REPO };
|
|
4962
|
+
if (typeof value !== 'string') {
|
|
4963
|
+
return {
|
|
4964
|
+
valid: false,
|
|
4965
|
+
code: 'invalid-execution-surface',
|
|
4966
|
+
error: `meta.executionSurface must be a string (got ${typeof value})`,
|
|
4967
|
+
};
|
|
4968
|
+
}
|
|
4969
|
+
const normalized = value.trim().toLowerCase();
|
|
4970
|
+
if (!normalized) return { valid: true, value: EXECUTION_SURFACE.REPO };
|
|
4971
|
+
if (!EXECUTION_SURFACE_VALUES.includes(normalized)) {
|
|
4972
|
+
return {
|
|
4973
|
+
valid: false,
|
|
4974
|
+
code: 'invalid-execution-surface',
|
|
4975
|
+
error: `meta.executionSurface must be one of ${EXECUTION_SURFACE_VALUES.join(', ')} (got "${value}")`,
|
|
4976
|
+
};
|
|
4977
|
+
}
|
|
4978
|
+
if (normalized === EXECUTION_SURFACE.CONTROL_PLANE && !isControlPlaneEligibleType(type)) {
|
|
4979
|
+
return {
|
|
4980
|
+
valid: false,
|
|
4981
|
+
code: 'invalid-execution-surface',
|
|
4982
|
+
error: `work type "${type}" may not declare meta.executionSurface "control-plane" — `
|
|
4983
|
+
+ `it reads or mutates repository files and requires a checkout. `
|
|
4984
|
+
+ `Eligible types: ${[...CONTROL_PLANE_ELIGIBLE_TYPES].sort().join(', ')}.`,
|
|
4985
|
+
};
|
|
4986
|
+
}
|
|
4987
|
+
if (normalized === EXECUTION_SURFACE.CLOUD && !isCloudEligibleType(type)) {
|
|
4988
|
+
return {
|
|
4989
|
+
valid: false,
|
|
4990
|
+
code: 'invalid-execution-surface',
|
|
4991
|
+
error: `work type "${type}" may not declare meta.executionSurface "cloud" — `
|
|
4992
|
+
+ `a cloud dispatch hands the task to a remote coding agent and adopts the pull `
|
|
4993
|
+
+ `request it authors, so only PR-producing types qualify. `
|
|
4994
|
+
+ `Eligible types: ${[...CLOUD_ELIGIBLE_TYPES].sort().join(', ')}.`,
|
|
4995
|
+
};
|
|
4996
|
+
}
|
|
4997
|
+
return { valid: true, value: normalized };
|
|
4998
|
+
}
|
|
4999
|
+
|
|
5000
|
+
/**
|
|
5001
|
+
* Read the declared surface off a work item (`meta.executionSurface`, with the
|
|
5002
|
+
* top-level `executionSurface` accepted for ad-hoc dispatch payloads). Returns
|
|
5003
|
+
* the RAW declared value so callers can run `validateExecutionSurface` and
|
|
5004
|
+
* surface a precise rejection; use `isControlPlaneExecution` when you only need
|
|
5005
|
+
* the fail-closed boolean.
|
|
5006
|
+
*/
|
|
5007
|
+
function readDeclaredExecutionSurface(item) {
|
|
5008
|
+
if (!item || typeof item !== 'object') return undefined;
|
|
5009
|
+
const fromMeta = item.meta && typeof item.meta === 'object' ? item.meta.executionSurface : undefined;
|
|
5010
|
+
return fromMeta !== undefined ? fromMeta : item.executionSurface;
|
|
5011
|
+
}
|
|
5012
|
+
|
|
5013
|
+
/**
|
|
5014
|
+
* Fail-closed predicate: true ONLY when the item explicitly and validly
|
|
5015
|
+
* declares control-plane for an eligible type. Any invalid, unknown, or
|
|
5016
|
+
* ineligible declaration returns false, so a malformed opt-in degrades to a
|
|
5017
|
+
* normal repo dispatch rather than silently skipping the checkout.
|
|
5018
|
+
*/
|
|
5019
|
+
function isControlPlaneExecution(item, type) {
|
|
5020
|
+
const declared = readDeclaredExecutionSurface(item);
|
|
5021
|
+
if (declared === undefined || declared === null) return false;
|
|
5022
|
+
const resolvedType = type !== undefined && type !== null ? type : item?.type;
|
|
5023
|
+
const result = validateExecutionSurface(declared, resolvedType);
|
|
5024
|
+
return result.valid && result.value === EXECUTION_SURFACE.CONTROL_PLANE;
|
|
5025
|
+
}
|
|
5026
|
+
|
|
5027
|
+
/**
|
|
5028
|
+
* Fail-closed predicate: true ONLY when the item explicitly and validly
|
|
5029
|
+
* declares the `cloud` surface for an eligible type. Any invalid, unknown, or
|
|
5030
|
+
* ineligible declaration returns false, so a malformed opt-in degrades to a
|
|
5031
|
+
* normal local repo dispatch rather than silently shipping the work to a
|
|
5032
|
+
* remote agent (the strictly safer default — local dispatch is the behavior
|
|
5033
|
+
* every existing work item already has).
|
|
5034
|
+
*/
|
|
5035
|
+
function isCloudExecution(item, type) {
|
|
5036
|
+
const declared = readDeclaredExecutionSurface(item);
|
|
5037
|
+
if (declared === undefined || declared === null) return false;
|
|
5038
|
+
const resolvedType = type !== undefined && type !== null ? type : item?.type;
|
|
5039
|
+
const result = validateExecutionSurface(declared, resolvedType);
|
|
5040
|
+
return result.valid && result.value === EXECUTION_SURFACE.CLOUD;
|
|
5041
|
+
}
|
|
5042
|
+
|
|
4529
5043
|
// Hybrid routing is meaningful only for work types that would otherwise need
|
|
4530
5044
|
// an isolated project worktree. Read-only root tasks already run from the
|
|
4531
5045
|
// project/root path and therefore gain nothing from a live-validation entry.
|
|
@@ -4613,15 +5127,266 @@ const PLAN_STATUS = {
|
|
|
4613
5127
|
};
|
|
4614
5128
|
const PRD_ITEM_STATUS = { MISSING: 'missing', UPDATED: 'updated', DONE: 'done' };
|
|
4615
5129
|
const PRD_MATERIALIZABLE = new Set([PRD_ITEM_STATUS.MISSING, PRD_ITEM_STATUS.UPDATED]);
|
|
5130
|
+
|
|
5131
|
+
// ── PRD revision lifecycle (W-msa0mrus00mh466f) ──────────────────────────────
|
|
5132
|
+
// `revision-requested` (an agent is actively rewriting the PRD) and `planStale`
|
|
5133
|
+
// (the source markdown changed under an existing PRD) are two DIFFERENT recovery
|
|
5134
|
+
// paths that could previously be true at the same time, so the dashboard offered
|
|
5135
|
+
// "Regenerate PRD" / "Resume as-is" while a revision agent was already in flight.
|
|
5136
|
+
// These helpers are the single place that derivation happens: the dashboard
|
|
5137
|
+
// renders what the server returns and never re-derives it.
|
|
5138
|
+
const PRD_REVISION_STATE = {
|
|
5139
|
+
NONE: 'none',
|
|
5140
|
+
IN_PROGRESS: 'in-progress',
|
|
5141
|
+
FAILED: 'failed',
|
|
5142
|
+
};
|
|
5143
|
+
// A revision operation still owns the PRD while its work item can make progress.
|
|
5144
|
+
// Anything outside this set is terminal, so the PRD needs an explicit recovery
|
|
5145
|
+
// state rather than a permanent "revision in progress".
|
|
5146
|
+
const PRD_REVISION_ACTIVE_WI_STATUSES = new Set([
|
|
5147
|
+
WI_STATUS.PENDING, WI_STATUS.DISPATCHED, WI_STATUS.QUEUED, WI_STATUS.PAUSED,
|
|
5148
|
+
]);
|
|
5149
|
+
|
|
5150
|
+
/**
|
|
5151
|
+
* Resolve the work item backing a PRD's revision request.
|
|
5152
|
+
*
|
|
5153
|
+
* Primary key is the `revisionWorkItemId` pointer persisted on the PRD. The
|
|
5154
|
+
* reverse `_revisionForPrd` stamp on the work item is the fallback so a PRD
|
|
5155
|
+
* whose pointer write raced (or predates the pointer) still correlates to its
|
|
5156
|
+
* operation — never by matching title text.
|
|
5157
|
+
*
|
|
5158
|
+
* @param {object|null} plan PRD object
|
|
5159
|
+
* @param {string} prdFile PRD filename (e.g. `foo-2026-08-01.json`)
|
|
5160
|
+
* @param {Array<object>} workItems Work items to search (central scope)
|
|
5161
|
+
* @returns {object|null}
|
|
5162
|
+
*/
|
|
5163
|
+
function findPrdRevisionWorkItem(plan, prdFile, workItems) {
|
|
5164
|
+
const items = Array.isArray(workItems) ? workItems : [];
|
|
5165
|
+
const pinnedId = (plan && typeof plan === 'object') ? String(plan.revisionWorkItemId || '').trim() : '';
|
|
5166
|
+
if (pinnedId) {
|
|
5167
|
+
const byId = items.find(w => w && w.id === pinnedId);
|
|
5168
|
+
if (byId) return byId;
|
|
5169
|
+
}
|
|
5170
|
+
const file = String(prdFile || '').trim();
|
|
5171
|
+
if (!file) return null;
|
|
5172
|
+
let fallback = null;
|
|
5173
|
+
for (const w of items) {
|
|
5174
|
+
if (!w || w.type !== WORK_TYPE.PLAN_TO_PRD || w._revisionForPrd !== file) continue;
|
|
5175
|
+
if (PRD_REVISION_ACTIVE_WI_STATUSES.has(w.status)) return w;
|
|
5176
|
+
if (!fallback || String(w.created || '') > String(fallback.created || '')) fallback = w;
|
|
5177
|
+
}
|
|
5178
|
+
return fallback;
|
|
5179
|
+
}
|
|
5180
|
+
|
|
5181
|
+
/**
|
|
5182
|
+
* Canonical revision state for a PRD. Pure — callers resolve the work item with
|
|
5183
|
+
* `findPrdRevisionWorkItem` first.
|
|
5184
|
+
*
|
|
5185
|
+
* @param {object|null} plan
|
|
5186
|
+
* @param {object|null} workItem
|
|
5187
|
+
* @returns {{state:string, active:boolean, workItemId:string, workItemStatus:string,
|
|
5188
|
+
* requestedAt:string, requestedBy:string, feedback:string, failReason:string}}
|
|
5189
|
+
*/
|
|
5190
|
+
function derivePrdRevisionState(plan, workItem) {
|
|
5191
|
+
const out = {
|
|
5192
|
+
state: PRD_REVISION_STATE.NONE, active: false,
|
|
5193
|
+
workItemId: '', workItemStatus: '',
|
|
5194
|
+
requestedAt: '', requestedBy: '', feedback: '', failReason: '',
|
|
5195
|
+
};
|
|
5196
|
+
if (!plan || typeof plan !== 'object') return out;
|
|
5197
|
+
if (plan.status !== PLAN_STATUS.REVISION_REQUESTED) return out;
|
|
5198
|
+
out.requestedAt = plan.revisionRequestedAt || '';
|
|
5199
|
+
out.requestedBy = plan.revisionRequestedBy || '';
|
|
5200
|
+
out.feedback = plan.revision_feedback || '';
|
|
5201
|
+
const wi = (workItem && typeof workItem === 'object') ? workItem : null;
|
|
5202
|
+
out.workItemId = String((wi && wi.id) || plan.revisionWorkItemId || '').trim();
|
|
5203
|
+
if (!wi) {
|
|
5204
|
+
// No operation backs the request. Surface it as recoverable instead of
|
|
5205
|
+
// pinning a permanent, misleading "in progress".
|
|
5206
|
+
out.state = PRD_REVISION_STATE.FAILED;
|
|
5207
|
+
out.failReason = 'Revision work item not found';
|
|
5208
|
+
return out;
|
|
5209
|
+
}
|
|
5210
|
+
out.workItemStatus = String(wi.status || '');
|
|
5211
|
+
if (PRD_REVISION_ACTIVE_WI_STATUSES.has(wi.status)) {
|
|
5212
|
+
out.state = PRD_REVISION_STATE.IN_PROGRESS;
|
|
5213
|
+
out.active = true;
|
|
5214
|
+
return out;
|
|
5215
|
+
}
|
|
5216
|
+
out.state = PRD_REVISION_STATE.FAILED;
|
|
5217
|
+
out.failReason = wi.failReason
|
|
5218
|
+
|| (DONE_STATUSES.has(wi.status)
|
|
5219
|
+
? 'Revision agent finished without updating this PRD'
|
|
5220
|
+
: `Revision work item ended as "${out.workItemStatus || 'unknown'}"`);
|
|
5221
|
+
return out;
|
|
5222
|
+
}
|
|
5223
|
+
|
|
5224
|
+
/**
|
|
5225
|
+
* Operator feedback to inject into a revision dispatch's prompt.
|
|
5226
|
+
*
|
|
5227
|
+
* A revision run is a plan-to-prd run whose ONLY distinguishing input is the
|
|
5228
|
+
* operator's feedback, so that text has to reach the agent explicitly. It used
|
|
5229
|
+
* to arrive by accident: the work item's `planFile` named the PRD `.json`, the
|
|
5230
|
+
* engine's `PLANS_DIR/<planFile>` read failed, and the description (which
|
|
5231
|
+
* embeds the feedback) leaked in through the `plan_content` fallback. Pointing
|
|
5232
|
+
* `planFile` at the real source markdown closed that accident, and
|
|
5233
|
+
* `plan-to-prd.md` has no `{{task_description}}` token, so the feedback needs
|
|
5234
|
+
* its own channel.
|
|
5235
|
+
*
|
|
5236
|
+
* The PRD is the single source of truth for the feedback (`handlePlansRevise`
|
|
5237
|
+
* writes it, `applyPrdRevisionCompletion` clears it), which also keeps a retry
|
|
5238
|
+
* that reuses the same work item from rendering superseded text.
|
|
5239
|
+
*
|
|
5240
|
+
* Gated on the structural `_revisionForPrd` ⇄ resolved-PRD-filename relation —
|
|
5241
|
+
* never title text — so a plain regeneration cannot pick up feedback left on a
|
|
5242
|
+
* PRD by an earlier failed revision.
|
|
5243
|
+
*
|
|
5244
|
+
* @param {object|null} workItem plan-to-prd work item being dispatched
|
|
5245
|
+
* @param {object|null} prd PRD the engine resolved for this dispatch
|
|
5246
|
+
* @param {string} prdFile filename of that resolved PRD
|
|
5247
|
+
* @returns {string} feedback text, or '' when this is not a revision dispatch
|
|
5248
|
+
*/
|
|
5249
|
+
function resolvePrdRevisionFeedback(workItem, prd, prdFile) {
|
|
5250
|
+
if (!workItem || typeof workItem !== 'object') return '';
|
|
5251
|
+
if (!prd || typeof prd !== 'object' || Array.isArray(prd)) return '';
|
|
5252
|
+
const target = String(workItem._revisionForPrd || '').trim();
|
|
5253
|
+
const resolved = String(prdFile || '').trim();
|
|
5254
|
+
if (!target || target !== resolved) return '';
|
|
5255
|
+
return String(prd.revision_feedback || '').trim();
|
|
5256
|
+
}
|
|
5257
|
+
|
|
5258
|
+
/**
|
|
5259
|
+
* Normalize an imported revision PRD so the completed revision and the stale
|
|
5260
|
+
* recovery controls can never be observed together: status returns to
|
|
5261
|
+
* awaiting-approval, staleness is cleared, the source-plan sync fields are
|
|
5262
|
+
* re-baselined against the live markdown, and the revision pointers are dropped.
|
|
5263
|
+
*
|
|
5264
|
+
* Returns a NEW object so the caller can persist it in a single store write.
|
|
5265
|
+
*
|
|
5266
|
+
* @param {object} prd PRD produced by the revision agent
|
|
5267
|
+
* @param {{plansDir?:string, previous?:object}} [opts]
|
|
5268
|
+
* @returns {object}
|
|
5269
|
+
*/
|
|
5270
|
+
function applyPrdRevisionCompletion(prd, { plansDir, previous } = {}) {
|
|
5271
|
+
if (!prd || typeof prd !== 'object' || Array.isArray(prd)) {
|
|
5272
|
+
throw new TypeError('applyPrdRevisionCompletion requires a PRD object');
|
|
5273
|
+
}
|
|
5274
|
+
const next = { ...prd };
|
|
5275
|
+
if (!next.source_plan && previous && previous.source_plan) next.source_plan = previous.source_plan;
|
|
5276
|
+
next.status = PLAN_STATUS.AWAITING_APPROVAL;
|
|
5277
|
+
const now = new Date().toISOString();
|
|
5278
|
+
next.revisionCompletedAt = now;
|
|
5279
|
+
next.lastSyncedFromPlan = now;
|
|
5280
|
+
delete next.planStale;
|
|
5281
|
+
delete next.revision_feedback;
|
|
5282
|
+
delete next.revisionRequestedAt;
|
|
5283
|
+
delete next.revisionRequestedBy;
|
|
5284
|
+
delete next.revisionWorkItemId;
|
|
5285
|
+
// Approval was granted for the pre-revision content — it does not carry over.
|
|
5286
|
+
delete next.approvedAt;
|
|
5287
|
+
delete next.approvedBy;
|
|
5288
|
+
const resolved = next.source_plan ? resolveSourcePlanPath(plansDir, next.source_plan) : null;
|
|
5289
|
+
if (resolved) {
|
|
5290
|
+
const hash = computeSourcePlanContentHash(resolved);
|
|
5291
|
+
if (hash) next._sourcePlanContentHash = hash;
|
|
5292
|
+
try {
|
|
5293
|
+
next.sourcePlanModifiedAt = new Date(Math.floor(fs.statSync(resolved).mtimeMs)).toISOString();
|
|
5294
|
+
} catch { /* markdown may have been archived/removed — keep the recorded baseline */ }
|
|
5295
|
+
}
|
|
5296
|
+
return next;
|
|
5297
|
+
}
|
|
5298
|
+
|
|
4616
5299
|
const PR_STATUS = { ACTIVE: 'active', MERGED: 'merged', ABANDONED: 'abandoned', CLOSED: 'closed', LINKED: 'linked' };
|
|
4617
5300
|
// PRs eligible for polling (status/build/comment checks) — excludes terminal statuses
|
|
4618
5301
|
const PR_POLLABLE_STATUSES = new Set([PR_STATUS.ACTIVE, PR_STATUS.LINKED]);
|
|
5302
|
+
|
|
5303
|
+
// ─── Bounded re-probe of abandoned PRs (W-msav5edk00g81710) ─────────────────
|
|
5304
|
+
//
|
|
5305
|
+
// `abandoned` is the one terminal status a PR can legitimately leave: both
|
|
5306
|
+
// GitHub and ADO allow reopening a closed PR. Filtering the poll loop on
|
|
5307
|
+
// PR_POLLABLE_STATUSES alone made the stamp a one-way door — the reopen handler
|
|
5308
|
+
// in engine/providers/github.js and engine/ado/index.js (P-f5reopenreset) was
|
|
5309
|
+
// unreachable because the record never got polled again, and the only un-flip
|
|
5310
|
+
// path was a boot-only, version-gated reconciliation pass.
|
|
5311
|
+
//
|
|
5312
|
+
// The seam below is the fix: an abandoned record stays pollable for a bounded
|
|
5313
|
+
// window measured from when it was abandoned, so the HOST gets to un-flip it.
|
|
5314
|
+
// Every caller funnels through `isPrPollable` so the two providers cannot drift.
|
|
5315
|
+
|
|
5316
|
+
/** Operator override for the bounded window; honors a literal 0 (disable). */
|
|
5317
|
+
function resolveAbandonedPrRepollWindowMs(config) {
|
|
5318
|
+
const raw = config?.engine?.abandonedPrRepollWindowMs;
|
|
5319
|
+
const n = Number(raw);
|
|
5320
|
+
if (Number.isFinite(n) && n >= 0) return n;
|
|
5321
|
+
return ENGINE_DEFAULTS.abandonedPrRepollWindowMs;
|
|
5322
|
+
}
|
|
5323
|
+
|
|
5324
|
+
/**
|
|
5325
|
+
* Instant (ms) a record entered `abandoned`, or null when unknown.
|
|
5326
|
+
* `_abandonedAt` is stamped at every abandon site; `closedAt` is the fallback
|
|
5327
|
+
* for records stamped before this WI shipped (the 404 trapdoor never set it).
|
|
5328
|
+
*/
|
|
5329
|
+
function getPrAbandonedAtMs(pr) {
|
|
5330
|
+
for (const raw of [pr?._abandonedAt, pr?.closedAt]) {
|
|
5331
|
+
if (!raw) continue;
|
|
5332
|
+
const parsed = Date.parse(raw);
|
|
5333
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
5334
|
+
}
|
|
5335
|
+
return null;
|
|
5336
|
+
}
|
|
5337
|
+
|
|
5338
|
+
/**
|
|
5339
|
+
* Single pollability predicate for both PR pollers.
|
|
5340
|
+
*
|
|
5341
|
+
* @param {object} pr
|
|
5342
|
+
* @param {object} [options]
|
|
5343
|
+
* @param {number} [options.windowMs] — bounded abandoned re-probe window. Defaults
|
|
5344
|
+
* to 0 (strict PR_POLLABLE_STATUSES) so widening is an explicit per-call-site
|
|
5345
|
+
* opt-in: only the STATUS poll needs the reopen signal, never the comment poll.
|
|
5346
|
+
* @param {number} [options.now]
|
|
5347
|
+
*/
|
|
5348
|
+
function isPrPollable(pr, options = {}) {
|
|
5349
|
+
if (!pr || typeof pr !== 'object') return false;
|
|
5350
|
+
if (PR_POLLABLE_STATUSES.has(pr.status)) return true;
|
|
5351
|
+
if (pr.status !== PR_STATUS.ABANDONED) return false;
|
|
5352
|
+
|
|
5353
|
+
const windowMs = Number(options.windowMs) || 0;
|
|
5354
|
+
if (!(windowMs > 0)) return false;
|
|
5355
|
+
// Already proven gone at the host — re-probing a deleted PR is pure waste.
|
|
5356
|
+
if (pr._reconciliation404Confirmed) return false;
|
|
5357
|
+
|
|
5358
|
+
const abandonedAt = getPrAbandonedAtMs(pr);
|
|
5359
|
+
// Fail CLOSED: without an anchor the age is unbounded, and an unbounded age
|
|
5360
|
+
// would turn a bounded window into unbounded re-probing of every historical
|
|
5361
|
+
// record. Those are healed by the version-gated startup pass instead.
|
|
5362
|
+
if (abandonedAt === null) return false;
|
|
5363
|
+
|
|
5364
|
+
const now = Number.isFinite(options.now) ? options.now : Date.now();
|
|
5365
|
+
return (now - abandonedAt) <= windowMs;
|
|
5366
|
+
}
|
|
5367
|
+
|
|
5368
|
+
/** Stamp the anchor the re-probe window is measured from. Returns true if changed. */
|
|
5369
|
+
function stampPrAbandonedAt(pr, at) {
|
|
5370
|
+
if (!pr || typeof pr !== 'object') return false;
|
|
5371
|
+
const iso = at || new Date().toISOString();
|
|
5372
|
+
if (pr._abandonedAt === iso) return false;
|
|
5373
|
+
pr._abandonedAt = iso;
|
|
5374
|
+
return true;
|
|
5375
|
+
}
|
|
5376
|
+
|
|
5377
|
+
/** Drop the anchor when a record leaves `abandoned`. Returns true if changed. */
|
|
5378
|
+
function clearPrAbandonedAt(pr) {
|
|
5379
|
+
if (!pr || typeof pr !== 'object') return false;
|
|
5380
|
+
if (!Object.prototype.hasOwnProperty.call(pr, '_abandonedAt')) return false;
|
|
5381
|
+
delete pr._abandonedAt;
|
|
5382
|
+
return true;
|
|
5383
|
+
}
|
|
4619
5384
|
const PR_PENDING_REASON = {
|
|
4620
5385
|
MISSING_BRANCH: 'missing_pr_branch',
|
|
4621
5386
|
};
|
|
4622
5387
|
// PR build-status enum — single source of truth for the literal strings written to
|
|
4623
|
-
// PR `buildStatus`. Previously drifted across engine/ado.js,
|
|
4624
|
-
// engine/github.js, engine/lifecycle.js, engine/watches.js, engine/queries.js, engine/cli.js
|
|
5388
|
+
// PR `buildStatus`. Previously drifted across engine/ado/index.js,
|
|
5389
|
+
// engine/providers/github.js, engine/orchestration/lifecycle.js, engine/watches/index.js, engine/core/queries.js, engine/operations/cli.js
|
|
4625
5390
|
// (P-bfa3d-constants-eslint, audit items #68-#82).
|
|
4626
5391
|
const BUILD_STATUS = {
|
|
4627
5392
|
PASSING: 'passing',
|
|
@@ -4630,8 +5395,8 @@ const BUILD_STATUS = {
|
|
|
4630
5395
|
NONE: 'none',
|
|
4631
5396
|
};
|
|
4632
5397
|
// PR review-status enum — single source of truth for the literal strings written to
|
|
4633
|
-
// PR `reviewStatus`. Previously drifted across engine/ado.js,
|
|
4634
|
-
// engine/github.js, engine/lifecycle.js, engine/watches.js, engine/queries.js, engine/cli.js
|
|
5398
|
+
// PR `reviewStatus`. Previously drifted across engine/ado/index.js,
|
|
5399
|
+
// engine/providers/github.js, engine/orchestration/lifecycle.js, engine/watches/index.js, engine/core/queries.js, engine/operations/cli.js
|
|
4635
5400
|
// (P-bfa3d-constants-eslint, audit items #68-#82).
|
|
4636
5401
|
const REVIEW_STATUS = {
|
|
4637
5402
|
APPROVED: 'approved',
|
|
@@ -4644,15 +5409,15 @@ const REVIEW_STATUS = {
|
|
|
4644
5409
|
// the migrated sites read self-documentingly and drift is grep-able.
|
|
4645
5410
|
// (P-bfa3d-constants-eslint, audit items #68-#82).
|
|
4646
5411
|
const FETCH_TIMEOUT_MS = {
|
|
4647
|
-
ADO_API: 4000, // engine/ado.js — ADO REST API single-shot fetches
|
|
4648
|
-
GH_CLI: 15000, // engine/github.js, engine/lifecycle.js — `gh pr/api` shell-outs
|
|
4649
|
-
GH_COMMENT: 30000, // engine/gh-comment.js — comment/review posts (slower endpoint)
|
|
5412
|
+
ADO_API: 4000, // engine/ado/index.js — ADO REST API single-shot fetches
|
|
5413
|
+
GH_CLI: 15000, // engine/providers/github.js, engine/orchestration/lifecycle.js — `gh pr/api` shell-outs
|
|
5414
|
+
GH_COMMENT: 30000, // engine/providers/gh-comment.js — comment/review posts (slower endpoint)
|
|
4650
5415
|
};
|
|
4651
|
-
// Retry delay between auto-link fallback attempts in engine/lifecycle.js#resolvePrLinkFallback.
|
|
5416
|
+
// Retry delay between auto-link fallback attempts in engine/orchestration/lifecycle.js#resolvePrLinkFallback.
|
|
4652
5417
|
// Previously a bare `3000` magic number; named so the cadence is grep-able and consistent
|
|
4653
5418
|
// if other retry paths need the same backoff. (P-bfa3d-constants-eslint).
|
|
4654
5419
|
const RETRY_DELAY_MS = 3000;
|
|
4655
|
-
// Max retries for ADO token-refresh inside engine/ado.js#adoFetch.
|
|
5420
|
+
// Max retries for ADO token-refresh inside engine/ado/index.js#adoFetch.
|
|
4656
5421
|
// Distinct from ENGINE_DEFAULTS.maxRetries (dispatch-level cap) — this is the
|
|
4657
5422
|
// per-request token-refresh ceiling and is intentionally separate.
|
|
4658
5423
|
// (P-bfa3d-constants-eslint).
|
|
@@ -4664,7 +5429,7 @@ const WATCH_STATUS = { ACTIVE: 'active', PAUSED: 'paused', TRIGGERED: 'triggered
|
|
|
4664
5429
|
// engine/watches.registerTargetType — this object lists the types shipped in
|
|
4665
5430
|
// the engine for use in switch statements and constants comparisons. The
|
|
4666
5431
|
// actual allowlist for createWatch/dashboard validation is the registry in
|
|
4667
|
-
// engine/watches.js (see getTargetTypes()).
|
|
5432
|
+
// engine/watches/index.js (see getTargetTypes()).
|
|
4668
5433
|
const WATCH_TARGET_TYPE = {
|
|
4669
5434
|
PR: 'pr',
|
|
4670
5435
|
WORK_ITEM: 'work-item',
|
|
@@ -4695,14 +5460,14 @@ const WATCH_CONDITION = {
|
|
|
4695
5460
|
DISABLED: 'disabled', // schedule disabled
|
|
4696
5461
|
ACTIVITY_CHANGE: 'activity-change', // agent transitioned status (e.g. idle → working)
|
|
4697
5462
|
// ── P-w4e2f6a1 — Phase 2.1: PR predicate conditions ──────────────────────
|
|
4698
|
-
// See engine/watches.js PR target type for trigger semantics.
|
|
5463
|
+
// See engine/watches/index.js PR target type for trigger semantics.
|
|
4699
5464
|
HEAD_COMMIT_CHANGE: 'head-commit-change', // PR headRefOid advanced (new push)
|
|
4700
5465
|
MERGEABLE_FLIPPED: 'mergeable-flipped', // mergeable transitioned between true↔false (NOT involving null)
|
|
4701
5466
|
READY_FOR_MERGE: 'ready-for-merge', // canonical compound: active+approved+passing+mergeable+!draft
|
|
4702
5467
|
BEHIND_MASTER: 'behind-master', // pr.behindBy > 0 (treats null/undefined as not-behind)
|
|
4703
5468
|
DRAFT_FLIPPED: 'draft-flipped', // isDraft transitioned between true↔false
|
|
4704
5469
|
// ── P-w5b8d2c9 — Phase 2.2: work-item / plan / pipeline predicates ───────
|
|
4705
|
-
// See engine/watches.js for trigger semantics. Counters (_unchangedTicks,
|
|
5470
|
+
// See engine/watches/index.js for trigger semantics. Counters (_unchangedTicks,
|
|
4706
5471
|
// _stuckStageTicks) are computed inside captureState by comparing the
|
|
4707
5472
|
// freshly-captured snapshot against prevState — see _captureState which
|
|
4708
5473
|
// now passes prevState as the 2nd arg.
|
|
@@ -4740,7 +5505,7 @@ const WATCH_ABSOLUTE_CONDITIONS = new Set([
|
|
|
4740
5505
|
WATCH_CONDITION.ITEM_FAILED_N_TIMES,
|
|
4741
5506
|
]);
|
|
4742
5507
|
// Built-in follow-up action types invoked by the engine when a watch fires.
|
|
4743
|
-
// The action registry in engine/
|
|
5508
|
+
// The action registry in engine/watches/actions.js is the source of truth; these
|
|
4744
5509
|
// constants exist so engine/dashboard code can reference action keys without
|
|
4745
5510
|
// magic strings. New action types can be registered at runtime via
|
|
4746
5511
|
// registerActionType() — unknown keys here are fine if registered elsewhere.
|
|
@@ -4772,10 +5537,10 @@ const WATCH_ACTION_TYPE = {
|
|
|
4772
5537
|
*/
|
|
4773
5538
|
function _smallStateMutator({ applyMutation, topic }) {
|
|
4774
5539
|
return (mutator) => {
|
|
4775
|
-
const store = require('
|
|
5540
|
+
const store = require('../persistence/small-state-store');
|
|
4776
5541
|
const { wrote, result } = store[applyMutation]((obj) => mutator(obj) || obj);
|
|
4777
5542
|
if (wrote) {
|
|
4778
|
-
try { require('
|
|
5543
|
+
try { require('../persistence/db-events').emitStateEvent(topic); } catch { /* optional */ }
|
|
4779
5544
|
}
|
|
4780
5545
|
return result;
|
|
4781
5546
|
};
|
|
@@ -4808,13 +5573,13 @@ const mutateWorktreePool = _smallStateMutator({
|
|
|
4808
5573
|
/** Route QA array mutations through their SQL stores. */
|
|
4809
5574
|
function _qaMutator({ applyMutation, topic }) {
|
|
4810
5575
|
return (mutator) => {
|
|
4811
|
-
const store = require('
|
|
5576
|
+
const store = require('../persistence/small-state-store');
|
|
4812
5577
|
const { wrote, result } = store[applyMutation]((arr) => {
|
|
4813
5578
|
if (!Array.isArray(arr)) arr = [];
|
|
4814
5579
|
return mutator(arr) || arr;
|
|
4815
5580
|
});
|
|
4816
5581
|
if (wrote) {
|
|
4817
|
-
try { require('
|
|
5582
|
+
try { require('../persistence/db-events').emitStateEvent(topic); } catch { /* optional */ }
|
|
4818
5583
|
}
|
|
4819
5584
|
return result;
|
|
4820
5585
|
};
|
|
@@ -4837,26 +5602,26 @@ const mutateQaSessions = _qaMutator({
|
|
|
4837
5602
|
* diffs by id. SQL-canonical (Phase 9.4); SQLite failures propagate.
|
|
4838
5603
|
*/
|
|
4839
5604
|
function mutateWatches(mutator) {
|
|
4840
|
-
const store = require('
|
|
5605
|
+
const store = require('../watches/store');
|
|
4841
5606
|
const { wrote, result } = store.applyWatchesMutation((arr) => {
|
|
4842
5607
|
if (!Array.isArray(arr)) arr = [];
|
|
4843
5608
|
return mutator(arr) || arr;
|
|
4844
5609
|
});
|
|
4845
5610
|
if (wrote) {
|
|
4846
|
-
try { require('
|
|
5611
|
+
try { require('../persistence/db-events').emitStateEvent('watches'); } catch { /* optional */ }
|
|
4847
5612
|
}
|
|
4848
5613
|
return result;
|
|
4849
5614
|
}
|
|
4850
5615
|
|
|
4851
5616
|
/** Route a metrics object mutation through the SQL store. */
|
|
4852
5617
|
function mutateMetrics(mutator) {
|
|
4853
|
-
const store = require('
|
|
5618
|
+
const store = require('../observability/metrics-store');
|
|
4854
5619
|
const { wrote, result } = store.applyMetricsMutation((m) => {
|
|
4855
5620
|
if (!m || typeof m !== 'object') m = {};
|
|
4856
5621
|
return mutator(m) || m;
|
|
4857
5622
|
});
|
|
4858
5623
|
if (wrote) {
|
|
4859
|
-
try { require('
|
|
5624
|
+
try { require('../persistence/db-events').emitStateEvent('metrics'); } catch { /* optional */ }
|
|
4860
5625
|
}
|
|
4861
5626
|
return result;
|
|
4862
5627
|
}
|
|
@@ -5004,6 +5769,7 @@ const FAILURE_CLASS = {
|
|
|
5004
5769
|
MERGE_CONFLICT: 'merge-conflict', // Git merge conflict in worktree or dependency
|
|
5005
5770
|
BUILD_FAILURE: 'build-failure', // Compilation, lint, or test failure
|
|
5006
5771
|
TIMEOUT: 'timeout', // Hard runtime timeout or stale-orphan timeout
|
|
5772
|
+
ENGINE_RESTART: 'engine-restart', // Dispatch process ownership was lost across an engine restart; use the infrastructure retry budget
|
|
5007
5773
|
EMPTY_OUTPUT: 'empty-output', // Agent produced no meaningful output
|
|
5008
5774
|
SPAWN_ERROR: 'spawn-error', // Process failed to start or crashed immediately
|
|
5009
5775
|
NETWORK_ERROR: 'network-error', // API rate limit, DNS, connectivity
|
|
@@ -5031,6 +5797,9 @@ const FAILURE_CLASS = {
|
|
|
5031
5797
|
LIVE_CHECKOUT_STALE_BASE: 'live-checkout-stale-base', // W-mr98op8w000ma4ad (live-checkout dispatch mode, STALE-BASE GUARD): spawnAgent was about to fork a NEW branch off the operator's current HEAD while HEAD sits on the project base ref (mainRef), but the LOCAL mainRef tip has DIVERGED from origin/<mainRef> with unpushed commit(s) (`git rev-list --count origin/<mainRef>..<mainRef>` > 0). Forking would silently inherit that COMMITTED contamination into the new branch and its PR — the exact scope-contamination class first seen on ADO PR 5411214 and recurring on AB#12016662 / ADO PR 5419146. Survives the dirty check (contamination is committed, not uncommitted) and cannot be caught by a headBranch !== mainRef check (headBranch IS mainRef here). Detected WITHOUT a fetch (issue #226) via the existing origin/<mainRef> remote-tracking ref. Distinct from LIVE_CHECKOUT_FAILED (transient) because the contaminated local base does NOT change on retry — it reproduces identically until a human reconciles the local base branch. Non-retryable — the engine NEVER resets/cleans the operator's local mainRef; the operator must reconcile it (push/reset/rebase their own way), then re-dispatch. The guard SKIPS (proceeds normally) when HEAD is not on mainRef, when no local origin/<mainRef> ref exists (local-only/no-remote repo), or when the divergence count cannot be computed.
|
|
5032
5798
|
LIVE_CHECKOUT_WRONG_BASE: 'live-checkout-wrong-base', // W-mr3lunnq000o9f41 (live-checkout dispatch mode): a NEW-branch fork was requested but the operator's checkout HEAD is on an unrelated topic/feature branch, NOT the project base (mainRef), and prepareLiveCheckout could not switch to a LOCAL base ref (no `refs/heads/<mainRef>`, or the local checkout itself failed). Forking off the stale HEAD would silently bake every commit already on that branch into the new branch AND its PR — committed-history scope contamination that survives the dirty/mid-operation preflights (the ADO PR 5411214 incident: ~22 unrelated OCM files, author-disavowed). The engine never fetches origin/<mainRef> in live mode (issue #226) so it cannot self-heal a missing local base. Non-retryable — the operator must `git checkout <mainRef>` in the operator checkout before re-dispatch. Does NOT fire for PR-targeted fixes or shared-branch continuations (those pass allowNonMainBase, intentionally continuing a non-main branch), nor when HEAD is already on mainRef, nor when a local mainRef ref exists (the guard switches to it and forks cleanly). OPT-IN AUTO-RECOVERY (`liveCheckoutAutoBaseRepair`, per-project > fleet-wide, default OFF): when enabled AND a `refs/remotes/origin/<mainRef>` remote-tracking ref exists, the guard materializes a local `<mainRef>` branch from it (a LOCAL ref op — still no `git fetch`, issue #226 preserved; origin/<mainRef> IS the base so no contamination) and forks cleanly instead of failing — eliminating the manual `git checkout <mainRef>` step. Still fails closed when neither a local base nor an origin-tracking base ref exists, or when the base checkout itself fails/hangs.
|
|
5033
5799
|
INVALID_WORKDIR: 'invalid-workdir', // P-714ef144: dispatch carried a meta.workdir override that failed validation — non-string, absolute path, drive-letter prefix, null byte, ".." segment, or post-resolve containment escape against project.localPath / worktree root. Engine refuses to spawn (the subpath would either be unreachable on disk or point outside the operator's allowed surface). Non-retryable — operator must fix the WI's meta.workdir before re-dispatch. Inbox alert lists the offending value + the resolved-vs-base mismatch.
|
|
5800
|
+
INVALID_EXECUTION_SURFACE: 'invalid-execution-surface', // W-ms5tb6ha016fda10: the dispatch declared `meta.executionSurface` that failed fail-closed validation — an unknown surface string, a non-string value, `control-plane` on a work type that reads/mutates repository files (fix/implement/review/test/verify/decompose/docs/build-fix-complex), or `cloud` on a work type whose deliverable is not a pull request (review/verify/explore/ask/setup/decompose/build-fix-complex). Each surface's opt-in is an ALLOWLIST (shared.CONTROL_PLANE_ELIGIBLE_TYPES / shared.CLOUD_ELIGIBLE_TYPES), never inferred from prose, so a repo-mutating task can never silently lose its checkout and a validation task can never silently be shipped to a remote agent that cannot build it. Non-retryable — the operator must fix or remove the field before re-dispatch.
|
|
5801
|
+
CLOUD_PROVIDER_UNAVAILABLE: 'cloud-provider-unavailable', // Cloud dispatch surface: the work item validly declared `meta.executionSurface: "cloud"` but no usable cloud provider could be resolved — the id is not registered, the provider is registered but reports `isConfigured() === false` for this project (missing credentials, no repo binding, disabled in config), or provider resolution threw. Structural, NOT connectivity: retrying the same dispatch reproduces it identically until an operator configures the provider or removes the surface declaration. Non-retryable. Deliberately distinct from a provider call that reached the service and failed — that is a session-level failure classified by the provider, not this.
|
|
5802
|
+
GIT_REF_LOCK: 'git-ref-lock', // W-ms5tb6ha016fda10: `git fetch` lost a LOCAL ref-lock race on a shared remote-tracking ref (`cannot lock ref 'refs/remotes/origin/<main>': is at <sha> but expected <sha>`) — two dispatches preparing fresh worktrees in the SAME clone at the same time, or an operator/dashboard git command racing the daemon. This is a local serialization defect, NOT connectivity: classifying it as NETWORK_ERROR hid the real cause and burned retries. In-process races are removed at the source by shared.withSerializedRemoteRefRefresh; this class covers the residual cross-process case. Retryable — the ref lock clears in seconds.
|
|
5034
5803
|
MODEL_UNAVAILABLE: 'model-unavailable', // W-mpg6isvy000xca4d: requested model returned overloaded_error / 503 / service_unavailable. Retriable — engine swaps in the runtime-appropriate fallback model on next spawn (Claude leans on --fallback-model already plumbed; Copilot overrides --model with engine.copilotFallbackModel).
|
|
5035
5804
|
WORKSPACE_MANIFEST_REPO: 'workspace-manifest-repo-forbidden', // W-mq07avbk000m5543: dispatch routed an agent to a project/repo not present in its workspace_manifest.allowed_repos. Structural — never retryable until the manifest is widened or a different agent is chosen.
|
|
5036
5805
|
WORKSPACE_MANIFEST_TOOL: 'workspace-manifest-tool-forbidden', // W-mq07avbk000m5543: out-of-scope tool call (manifest enforcement at the runtime gate). Non-retryable as-is.
|
|
@@ -5095,6 +5864,44 @@ function resolveTerminalWorkerState({ code, effectiveResult, errorReason } = {})
|
|
|
5095
5864
|
};
|
|
5096
5865
|
}
|
|
5097
5866
|
|
|
5867
|
+
// W-ms9tcs3o01y03383 — how a dispatch reached its terminal state. Persisted on
|
|
5868
|
+
// the completed dispatch record so "finished normally", "recovered from a
|
|
5869
|
+
// durable report" and "a live resume genuinely stalled" are distinguishable
|
|
5870
|
+
// after the fact instead of all reading as a plain success/timeout.
|
|
5871
|
+
const DISPATCH_COMPLETION_PATH = {
|
|
5872
|
+
// The tracked process closed and the normal close handler finalized it.
|
|
5873
|
+
PROCESS_CLOSE: 'process-close',
|
|
5874
|
+
// The process had provably exited and a dispatch-owned completion report was
|
|
5875
|
+
// already on disk, so the close handler skipped queued resume bookkeeping and
|
|
5876
|
+
// finalized straight away.
|
|
5877
|
+
REPORT_RECOVERY_IMMEDIATE: 'report-recovery-immediate',
|
|
5878
|
+
// The engine lost/never had the close event and recovered the outcome from
|
|
5879
|
+
// the [process-exit] sentinel in live-output.log (restart reattachment).
|
|
5880
|
+
OUTPUT_DETECTION: 'output-detection',
|
|
5881
|
+
// A still-live resumed runtime blew its first-output heartbeat budget, but a
|
|
5882
|
+
// dispatch-owned report proved the turn had already succeeded.
|
|
5883
|
+
RESUME_STALL_REPORT: 'resume-stall-report',
|
|
5884
|
+
// A still-live resumed runtime blew its heartbeat budget with no report.
|
|
5885
|
+
RESUME_STALL_TIMEOUT: 'resume-stall-timeout',
|
|
5886
|
+
};
|
|
5887
|
+
|
|
5888
|
+
// W-ms9tcs3o01y03383 — the completion-report trust boundary, expressed once.
|
|
5889
|
+
//
|
|
5890
|
+
// A completion report is only evidence about THIS dispatch when it echoes the
|
|
5891
|
+
// per-spawn nonce the engine minted (and carries across steering resumes). This
|
|
5892
|
+
// is the pure half of the check — callers supply the parsed report, so the same
|
|
5893
|
+
// decision is made by the close handler, the resume-stall watchdog, and the
|
|
5894
|
+
// output-detection recovery path without any of them re-deriving the rule.
|
|
5895
|
+
//
|
|
5896
|
+
// Deliberately fails closed: no expected nonce, no report, or a
|
|
5897
|
+
// missing/non-matching `nonce` field all return null.
|
|
5898
|
+
function acceptCompletionReportForNonce(report, expectedNonce) {
|
|
5899
|
+
if (!expectedNonce || typeof expectedNonce !== 'string') return null;
|
|
5900
|
+
if (!isPlainObject(report)) return null;
|
|
5901
|
+
if (typeof report.nonce !== 'string' || report.nonce !== expectedNonce) return null;
|
|
5902
|
+
return report;
|
|
5903
|
+
}
|
|
5904
|
+
|
|
5098
5905
|
const _dispatchFinalizationOwners = new Map();
|
|
5099
5906
|
const _dispatchFinalizationWaiters = new Map();
|
|
5100
5907
|
|
|
@@ -5155,7 +5962,7 @@ const DEFAULT_CLAUDE = {
|
|
|
5155
5962
|
|
|
5156
5963
|
// Bump rolling-daily strip counter for docs/deprecated.json#prune-default-claude-config
|
|
5157
5964
|
// removal gate (>=30 consecutive days of zero strips). Best-effort; mirrors
|
|
5158
|
-
// engine/cleanup.js:1200-1213 _engine.legacyStatusMigrations.
|
|
5965
|
+
// engine/orchestration/cleanup.js:1200-1213 _engine.legacyStatusMigrations.
|
|
5159
5966
|
function _bumpPruneDefaultClaudeStrip() {
|
|
5160
5967
|
try {
|
|
5161
5968
|
const dateKey = new Date().toISOString().slice(0, 10);
|
|
@@ -5662,8 +6469,8 @@ function initializeProjectState(project) {
|
|
|
5662
6469
|
projectStateDirEnsure(project);
|
|
5663
6470
|
// SQL scopes need no empty sentinel rows. Touch both stores so migrations
|
|
5664
6471
|
// and schema validation still happen at project setup time.
|
|
5665
|
-
require('
|
|
5666
|
-
require('
|
|
6472
|
+
require('../planning/work-items-store').readWorkItemsForScope(project.name);
|
|
6473
|
+
require('../persistence/pull-requests-store').readPullRequestsForScope(project.name);
|
|
5667
6474
|
return result;
|
|
5668
6475
|
}
|
|
5669
6476
|
|
|
@@ -5691,6 +6498,62 @@ function realPathForComparison(filePath) {
|
|
|
5691
6498
|
}
|
|
5692
6499
|
}
|
|
5693
6500
|
|
|
6501
|
+
/**
|
|
6502
|
+
* Async twin of `realPathForComparison`, for callers that resolve MANY paths on
|
|
6503
|
+
* a latency-sensitive event loop (the dashboard's worktree inventory refresh).
|
|
6504
|
+
* Same missing-path walk, same error contract.
|
|
6505
|
+
*
|
|
6506
|
+
* Consistency rule: resolve every side of a comparison with the SAME twin.
|
|
6507
|
+
* `fs.realpathSync.native` can canonicalize case on a case-insensitive volume
|
|
6508
|
+
* where the promises API does not, so mixing the two could make an
|
|
6509
|
+
* already-resolved path miscompare on macOS.
|
|
6510
|
+
*/
|
|
6511
|
+
async function realPathForComparisonAsync(filePath) {
|
|
6512
|
+
const resolved = path.resolve(filePath);
|
|
6513
|
+
try {
|
|
6514
|
+
return await fs.promises.realpath(resolved);
|
|
6515
|
+
} catch (err) {
|
|
6516
|
+
if (!err || (err.code !== 'ENOENT' && err.code !== 'ENOTDIR')) throw err;
|
|
6517
|
+
}
|
|
6518
|
+
|
|
6519
|
+
let existing = resolved;
|
|
6520
|
+
const missingParts = [];
|
|
6521
|
+
while (true) {
|
|
6522
|
+
const parent = path.dirname(existing);
|
|
6523
|
+
missingParts.unshift(path.basename(existing));
|
|
6524
|
+
if (parent === existing) return resolved;
|
|
6525
|
+
existing = parent;
|
|
6526
|
+
try {
|
|
6527
|
+
return path.join(await fs.promises.realpath(existing), ...missingParts);
|
|
6528
|
+
} catch (err) {
|
|
6529
|
+
if (!err || (err.code !== 'ENOENT' && err.code !== 'ENOTDIR')) throw err;
|
|
6530
|
+
}
|
|
6531
|
+
}
|
|
6532
|
+
}
|
|
6533
|
+
|
|
6534
|
+
/**
|
|
6535
|
+
* Run `worker` over `items` with at most `limit` in flight. The engine's
|
|
6536
|
+
* canonical bounded-concurrency mapper — use this instead of adding another
|
|
6537
|
+
* private copy of the promise-pool pattern.
|
|
6538
|
+
*/
|
|
6539
|
+
async function mapWithConcurrency(items, limit, worker) {
|
|
6540
|
+
const list = Array.isArray(items) ? items : [];
|
|
6541
|
+
if (list.length === 0) return [];
|
|
6542
|
+
const results = new Array(list.length);
|
|
6543
|
+
let next = 0;
|
|
6544
|
+
const runners = Array.from(
|
|
6545
|
+
{ length: Math.max(1, Math.min(Number(limit) || 1, list.length)) },
|
|
6546
|
+
async () => {
|
|
6547
|
+
while (next < list.length) {
|
|
6548
|
+
const index = next++;
|
|
6549
|
+
results[index] = await worker(list[index], index);
|
|
6550
|
+
}
|
|
6551
|
+
},
|
|
6552
|
+
);
|
|
6553
|
+
await Promise.all(runners);
|
|
6554
|
+
return results;
|
|
6555
|
+
}
|
|
6556
|
+
|
|
5694
6557
|
// ── ID Generation ────────────────────────────────────────────────────────────
|
|
5695
6558
|
|
|
5696
6559
|
function nextWorkItemId(items, prefix) {
|
|
@@ -5864,17 +6727,37 @@ function _xmlEscapeForPrompt(s) {
|
|
|
5864
6727
|
}
|
|
5865
6728
|
|
|
5866
6729
|
// Render a list of skills as an <available_skills> XML block matching the
|
|
5867
|
-
// shape Copilot CLI's default system prompt injects. When
|
|
5868
|
-
// we still emit the block (with a sentinel comment
|
|
5869
|
-
//
|
|
5870
|
-
|
|
5871
|
-
|
|
6730
|
+
// shape Copilot CLI's default system prompt injects. When both lists are empty
|
|
6731
|
+
// we still emit the block (with a sentinel comment naming every source that was
|
|
6732
|
+
// searched — user, plugin, and project checkouts) so the agent learns the paths
|
|
6733
|
+
// exist rather than silently inferring nothing's there.
|
|
6734
|
+
//
|
|
6735
|
+
// `skills` are the runtime-native user/plugin skills (from listCopilotSkills).
|
|
6736
|
+
// `projectSkills` (optional) are project-embedded skills discovered under the
|
|
6737
|
+
// configured project checkouts (incl. deeply-nested `.claude/skills` dirs) so a
|
|
6738
|
+
// request like "run my weekly maintenance skill" resolves from the registry
|
|
6739
|
+
// instead of an ad-hoc filesystem grep. User/plugin skills win a name collision
|
|
6740
|
+
// (mirrors listCopilotSkills' user-wins precedence); each entry may carry a
|
|
6741
|
+
// `path` that is rendered as a `<path>` element so the agent can open it directly.
|
|
6742
|
+
function buildAvailableSkillsBlock(skills, projectSkills) {
|
|
6743
|
+
const userList = Array.isArray(skills) ? skills : [];
|
|
6744
|
+
const projList = Array.isArray(projectSkills) ? projectSkills : [];
|
|
6745
|
+
const byName = new Map();
|
|
6746
|
+
for (const s of userList) if (s && s.name && !byName.has(s.name)) byName.set(s.name, s);
|
|
6747
|
+
for (const s of projList) if (s && s.name && !byName.has(s.name)) byName.set(s.name, s);
|
|
6748
|
+
const list = Array.from(byName.values());
|
|
5872
6749
|
if (list.length === 0) {
|
|
5873
|
-
return '<available_skills>\n <!-- No skills found in ~/.copilot/skills
|
|
5874
|
-
}
|
|
5875
|
-
const items = list.map(s =>
|
|
5876
|
-
|
|
5877
|
-
|
|
6750
|
+
return '<available_skills>\n <!-- No skills found in ~/.copilot/skills/, ~/.copilot/installed-plugins/<marketplace>/<plugin>/skills/, or any configured project checkout -->\n</available_skills>';
|
|
6751
|
+
}
|
|
6752
|
+
const items = list.map(s => {
|
|
6753
|
+
const parts = [
|
|
6754
|
+
` <name>${_xmlEscapeForPrompt(s.name)}</name>`,
|
|
6755
|
+
` <description>${_xmlEscapeForPrompt(s.description)}</description>`,
|
|
6756
|
+
` <location>${_xmlEscapeForPrompt(s.location)}</location>`,
|
|
6757
|
+
];
|
|
6758
|
+
if (s.path) parts.push(` <path>${_xmlEscapeForPrompt(s.path)}</path>`);
|
|
6759
|
+
return `<skill>\n${parts.join('\n')}\n</skill>`;
|
|
6760
|
+
}).join('\n');
|
|
5878
6761
|
return `<available_skills>\n${items}\n</available_skills>`;
|
|
5879
6762
|
}
|
|
5880
6763
|
|
|
@@ -5893,10 +6776,15 @@ function renderCcSystemPrompt(raw, opts) {
|
|
|
5893
6776
|
// Available-skills enumeration walks two directory trees and reads each
|
|
5894
6777
|
// SKILL.md's frontmatter — only do it when the template asks for it.
|
|
5895
6778
|
// Tests pass `opts.skillsHomeDir` to redirect the scan at a fixture dir.
|
|
6779
|
+
// `opts.projectSkills` (optional) carries project-embedded skills discovered
|
|
6780
|
+
// under configured project checkouts so they appear in the CC registry
|
|
6781
|
+
// alongside user/plugin skills (the caller computes them from the cached
|
|
6782
|
+
// queries skill inventory — this stays off the /api/status hot path).
|
|
5896
6783
|
if (out.includes('{{available_skills}}')) {
|
|
5897
6784
|
const skillsOpts = (opts && typeof opts.skillsHomeDir === 'string')
|
|
5898
6785
|
? { homeDir: opts.skillsHomeDir } : undefined;
|
|
5899
|
-
|
|
6786
|
+
const projectSkills = (opts && Array.isArray(opts.projectSkills)) ? opts.projectSkills : [];
|
|
6787
|
+
out = out.replace(/\{\{available_skills\}\}/g, buildAvailableSkillsBlock(listCopilotSkills(skillsOpts), projectSkills));
|
|
5900
6788
|
}
|
|
5901
6789
|
return out
|
|
5902
6790
|
.replace(/\{\{cc_turn_id\}\}/g, turnId)
|
|
@@ -5950,7 +6838,7 @@ function isLiveCommandCenterPath(absPath, opts) {
|
|
|
5950
6838
|
if (rel === '' || rel === '.') return false; // root itself is not a "file"
|
|
5951
6839
|
const relForMatch = rel.toLowerCase();
|
|
5952
6840
|
if (_CC_PROTECTED_BASENAMES.includes(relForMatch)) return true;
|
|
5953
|
-
if (/^(
|
|
6841
|
+
if (/^engine\/.+\.js$/.test(relForMatch) || /^bin\/[^/]+\.js$/.test(relForMatch)) return true;
|
|
5954
6842
|
for (const prefix of _CC_PROTECTED_PREFIXES) {
|
|
5955
6843
|
if (relForMatch === prefix.slice(0, -1) /* exact dir */ || relForMatch.startsWith(prefix)) return true;
|
|
5956
6844
|
}
|
|
@@ -6080,7 +6968,7 @@ function sanitizeBranch(name) {
|
|
|
6080
6968
|
// gone from engine.js. PR-targeted dispatches and `shared-branch` plans bypass
|
|
6081
6969
|
// this helper entirely (they reuse the existing branch).
|
|
6082
6970
|
//
|
|
6083
|
-
// `getOperatorLogin` is a thin shim around `engine/operator-identity` so other
|
|
6971
|
+
// `getOperatorLogin` is a thin shim around `engine/core/operator-identity` so other
|
|
6084
6972
|
// modules don't need a second require. Required lazily to keep shared.js free
|
|
6085
6973
|
// of side-effecting child_process imports at module load.
|
|
6086
6974
|
|
|
@@ -6098,6 +6986,18 @@ function deriveWorkItemBranchName(item, config) {
|
|
|
6098
6986
|
// `config` are intentionally unused here but retained for callers that
|
|
6099
6987
|
// may still inspect them.
|
|
6100
6988
|
void config;
|
|
6989
|
+
// W-ms5tb6ha016fda10 — a validly-declared control-plane item owns no branch:
|
|
6990
|
+
// it never checks out a repository, so stamping `work/<wi-id>` on it would
|
|
6991
|
+
// both mislead the dashboard and put it in the branch mutex, needlessly
|
|
6992
|
+
// serializing maintenance jobs that share nothing. Fail-closed by
|
|
6993
|
+
// construction (isControlPlaneExecution rejects ineligible types), so a
|
|
6994
|
+
// repo-mutating item still derives its branch exactly as before.
|
|
6995
|
+
if (isControlPlaneExecution(item)) return null;
|
|
6996
|
+
// A cloud-dispatched item owns no LOCAL branch either: the remote agent
|
|
6997
|
+
// creates and pushes its own. Stamping `work/<wi-id>` here would put the
|
|
6998
|
+
// item in the local branch mutex (serializing dispatches that share no
|
|
6999
|
+
// checkout) and would advertise a branch that never exists locally.
|
|
7000
|
+
if (isCloudExecution(item)) return null;
|
|
6101
7001
|
const wid = String(item?.id || '').trim();
|
|
6102
7002
|
if (!wid) return null;
|
|
6103
7003
|
return sanitizeBranch(`work/${wid}`);
|
|
@@ -6127,7 +7027,7 @@ function buildWorktreeDirName({
|
|
|
6127
7027
|
// so the dir name is a single basename. Without this, `path.join(parent,
|
|
6128
7028
|
// dirName)` creates `parent/work/W-…/` on Linux for a `work/W-…` branch,
|
|
6129
7029
|
// and `readdirSync(parent)` returns `work` (not the full name) — breaking
|
|
6130
|
-
// engine/
|
|
7030
|
+
// engine/worktrees/gc.js's globalLiveDirNames lookup and evicting live
|
|
6131
7031
|
// worktrees on boot.
|
|
6132
7032
|
const branchSlug = sanitizeBranch(branchName || 'worktree').replace(/\//g, '-');
|
|
6133
7033
|
return `${projectSlug}-${branchSlug}-${suffix}`;
|
|
@@ -6153,6 +7053,40 @@ function _pathForComparison(filePath) {
|
|
|
6153
7053
|
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
6154
7054
|
}
|
|
6155
7055
|
|
|
7056
|
+
/**
|
|
7057
|
+
* Public name for the comparison form of a path: fully symlink-resolved and
|
|
7058
|
+
* case-folded on Windows. Callers that must compare many paths off the event
|
|
7059
|
+
* loop resolve once with `pathForComparisonAsync` and then compare with
|
|
7060
|
+
* `isResolvedPathInsideOrEqual`, instead of paying a realpath syscall inside
|
|
7061
|
+
* every containment test.
|
|
7062
|
+
*/
|
|
7063
|
+
function pathForComparison(filePath) {
|
|
7064
|
+
return _pathForComparison(filePath);
|
|
7065
|
+
}
|
|
7066
|
+
|
|
7067
|
+
async function pathForComparisonAsync(filePath) {
|
|
7068
|
+
const resolved = await realPathForComparisonAsync(String(filePath));
|
|
7069
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
7070
|
+
}
|
|
7071
|
+
|
|
7072
|
+
/**
|
|
7073
|
+
* The containment rule, over paths that are ALREADY in comparison form.
|
|
7074
|
+
*
|
|
7075
|
+
* This is the single definition of "inside or equal" — `isPathInside` and
|
|
7076
|
+
* `isPathInsideOrEqual` both delegate here after normalizing, so a caller that
|
|
7077
|
+
* pre-resolves paths asynchronously applies exactly the same rule and there is
|
|
7078
|
+
* no second copy to drift.
|
|
7079
|
+
*/
|
|
7080
|
+
function isResolvedPathInsideOrEqual(childAbs, parentAbs) {
|
|
7081
|
+
if (!childAbs || !parentAbs) return false;
|
|
7082
|
+
if (childAbs === parentAbs) return true;
|
|
7083
|
+
const rel = path.relative(parentAbs, childAbs);
|
|
7084
|
+
if (rel === '') return false;
|
|
7085
|
+
if (rel.startsWith('..')) return false;
|
|
7086
|
+
if (path.isAbsolute(rel)) return false;
|
|
7087
|
+
return true;
|
|
7088
|
+
}
|
|
7089
|
+
|
|
6156
7090
|
/**
|
|
6157
7091
|
* True when `childPath` is strictly nested within `parentPath` (descendant,
|
|
6158
7092
|
* NOT the same path). Cross-platform via `path.relative`; resilient to mixed
|
|
@@ -6163,11 +7097,8 @@ function isPathInside(childPath, parentPath) {
|
|
|
6163
7097
|
if (!childPath || !parentPath) return false;
|
|
6164
7098
|
const childAbs = _pathForComparison(childPath);
|
|
6165
7099
|
const parentAbs = _pathForComparison(parentPath);
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
if (rel.startsWith('..')) return false;
|
|
6169
|
-
if (path.isAbsolute(rel)) return false;
|
|
6170
|
-
return true;
|
|
7100
|
+
if (childAbs === parentAbs) return false;
|
|
7101
|
+
return isResolvedPathInsideOrEqual(childAbs, parentAbs);
|
|
6171
7102
|
}
|
|
6172
7103
|
|
|
6173
7104
|
/**
|
|
@@ -6182,10 +7113,10 @@ function isPathInside(childPath, parentPath) {
|
|
|
6182
7113
|
*/
|
|
6183
7114
|
function isPathInsideOrEqual(childPath, parentPath) {
|
|
6184
7115
|
if (!childPath || !parentPath) return false;
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
7116
|
+
return isResolvedPathInsideOrEqual(
|
|
7117
|
+
_pathForComparison(childPath),
|
|
7118
|
+
_pathForComparison(parentPath),
|
|
7119
|
+
);
|
|
6189
7120
|
}
|
|
6190
7121
|
|
|
6191
7122
|
function pathsOverlap(firstPath, secondPath) {
|
|
@@ -6366,7 +7297,7 @@ const READ_ONLY_ROOT_TASK_TYPES = new Set(['meeting', 'ask', 'explore', 'plan-to
|
|
|
6366
7297
|
* NOTE (W-mp8ho6w500034a58): Pipeline branches no longer override this.
|
|
6367
7298
|
* Read-only pipeline stages don't commit, so a `pipeline/...` branch is a
|
|
6368
7299
|
* meaningless label for them — the dispatcher short-circuits read-only WIs
|
|
6369
|
-
* regardless of branch name, and `engine/pipeline.js` now omits the branch
|
|
7300
|
+
* regardless of branch name, and `engine/orchestration/pipeline.js` now omits the branch
|
|
6370
7301
|
* field for read-only stages. Only code-mutating pipeline stages need a
|
|
6371
7302
|
* worktree, and they take the normal code-mutating path below.
|
|
6372
7303
|
*
|
|
@@ -6402,8 +7333,9 @@ const READ_ONLY_ROOT_TASK_TYPES = new Set(['meeting', 'ask', 'explore', 'plan-to
|
|
|
6402
7333
|
* @param {{ localPath?: string|null, checkoutMode?: string|null, worktreeMode?: string|null }|null|undefined} project
|
|
6403
7334
|
* @param {string} type — work type (e.g. 'fix', 'explore', 'meeting')
|
|
6404
7335
|
* @param {string} minionsDir — MINIONS_DIR fallback anchor (ignored in live mode)
|
|
6405
|
-
* @param {{ workdir?: string|null }} [options] — optional per-WI overrides
|
|
6406
|
-
* @returns {{ cwd: string|null, worktreeRootDir: string|null, liveMode?: boolean, readOnlyWorktree?: boolean, workdir?: string|null }}
|
|
7336
|
+
* @param {{ workdir?: string|null, executionSurface?: string|null }} [options] — optional per-WI overrides
|
|
7337
|
+
* @returns {{ cwd: string|null, worktreeRootDir: string|null, liveMode?: boolean, readOnlyWorktree?: boolean, controlPlane?: boolean, workdir?: string|null }}
|
|
7338
|
+
* - For control-plane surface (any project): { cwd: <MINIONS_DIR[/workdir]>, worktreeRootDir: null, controlPlane: true }
|
|
6407
7339
|
* - For live mode (any type): { cwd: <abs localPath[/workdir]>, worktreeRootDir: null, liveMode: true }
|
|
6408
7340
|
* - For project-bound isolated read-only types:
|
|
6409
7341
|
* { cwd: null, worktreeRootDir: <project root>, readOnlyWorktree: true, workdir }
|
|
@@ -6424,6 +7356,63 @@ function resolveSpawnPaths(project, type, minionsDir, options) {
|
|
|
6424
7356
|
? options.workdir
|
|
6425
7357
|
: null;
|
|
6426
7358
|
|
|
7359
|
+
// ── Cloud short-circuit (Agency / Copilot cloud dispatch) ─────────────
|
|
7360
|
+
// A cloud dispatch allocates NO local checkout at all: the remote agent
|
|
7361
|
+
// owns its own workspace, its own branch, and authors its own PR. The
|
|
7362
|
+
// engine still needs a local cwd for the provider call itself (`az`, `gh`,
|
|
7363
|
+
// or an HTTPS request), so it runs from MINIONS_DIR — the same anchor
|
|
7364
|
+
// control-plane uses.
|
|
7365
|
+
//
|
|
7366
|
+
// `workdir` is deliberately echoed back UNAPPLIED. For a repo surface it is
|
|
7367
|
+
// a subpath of the checkout; here there is no checkout, so resolving it
|
|
7368
|
+
// against MINIONS_DIR would be meaningless (and would fail containment for
|
|
7369
|
+
// no reason). The caller forwards it to the provider as a repo-relative
|
|
7370
|
+
// scope hint for the remote agent.
|
|
7371
|
+
//
|
|
7372
|
+
// Ordered ahead of live-checkout and the read-only / mutating split for the
|
|
7373
|
+
// same reason control-plane is: "does this need a checkout?" must be one
|
|
7374
|
+
// decision in one place. As with control-plane, the caller is responsible
|
|
7375
|
+
// for having validated the surface fail-closed via `validateExecutionSurface`.
|
|
7376
|
+
if (options && typeof options === 'object'
|
|
7377
|
+
&& options.executionSurface === EXECUTION_SURFACE.CLOUD) {
|
|
7378
|
+
if (!minionsDir) {
|
|
7379
|
+
const err = new Error('Cannot resolve cwd for cloud spawn: no MINIONS_DIR provided.');
|
|
7380
|
+
err.code = 'WORKTREE_ROOTDIR_MISSING_BASE';
|
|
7381
|
+
throw err;
|
|
7382
|
+
}
|
|
7383
|
+
return {
|
|
7384
|
+
cwd: path.resolve(String(minionsDir)),
|
|
7385
|
+
worktreeRootDir: null,
|
|
7386
|
+
cloud: true,
|
|
7387
|
+
workdir,
|
|
7388
|
+
};
|
|
7389
|
+
}
|
|
7390
|
+
|
|
7391
|
+
// ── Control-plane short-circuit (W-ms5tb6ha016fda10) ───────────────────
|
|
7392
|
+
// Runs BEFORE live-checkout and before the read-only / code-mutating split
|
|
7393
|
+
// so "this task needs no repository checkout" is a single, explicit
|
|
7394
|
+
// decision made in exactly one place. Deliberately ordered ahead of live
|
|
7395
|
+
// mode too: a control-plane job must not enter the operator's checkout, and
|
|
7396
|
+
// must not consume the one-mutating-dispatch-per-project live slot.
|
|
7397
|
+
//
|
|
7398
|
+
// The caller is responsible for having validated the surface (fail-closed)
|
|
7399
|
+
// via `validateExecutionSurface`; this resolver only honors the already-
|
|
7400
|
+
// validated boolean so the eligibility rule cannot fork.
|
|
7401
|
+
if (options && typeof options === 'object'
|
|
7402
|
+
&& options.executionSurface === EXECUTION_SURFACE.CONTROL_PLANE) {
|
|
7403
|
+
if (!minionsDir) {
|
|
7404
|
+
const err = new Error('Cannot resolve cwd for control-plane spawn: no MINIONS_DIR provided.');
|
|
7405
|
+
err.code = 'WORKTREE_ROOTDIR_MISSING_BASE';
|
|
7406
|
+
throw err;
|
|
7407
|
+
}
|
|
7408
|
+
const cpBase = path.resolve(String(minionsDir));
|
|
7409
|
+
return {
|
|
7410
|
+
cwd: _applyWorkdirOrThrow(cpBase, workdir),
|
|
7411
|
+
worktreeRootDir: null,
|
|
7412
|
+
controlPlane: true,
|
|
7413
|
+
};
|
|
7414
|
+
}
|
|
7415
|
+
|
|
6427
7416
|
// ── Live-checkout mode short-circuit (P-a3f9b202) ──────────────────────
|
|
6428
7417
|
// Runs BEFORE the read-only / code-mutating split so live mode is the
|
|
6429
7418
|
// single decision point regardless of task type — read-only tasks in
|
|
@@ -6784,7 +7773,7 @@ function syncBundledPersonalSkills(skillsDir, opts = {}) {
|
|
|
6784
7773
|
if (!skillsDir || !fs.existsSync(skillsDir)) return result;
|
|
6785
7774
|
|
|
6786
7775
|
const homeDir = opts.homeDir || os.homedir();
|
|
6787
|
-
const { listRuntimes, resolveRuntime } = require('
|
|
7776
|
+
const { listRuntimes, resolveRuntime } = require('../runtimes');
|
|
6788
7777
|
const personalRoots = new Set();
|
|
6789
7778
|
for (const runtimeName of listRuntimes()) {
|
|
6790
7779
|
const runtime = resolveRuntime(runtimeName);
|
|
@@ -7589,10 +8578,10 @@ function findPrRecord(prs, prRef, project = null) {
|
|
|
7589
8578
|
// - the link-time "...(polling...)" placeholder
|
|
7590
8579
|
// - serialized-JSON / agent-output leakage (contains {}"[] chars)
|
|
7591
8580
|
// - bare hex/uuid-ish ids (>= 8 hex/dash chars)
|
|
7592
|
-
// - the fallback WRITER shapes "PR created by <agent>" (engine/lifecycle.js)
|
|
7593
|
-
// and bare "PR #<n>" (engine/ado.js central poller)
|
|
7594
|
-
// The WRITER (engine/lifecycle.js) and the DETECTORS (engine/github.js,
|
|
7595
|
-
// engine/ado.js) MUST agree — route every site through this helper so they
|
|
8581
|
+
// - the fallback WRITER shapes "PR created by <agent>" (engine/orchestration/lifecycle.js)
|
|
8582
|
+
// and bare "PR #<n>" (engine/ado/index.js central poller)
|
|
8583
|
+
// The WRITER (engine/orchestration/lifecycle.js) and the DETECTORS (engine/providers/github.js,
|
|
8584
|
+
// engine/ado/index.js) MUST agree — route every site through this helper so they
|
|
7596
8585
|
// cannot drift apart again (the github poller previously never backfilled a
|
|
7597
8586
|
// frozen "PR created by Ripley" title because its detector didn't know the shape).
|
|
7598
8587
|
function isPlaceholderPrTitle(title) {
|
|
@@ -7613,6 +8602,113 @@ function snapshotPrRecord(pr) {
|
|
|
7613
8602
|
return JSON.parse(JSON.stringify(pr));
|
|
7614
8603
|
}
|
|
7615
8604
|
|
|
8605
|
+
// ── PR author identity (separate from `pr.agent`) ────────────────────────────
|
|
8606
|
+
//
|
|
8607
|
+
// `pr.agent` = the Minions agent that created/owns the automated work. Used for
|
|
8608
|
+
// routing, self-review prevention, fix ownership, and metrics. The
|
|
8609
|
+
// sentinel `PR_AGENT_HUMAN` ('human') means "no Minions author-agent"
|
|
8610
|
+
// (a human/externally created PR). `agent` is NEVER a human login.
|
|
8611
|
+
// `pr.author` = the real individual on whose behalf the PR exists, linked to an
|
|
8612
|
+
// authoritative provider identity. Structured shape:
|
|
8613
|
+
// { provider, id, nodeId?, descriptor?, login?, displayName?, url? }
|
|
8614
|
+
// Unknown/legacy records leave `author` unset and render "Unknown".
|
|
8615
|
+
//
|
|
8616
|
+
// See docs/pr-author-identity.md.
|
|
8617
|
+
const PR_AGENT_HUMAN = 'human';
|
|
8618
|
+
|
|
8619
|
+
const _PR_AUTHOR_FIELDS = ['provider', 'id', 'nodeId', 'descriptor', 'login', 'displayName', 'url'];
|
|
8620
|
+
|
|
8621
|
+
function _authorField(value, max) {
|
|
8622
|
+
if (value == null) return '';
|
|
8623
|
+
const s = String(value).trim();
|
|
8624
|
+
return s ? s.slice(0, max) : '';
|
|
8625
|
+
}
|
|
8626
|
+
|
|
8627
|
+
// Accept only real http(s) profile links; anything else is dropped so the
|
|
8628
|
+
// dashboard never renders an unvalidated href.
|
|
8629
|
+
function _authorProfileUrl(value) {
|
|
8630
|
+
const s = _authorField(value, 400);
|
|
8631
|
+
if (!s) return '';
|
|
8632
|
+
return /^https?:\/\/[^\s]+$/i.test(s) ? s : '';
|
|
8633
|
+
}
|
|
8634
|
+
|
|
8635
|
+
// Build a structured author identity from a GitHub PR `user` object.
|
|
8636
|
+
function buildGithubAuthorIdentity(user) {
|
|
8637
|
+
if (!user || typeof user !== 'object') return null;
|
|
8638
|
+
const out = { provider: 'github' };
|
|
8639
|
+
const id = _authorField(user.id, 64);
|
|
8640
|
+
const nodeId = _authorField(user.node_id, 128);
|
|
8641
|
+
const login = _authorField(user.login, 120);
|
|
8642
|
+
const displayName = _authorField(user.name, 200);
|
|
8643
|
+
const url = _authorProfileUrl(user.html_url);
|
|
8644
|
+
if (id) out.id = id;
|
|
8645
|
+
if (nodeId) out.nodeId = nodeId;
|
|
8646
|
+
if (login) out.login = login;
|
|
8647
|
+
if (displayName) out.displayName = displayName;
|
|
8648
|
+
if (url) out.url = url;
|
|
8649
|
+
return (out.id || out.nodeId || out.login || out.displayName) ? out : null;
|
|
8650
|
+
}
|
|
8651
|
+
|
|
8652
|
+
// Build a structured author identity from an ADO PR `createdBy` object. ADO's
|
|
8653
|
+
// createdBy.url is an API endpoint (not a browsable profile page), so no url is
|
|
8654
|
+
// surfaced unless a real http(s) profile link is present in _links.
|
|
8655
|
+
function buildAdoAuthorIdentity(createdBy) {
|
|
8656
|
+
if (!createdBy || typeof createdBy !== 'object') return null;
|
|
8657
|
+
const out = { provider: 'ado' };
|
|
8658
|
+
const id = _authorField(createdBy.id, 64);
|
|
8659
|
+
const descriptor = _authorField(createdBy.descriptor, 256);
|
|
8660
|
+
const login = _authorField(createdBy.uniqueName, 200);
|
|
8661
|
+
const displayName = _authorField(createdBy.displayName, 200);
|
|
8662
|
+
const url = _authorProfileUrl(createdBy._links && createdBy._links.web && createdBy._links.web.href);
|
|
8663
|
+
if (id) out.id = id;
|
|
8664
|
+
if (descriptor) out.descriptor = descriptor;
|
|
8665
|
+
if (login) out.login = login;
|
|
8666
|
+
if (displayName) out.displayName = displayName;
|
|
8667
|
+
if (url) out.url = url;
|
|
8668
|
+
return (out.id || out.descriptor || out.login || out.displayName) ? out : null;
|
|
8669
|
+
}
|
|
8670
|
+
|
|
8671
|
+
// Normalize an arbitrary author value (structured object or bare string) into
|
|
8672
|
+
// the canonical structured shape, or null when nothing usable is present.
|
|
8673
|
+
function normalizePrAuthorIdentity(raw) {
|
|
8674
|
+
if (!raw) return null;
|
|
8675
|
+
if (typeof raw === 'string') {
|
|
8676
|
+
const login = raw.trim().slice(0, 200);
|
|
8677
|
+
return login ? { login } : null;
|
|
8678
|
+
}
|
|
8679
|
+
if (typeof raw !== 'object') return null;
|
|
8680
|
+
const out = {};
|
|
8681
|
+
for (const key of _PR_AUTHOR_FIELDS) {
|
|
8682
|
+
const value = key === 'url' ? _authorProfileUrl(raw[key]) : _authorField(raw[key], 256);
|
|
8683
|
+
if (value) out[key] = value;
|
|
8684
|
+
}
|
|
8685
|
+
return Object.keys(out).length ? out : null;
|
|
8686
|
+
}
|
|
8687
|
+
|
|
8688
|
+
// Centralized merge so a partial provider refresh cannot erase a richer author
|
|
8689
|
+
// identity already stored. Field-by-field: a non-empty incoming value wins,
|
|
8690
|
+
// otherwise the existing value is preserved. A provider change trusts the newer
|
|
8691
|
+
// identity wholesale (the person the platform now reports authored the PR).
|
|
8692
|
+
function mergePrAuthorIdentity(existing, incoming) {
|
|
8693
|
+
const cur = normalizePrAuthorIdentity(existing);
|
|
8694
|
+
const inc = normalizePrAuthorIdentity(incoming);
|
|
8695
|
+
if (!inc) return cur;
|
|
8696
|
+
if (!cur) return inc;
|
|
8697
|
+
if (cur.provider && inc.provider && cur.provider !== inc.provider) return inc;
|
|
8698
|
+
const out = { ...cur };
|
|
8699
|
+
for (const key of _PR_AUTHOR_FIELDS) {
|
|
8700
|
+
if (inc[key]) out[key] = inc[key];
|
|
8701
|
+
}
|
|
8702
|
+
return out;
|
|
8703
|
+
}
|
|
8704
|
+
|
|
8705
|
+
// Human-readable label for a PR author (typed SQL column + filter/search text).
|
|
8706
|
+
function prAuthorLabel(author) {
|
|
8707
|
+
const a = normalizePrAuthorIdentity(author);
|
|
8708
|
+
if (!a) return '';
|
|
8709
|
+
return a.displayName || a.login || a.id || '';
|
|
8710
|
+
}
|
|
8711
|
+
|
|
7616
8712
|
function _jsonEqual(a, b) {
|
|
7617
8713
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
7618
8714
|
}
|
|
@@ -7684,17 +8780,28 @@ function normalizePrLinkItems(value) {
|
|
|
7684
8780
|
return [...new Set(items.filter(item => typeof item === 'string' && item))];
|
|
7685
8781
|
}
|
|
7686
8782
|
|
|
7687
|
-
// Canonical
|
|
7688
|
-
// All records are expected to carry `contextOnly` after the 2026-06-24
|
|
7689
|
-
// on-disk sweep confirmed no live `_contextOnly`/`_autoObserve`/`_manual`
|
|
7690
|
-
// keys remain (and the boot migration has been removed).
|
|
8783
|
+
// Canonical context-only flag retained for API/backward compatibility.
|
|
7691
8784
|
function isContextOnlyPrRecord(pr) {
|
|
7692
8785
|
if (!pr || typeof pr !== 'object') return false;
|
|
7693
8786
|
return pr.contextOnly === true;
|
|
7694
8787
|
}
|
|
7695
8788
|
|
|
8789
|
+
function isPrAutoFixEnabled(pr) {
|
|
8790
|
+
if (!pr || typeof pr !== 'object') return false;
|
|
8791
|
+
return typeof pr.autoFixEnabled === 'boolean'
|
|
8792
|
+
? pr.autoFixEnabled
|
|
8793
|
+
: !isContextOnlyPrRecord(pr);
|
|
8794
|
+
}
|
|
8795
|
+
|
|
8796
|
+
function isPrAutoReviewEnabled(pr) {
|
|
8797
|
+
if (!pr || typeof pr !== 'object') return false;
|
|
8798
|
+
return typeof pr.autoReviewEnabled === 'boolean'
|
|
8799
|
+
? pr.autoReviewEnabled
|
|
8800
|
+
: !isContextOnlyPrRecord(pr);
|
|
8801
|
+
}
|
|
8802
|
+
|
|
7696
8803
|
function isAutoManagedPrRecord(pr) {
|
|
7697
|
-
return
|
|
8804
|
+
return isPrAutoFixEnabled(pr) || isPrAutoReviewEnabled(pr);
|
|
7698
8805
|
}
|
|
7699
8806
|
|
|
7700
8807
|
/**
|
|
@@ -7717,7 +8824,7 @@ const EXPLICIT_WORK_ITEM_ORIGIN_ROOTS = new Set([
|
|
|
7717
8824
|
'dashboard', // dashboard UI + POST /api/work-items, incl. CC/agent API callers
|
|
7718
8825
|
// (dashboard.js hard-codes createdBy:'dashboard'; also
|
|
7719
8826
|
// dashboard:execute, dashboard:revision, dashboard:promotion:…)
|
|
7720
|
-
'human', // engine/meeting.js
|
|
8827
|
+
'human', // engine/orchestration/meeting.js
|
|
7721
8828
|
'operator',
|
|
7722
8829
|
'watch', // operator-authored watch rule (watch:<watchId>)
|
|
7723
8830
|
'scheduler', // operator-authored schedule (scheduler, scheduler:harness)
|
|
@@ -7730,7 +8837,7 @@ const EXPLICIT_WORK_ITEM_ORIGIN_ROOTS = new Set([
|
|
|
7730
8837
|
* Normalize a work item's `createdBy` to its namespace root so the allowlist
|
|
7731
8838
|
* above stays short. Creators namespace themselves as `<root>:<detail>`
|
|
7732
8839
|
* (`dashboard:execute`, `pipeline:<id>`, `watch:<id>`, `scheduler:harness`);
|
|
7733
|
-
* `qa-session-<phase>` (engine/qa
|
|
8840
|
+
* `qa-session-<phase>` (engine/qa/sessions.js) uses a hyphen instead.
|
|
7734
8841
|
*/
|
|
7735
8842
|
function workItemOriginRoot(createdBy) {
|
|
7736
8843
|
const raw = typeof createdBy === 'string' ? createdBy.trim().toLowerCase() : '';
|
|
@@ -7798,7 +8905,7 @@ function getPrLinks() {
|
|
|
7798
8905
|
// normalization.
|
|
7799
8906
|
const projectsByName = new Map(getProjects().map(project => [project.name || path.basename(project.localPath || ''), project]));
|
|
7800
8907
|
try {
|
|
7801
|
-
const store = require('
|
|
8908
|
+
const store = require('../persistence/pull-requests-store');
|
|
7802
8909
|
const allPrs = store.readAllPullRequests() || [];
|
|
7803
8910
|
for (const pr of allPrs) {
|
|
7804
8911
|
if (!pr?.id) continue;
|
|
@@ -7813,7 +8920,7 @@ function getPrLinks() {
|
|
|
7813
8920
|
// Static PR links are stored in SQLite.
|
|
7814
8921
|
let static_ = null;
|
|
7815
8922
|
try {
|
|
7816
|
-
const store = require('
|
|
8923
|
+
const store = require('../persistence/small-state-store');
|
|
7817
8924
|
static_ = store.readPrLinks();
|
|
7818
8925
|
} catch { static_ = null; }
|
|
7819
8926
|
if (static_ && typeof static_ === 'object' && !Array.isArray(static_)) {
|
|
@@ -7867,9 +8974,9 @@ function addPrLink(prId, itemId, { project = null, url = '', prNumber = null } =
|
|
|
7867
8974
|
links[effectivePrId] = [...mergedCurrent];
|
|
7868
8975
|
return links;
|
|
7869
8976
|
};
|
|
7870
|
-
const { wrote } = require('
|
|
8977
|
+
const { wrote } = require('../persistence/small-state-store').applyPrLinksMutation(mutator);
|
|
7871
8978
|
if (wrote) {
|
|
7872
|
-
try { require('
|
|
8979
|
+
try { require('../persistence/db-events').emitStateEvent('pr_links'); } catch { /* optional */ }
|
|
7873
8980
|
}
|
|
7874
8981
|
|
|
7875
8982
|
if (!project) return;
|
|
@@ -7895,7 +9002,8 @@ function addPrLink(prId, itemId, { project = null, url = '', prNumber = null } =
|
|
|
7895
9002
|
const _DEDUP_MERGE_SCALAR_KEYS = [
|
|
7896
9003
|
'url', 'title', 'agent', 'branch', 'description', 'sourcePlan', 'itemType',
|
|
7897
9004
|
'mergedAt', 'closedAt', 'created', 'reviewStatus',
|
|
7898
|
-
'minionsReview', '
|
|
9005
|
+
'minionsReview', 'contextOnly', 'autoFixEnabled', 'autoReviewEnabled',
|
|
9006
|
+
'_automationFixCauses',
|
|
7899
9007
|
];
|
|
7900
9008
|
|
|
7901
9009
|
// Pick the "most complete" record from a list of duplicates.
|
|
@@ -7921,6 +9029,10 @@ function _mergeDuplicatePrInto(winner, loser) {
|
|
|
7921
9029
|
winner[key] = loser[key];
|
|
7922
9030
|
}
|
|
7923
9031
|
}
|
|
9032
|
+
// Author is a structured identity — merge field-by-field so the winner keeps
|
|
9033
|
+
// the richest identity across duplicates rather than an arbitrary one.
|
|
9034
|
+
const mergedAuthor = mergePrAuthorIdentity(winner.author, loser.author);
|
|
9035
|
+
if (mergedAuthor) winner.author = mergedAuthor;
|
|
7924
9036
|
winner.prdItems = normalizePrLinkItems([...(winner.prdItems || []), ...(loser.prdItems || [])]);
|
|
7925
9037
|
if (winner.userDeleted === true || loser.userDeleted === true) {
|
|
7926
9038
|
winner.userDeleted = true;
|
|
@@ -7985,7 +9097,7 @@ function _clearPrTombstonesForIdentity(identity) {
|
|
|
7985
9097
|
if (!identity) return 0;
|
|
7986
9098
|
const projectsByName = new Map(getProjects().map(project => [project.name, project]));
|
|
7987
9099
|
const scopes = new Map();
|
|
7988
|
-
const store = require('
|
|
9100
|
+
const store = require('../persistence/pull-requests-store');
|
|
7989
9101
|
for (const pr of store.readAllPullRequests() || []) {
|
|
7990
9102
|
if (pr?.userDeleted !== true) continue;
|
|
7991
9103
|
const project = pr._scope && pr._scope !== 'central'
|
|
@@ -8076,6 +9188,11 @@ function upsertPullRequestRecord(source, entry, { project = null, itemId = null,
|
|
|
8076
9188
|
if (normalizedEntry.contextOnly != null) {
|
|
8077
9189
|
normalizedEntry.contextOnly = normalizedEntry.contextOnly === true;
|
|
8078
9190
|
}
|
|
9191
|
+
// Canonicalize the author identity on the create path (the update path merges
|
|
9192
|
+
// via mergePrAuthorIdentity). A null result is stored as-is → renders Unknown.
|
|
9193
|
+
if (Object.prototype.hasOwnProperty.call(normalizedEntry, 'author')) {
|
|
9194
|
+
normalizedEntry.author = normalizePrAuthorIdentity(normalizedEntry.author);
|
|
9195
|
+
}
|
|
8079
9196
|
|
|
8080
9197
|
let created = false;
|
|
8081
9198
|
let linked = false;
|
|
@@ -8121,7 +9238,7 @@ function upsertPullRequestRecord(source, entry, { project = null, itemId = null,
|
|
|
8121
9238
|
// silently clearing the tombstone. Check globally before inserting.
|
|
8122
9239
|
if (!target && !allowResurrect) {
|
|
8123
9240
|
try {
|
|
8124
|
-
const store = require('
|
|
9241
|
+
const store = require('../persistence/pull-requests-store');
|
|
8125
9242
|
const projectsByName = new Map(getProjects().map(candidate => [candidate.name, candidate]));
|
|
8126
9243
|
const globalMatch = (store.readAllPullRequests() || []).find(pr => {
|
|
8127
9244
|
if (!pr || pr.userDeleted !== true) return false;
|
|
@@ -8148,6 +9265,14 @@ function upsertPullRequestRecord(source, entry, { project = null, itemId = null,
|
|
|
8148
9265
|
target.id = canonicalId;
|
|
8149
9266
|
if (prNumber != null) target.prNumber = prNumber;
|
|
8150
9267
|
const targetWasAutoManaged = isAutoManagedPrRecord(target);
|
|
9268
|
+
// Author identity merges through mergePrAuthorIdentity so a partial
|
|
9269
|
+
// provider refresh (which may omit author entirely) never erases a richer
|
|
9270
|
+
// author already stored. This is intentionally separate from the
|
|
9271
|
+
// fill-if-empty scalar loop below because author is a structured object.
|
|
9272
|
+
if (normalizedEntry.author != null) {
|
|
9273
|
+
const mergedAuthor = mergePrAuthorIdentity(target.author, normalizedEntry.author);
|
|
9274
|
+
if (mergedAuthor) target.author = mergedAuthor;
|
|
9275
|
+
}
|
|
8151
9276
|
for (const key of ['url', 'title', 'description', 'agent', 'branch', 'reviewStatus', 'status', 'created', 'sourcePlan', 'itemType', 'prdItemId']) {
|
|
8152
9277
|
if (normalizedEntry[key] != null && normalizedEntry[key] !== '' && (target[key] == null || target[key] === '')) {
|
|
8153
9278
|
target[key] = normalizedEntry[key];
|
|
@@ -8159,6 +9284,14 @@ function upsertPullRequestRecord(source, entry, { project = null, itemId = null,
|
|
|
8159
9284
|
if (normalizedEntry.contextOnly != null) {
|
|
8160
9285
|
const wouldDemoteManagedPr = normalizedEntry.contextOnly === true && targetWasAutoManaged;
|
|
8161
9286
|
target.contextOnly = wouldDemoteManagedPr ? false : normalizedEntry.contextOnly === true;
|
|
9287
|
+
if (!wouldDemoteManagedPr) {
|
|
9288
|
+
if (typeof normalizedEntry.autoFixEnabled === 'boolean') {
|
|
9289
|
+
target.autoFixEnabled = normalizedEntry.autoFixEnabled;
|
|
9290
|
+
}
|
|
9291
|
+
if (typeof normalizedEntry.autoReviewEnabled === 'boolean') {
|
|
9292
|
+
target.autoReviewEnabled = normalizedEntry.autoReviewEnabled;
|
|
9293
|
+
}
|
|
9294
|
+
}
|
|
8162
9295
|
}
|
|
8163
9296
|
}
|
|
8164
9297
|
target.prdItems = normalizePrLinkItems(target.prdItems || []);
|
|
@@ -8293,7 +9426,7 @@ function autoEnrollPrFromWorkItem(item, project, minionsDir) {
|
|
|
8293
9426
|
prNumber: prNum,
|
|
8294
9427
|
title: `PR #${prNum != null ? prNum : '?'} (polling...)`,
|
|
8295
9428
|
description: '',
|
|
8296
|
-
agent:
|
|
9429
|
+
agent: PR_AGENT_HUMAN,
|
|
8297
9430
|
branch: '',
|
|
8298
9431
|
reviewStatus: 'pending',
|
|
8299
9432
|
status: 'active',
|
|
@@ -8314,7 +9447,7 @@ const autoEnrollPrFromFixWorkItem = autoEnrollPrFromWorkItem;
|
|
|
8314
9447
|
|
|
8315
9448
|
// Cross-platform process-management helpers (kill trees, PID liveness,
|
|
8316
9449
|
// process-table enumeration, cwd-holder detection, descendant/reachability
|
|
8317
|
-
// BFS) were extracted to engine/process-utils.js (P-mrb8yg9x001k1401-b) and
|
|
9450
|
+
// BFS) were extracted to engine/processes/process-utils.js (P-mrb8yg9x001k1401-b) and
|
|
8318
9451
|
// are re-exported below via `...processUtils`.
|
|
8319
9452
|
|
|
8320
9453
|
// W-mq6f2fe0000557fa — clear the per-path failure cooldown entry so the
|
|
@@ -8331,7 +9464,7 @@ function clearWorktreeFailureCache(wtPath) {
|
|
|
8331
9464
|
|
|
8332
9465
|
// Cross-platform process-management helpers (kill trees, PID liveness,
|
|
8333
9466
|
// process-table enumeration, cwd-holder detection, descendant/reachability
|
|
8334
|
-
// BFS) were extracted to engine/process-utils.js (P-mrb8yg9x001k1401-b) and
|
|
9467
|
+
// BFS) were extracted to engine/processes/process-utils.js (P-mrb8yg9x001k1401-b) and
|
|
8335
9468
|
// are re-exported below via `...processUtils`.
|
|
8336
9469
|
|
|
8337
9470
|
// ─── Work Items & Pull Requests Mutation Helpers ────────────────────────────
|
|
@@ -8352,11 +9485,11 @@ function stateScope(source) {
|
|
|
8352
9485
|
}
|
|
8353
9486
|
|
|
8354
9487
|
function readWorkItems(source = null) {
|
|
8355
|
-
return require('
|
|
9488
|
+
return require('../planning/work-items-store').readWorkItemsForScope(stateScope(source));
|
|
8356
9489
|
}
|
|
8357
9490
|
|
|
8358
9491
|
function mutateWorkItems(source, mutator, opts = {}) {
|
|
8359
|
-
const store = require('
|
|
9492
|
+
const store = require('../planning/work-items-store');
|
|
8360
9493
|
const scope = stateScope(source);
|
|
8361
9494
|
const normalizedOpts = { ...opts };
|
|
8362
9495
|
if (Array.isArray(opts.readScopes)) {
|
|
@@ -8367,7 +9500,7 @@ function mutateWorkItems(source, mutator, opts = {}) {
|
|
|
8367
9500
|
return mutator(items, comparisonItems) || items;
|
|
8368
9501
|
}, normalizedOpts);
|
|
8369
9502
|
if (wrote) {
|
|
8370
|
-
try { require('
|
|
9503
|
+
try { require('../persistence/db-events').emitStateEvent('work_items'); } catch { /* optional */ }
|
|
8371
9504
|
try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
|
|
8372
9505
|
}
|
|
8373
9506
|
return result;
|
|
@@ -8387,17 +9520,17 @@ function reopenWorkItem(wi) {
|
|
|
8387
9520
|
}
|
|
8388
9521
|
|
|
8389
9522
|
function readPullRequests(source = null) {
|
|
8390
|
-
return require('
|
|
9523
|
+
return require('../persistence/pull-requests-store').readPullRequestsForScope(stateScope(source));
|
|
8391
9524
|
}
|
|
8392
9525
|
|
|
8393
9526
|
function mutatePullRequests(source, mutator) {
|
|
8394
|
-
const store = require('
|
|
9527
|
+
const store = require('../persistence/pull-requests-store');
|
|
8395
9528
|
const { wrote, result } = store.applyPullRequestsMutation(stateScope(source), (prs) => {
|
|
8396
9529
|
if (!Array.isArray(prs)) prs = [];
|
|
8397
9530
|
return mutator(prs) || prs;
|
|
8398
9531
|
});
|
|
8399
9532
|
if (wrote) {
|
|
8400
|
-
try { require('
|
|
9533
|
+
try { require('../persistence/db-events').emitStateEvent('pull_requests'); } catch { /* optional */ }
|
|
8401
9534
|
}
|
|
8402
9535
|
return result;
|
|
8403
9536
|
}
|
|
@@ -8525,16 +9658,16 @@ function getPrFixAutomationCause({ dispatchKey = '', source = '', task = '' } =
|
|
|
8525
9658
|
}
|
|
8526
9659
|
|
|
8527
9660
|
// Source-branch head SHA, normalized across hosts. GitHub PRs carry
|
|
8528
|
-
// `headSha`/`headRefOid` (engine/github.js:718-742 keeps both in sync); ADO PRs
|
|
8529
|
-
// carry `_adoSourceCommit`/`headRefOid` (engine/ado.js:1083-1129) and a legacy
|
|
8530
|
-
// `_adoHeadCommit`. Mirrors engine/lifecycle.js:1849 getPrFixBaselineHead.
|
|
9661
|
+
// `headSha`/`headRefOid` (engine/providers/github.js:718-742 keeps both in sync); ADO PRs
|
|
9662
|
+
// carry `_adoSourceCommit`/`headRefOid` (engine/ado/index.js:1083-1129) and a legacy
|
|
9663
|
+
// `_adoHeadCommit`. Mirrors engine/orchestration/lifecycle.js:1849 getPrFixBaselineHead.
|
|
8531
9664
|
function _prHeadSha(pr) {
|
|
8532
9665
|
return String(pr?.headRefOid || pr?.headSha || pr?._adoSourceCommit || pr?._adoHeadCommit || '').trim();
|
|
8533
9666
|
}
|
|
8534
9667
|
|
|
8535
9668
|
// Target-branch base SHA, normalized across hosts. GitHub PRs carry `baseSha`
|
|
8536
|
-
// (engine/github.js:746-749); ADO PRs carry `_adoTargetCommit`
|
|
8537
|
-
// (engine/ado.js:1168-1172, inline in pollPrStatus).
|
|
9669
|
+
// (engine/providers/github.js:746-749); ADO PRs carry `_adoTargetCommit`
|
|
9670
|
+
// (engine/ado/index.js:1168-1172, inline in pollPrStatus).
|
|
8538
9671
|
function _prBaseSha(pr) {
|
|
8539
9672
|
return String(pr?.baseSha || pr?._adoTargetCommit || '').trim();
|
|
8540
9673
|
}
|
|
@@ -8769,7 +9902,7 @@ function isWorktreePathLive(worktreePath, opts = {}) {
|
|
|
8769
9902
|
const excludeSpawnLeaseId = opts.excludeSpawnLeaseId ? String(opts.excludeSpawnLeaseId) : null;
|
|
8770
9903
|
let db = opts.db || null;
|
|
8771
9904
|
if (!db) {
|
|
8772
|
-
try { db = require('
|
|
9905
|
+
try { db = require('../db').getDb(); }
|
|
8773
9906
|
catch (e) {
|
|
8774
9907
|
log('warn', `isWorktreePathLive: SQL unavailable for ${worktreePath} (${e.message}) — fail-open (assume live)`);
|
|
8775
9908
|
return true;
|
|
@@ -8803,6 +9936,52 @@ function isWorktreePathLive(worktreePath, opts = {}) {
|
|
|
8803
9936
|
return false;
|
|
8804
9937
|
}
|
|
8805
9938
|
|
|
9939
|
+
// W-ms9q3dql00vxd4a8 — batched, READ-ONLY sibling of `isWorktreePathLive` for
|
|
9940
|
+
// reporting paths that classify a whole worktree root at once (the worktree
|
|
9941
|
+
// inventory and the dashboard's lifecycle count). The per-path guard runs a
|
|
9942
|
+
// full pending+active scan for EVERY path, which is right for the handful of
|
|
9943
|
+
// destructive sites but is O(directories) queries for a 150-directory root.
|
|
9944
|
+
//
|
|
9945
|
+
// Deliberately NOT used by any wipe site: those keep the per-path guard and its
|
|
9946
|
+
// `excludeDispatchId` / `excludeSpawnLeaseId` semantics. The failure contract
|
|
9947
|
+
// mirrors the guard — `{ ok: false }` means "could not determine", and callers
|
|
9948
|
+
// MUST fail OPEN (treat every path as live) exactly as the guard does.
|
|
9949
|
+
function listLiveWorktreePaths(opts = {}) {
|
|
9950
|
+
let db = opts.db || null;
|
|
9951
|
+
if (!db) {
|
|
9952
|
+
try { db = require('../db').getDb(); }
|
|
9953
|
+
catch (e) {
|
|
9954
|
+
log('warn', `listLiveWorktreePaths: SQL unavailable (${e.message}) — fail-open (assume live)`);
|
|
9955
|
+
return { ok: false, paths: new Set() };
|
|
9956
|
+
}
|
|
9957
|
+
}
|
|
9958
|
+
if (!db) {
|
|
9959
|
+
log('warn', 'listLiveWorktreePaths: no db handle — fail-open (assume live)');
|
|
9960
|
+
return { ok: false, paths: new Set() };
|
|
9961
|
+
}
|
|
9962
|
+
let rows;
|
|
9963
|
+
try {
|
|
9964
|
+
rows = db.prepare(`
|
|
9965
|
+
SELECT json_extract(data, '$.worktreePath') AS top_wt,
|
|
9966
|
+
json_extract(data, '$.meta.worktreePath') AS meta_wt
|
|
9967
|
+
FROM dispatches
|
|
9968
|
+
WHERE status IN ('pending', 'active')
|
|
9969
|
+
`).all();
|
|
9970
|
+
} catch (e) {
|
|
9971
|
+
log('warn', `listLiveWorktreePaths: query threw (${e.message}) — fail-open (assume live)`);
|
|
9972
|
+
return { ok: false, paths: new Set() };
|
|
9973
|
+
}
|
|
9974
|
+
const paths = new Set();
|
|
9975
|
+
for (const row of rows || []) {
|
|
9976
|
+
for (const raw of [row?.top_wt, row?.meta_wt]) {
|
|
9977
|
+
if (!raw) continue;
|
|
9978
|
+
const normalized = _normalizeWorktreePath(raw);
|
|
9979
|
+
if (normalized) paths.add(normalized);
|
|
9980
|
+
}
|
|
9981
|
+
}
|
|
9982
|
+
return { ok: true, paths };
|
|
9983
|
+
}
|
|
9984
|
+
|
|
8806
9985
|
// P-1d6b1aa7 — separate, best-effort lookup of the specific dispatch row
|
|
8807
9986
|
// that is blocking a wipe, so callers can fold self-diagnosing detail (id,
|
|
8808
9987
|
// status, age-in-non-terminal-state) into the skip-live inbox note instead
|
|
@@ -8822,7 +10001,7 @@ function getWorktreeBlockingDispatchInfo(worktreePath, opts = {}) {
|
|
|
8822
10001
|
const excludeSpawnLeaseId = opts.excludeSpawnLeaseId ? String(opts.excludeSpawnLeaseId) : null;
|
|
8823
10002
|
let db = opts.db || null;
|
|
8824
10003
|
if (!db) {
|
|
8825
|
-
try { db = require('
|
|
10004
|
+
try { db = require('../db').getDb(); }
|
|
8826
10005
|
catch { return null; }
|
|
8827
10006
|
}
|
|
8828
10007
|
if (!db) return null;
|
|
@@ -8906,6 +10085,36 @@ function _writeWorktreeSkipLiveInboxNote(worktreePath, callerTag, blockingInfo)
|
|
|
8906
10085
|
} catch { /* best-effort — never throw from the skip-note writer */ }
|
|
8907
10086
|
}
|
|
8908
10087
|
|
|
10088
|
+
// W-ms9q3dql00vxd4a8 — a removal that REPORTS success but leaves the directory
|
|
10089
|
+
// behind is how an install accumulates unclassifiable residue. `git worktree
|
|
10090
|
+
// remove --force`, `fs.rmSync` and `rd /s /q` all clear the CONTENTS first —
|
|
10091
|
+
// including `.git` and the `.minions-worktree` ownership marker — and can then
|
|
10092
|
+
// still fail to unlink the top directory when something holds it open (a
|
|
10093
|
+
// process CWD, AV, Explorer). The husk left behind has no marker, no `gitdir:`
|
|
10094
|
+
// pointer and no registration, so every safe GC classifies it as FOREIGN and
|
|
10095
|
+
// keeps it forever: the observed 107 empty `W-*` directories on this install.
|
|
10096
|
+
//
|
|
10097
|
+
// So verify instead of assuming. When the path survives: finish the job if it
|
|
10098
|
+
// is now empty, otherwise re-stamp ownership so the residue stays reclaimable
|
|
10099
|
+
// by the ordinary orphan sweep, and report the truth to the caller.
|
|
10100
|
+
// Returns true only when the path is genuinely gone.
|
|
10101
|
+
function _verifyWorktreeRemoved(resolved, hadOwnerMarker) {
|
|
10102
|
+
let exists;
|
|
10103
|
+
try { exists = fs.existsSync(resolved); }
|
|
10104
|
+
catch { return true; } // unreadable — nothing further we can safely do
|
|
10105
|
+
if (!exists) return true;
|
|
10106
|
+
try {
|
|
10107
|
+
if (fs.readdirSync(resolved).length === 0) fs.rmdirSync(resolved);
|
|
10108
|
+
} catch { /* still occupied or unreadable — fall through to the re-stamp */ }
|
|
10109
|
+
let survived = true;
|
|
10110
|
+
try { survived = fs.existsSync(resolved); } catch { survived = true; }
|
|
10111
|
+
if (!survived) return true;
|
|
10112
|
+
_restoreWorktreeOwnerMarkerAfterFailedRemoval(resolved, hadOwnerMarker);
|
|
10113
|
+
log('warn', `removeWorktree: removal reported success but ${resolved} still exists — ownership re-stamped so the orphan sweep can reclaim it`);
|
|
10114
|
+
try { bumpWorktreeGcMetric('survivedRemoval'); } catch { /* metric optional */ }
|
|
10115
|
+
return false;
|
|
10116
|
+
}
|
|
10117
|
+
|
|
8909
10118
|
// P-c7e2b405 — INTENTIONAL divergence from worktree-gc.reapAndRemoveWorktree:
|
|
8910
10119
|
// this is the low-level `git worktree remove --force` → fs.rmSync → rd /s /q
|
|
8911
10120
|
// removal primitive WITH its own EBUSY/_retryFsOp backoff loop. The engine-side
|
|
@@ -8997,6 +10206,31 @@ function removeWorktree(wtPath, gitRoot, worktreeRoot, opts = {}) {
|
|
|
8997
10206
|
const prior = _removeWorktreeFailures.get(resolved);
|
|
8998
10207
|
if (prior && prior.count >= 3 && Date.now() - prior.lastAttempt < 3600000) return false;
|
|
8999
10208
|
|
|
10209
|
+
const _recordRemovalFailure = (message) => {
|
|
10210
|
+
const fail = _removeWorktreeFailures.get(resolved) || { count: 0, lastAttempt: 0 };
|
|
10211
|
+
fail.count++;
|
|
10212
|
+
fail.lastAttempt = Date.now();
|
|
10213
|
+
fail.lastError = message || 'unknown';
|
|
10214
|
+
_removeWorktreeFailures.set(resolved, fail);
|
|
10215
|
+
_pruneRemoveWorktreeFailures();
|
|
10216
|
+
bumpWorktreeGcMetric('totalFailure');
|
|
10217
|
+
if (fail.count <= 3) log('warn', `removeWorktree: failed for ${wtPath} (attempt ${fail.count}/3): ${fail.lastError}`);
|
|
10218
|
+
return false;
|
|
10219
|
+
};
|
|
10220
|
+
// W-ms9q3dql00vxd4a8 — every "success" exit funnels through here so a stage
|
|
10221
|
+
// that cleared the contents but could not unlink the directory is reported as
|
|
10222
|
+
// the failure it is, with ownership re-stamped, instead of leaking an
|
|
10223
|
+
// unclassifiable husk that no GC will ever be allowed to reclaim.
|
|
10224
|
+
const _settleRemoval = (stage, extraMetric) => {
|
|
10225
|
+
if (!_verifyWorktreeRemoved(resolved, hadOwnerMarker)) {
|
|
10226
|
+
return _recordRemovalFailure(`${stage} reported success but the directory survived`);
|
|
10227
|
+
}
|
|
10228
|
+
_removeWorktreeFailures.delete(resolved);
|
|
10229
|
+
bumpWorktreeGcMetric('success');
|
|
10230
|
+
if (extraMetric) bumpWorktreeGcMetric(extraMetric);
|
|
10231
|
+
return true;
|
|
10232
|
+
};
|
|
10233
|
+
|
|
9000
10234
|
// Windows: purge reserved-name pseudo-files (NUL, CON, etc.) that block normal deletion
|
|
9001
10235
|
if (process.platform === 'win32') {
|
|
9002
10236
|
_purgeReservedFiles(resolved);
|
|
@@ -9006,9 +10240,7 @@ function removeWorktree(wtPath, gitRoot, worktreeRoot, opts = {}) {
|
|
|
9006
10240
|
try {
|
|
9007
10241
|
// P-1d7a4f80 — argv form (shell:false) so wtPath can never be re-parsed by a shell.
|
|
9008
10242
|
shellSafeGitSync(['worktree', 'remove', wtPath, '--force'], { cwd: gitRoot, timeout: 15000 });
|
|
9009
|
-
|
|
9010
|
-
bumpWorktreeGcMetric('success');
|
|
9011
|
-
return true;
|
|
10243
|
+
return _settleRemoval('git worktree remove --force');
|
|
9012
10244
|
} catch (gitErr) {
|
|
9013
10245
|
try {
|
|
9014
10246
|
// P-b3d9a162 — re-run the real-repo refusal: a lock that released only
|
|
@@ -9026,10 +10258,7 @@ function removeWorktree(wtPath, gitRoot, worktreeRoot, opts = {}) {
|
|
|
9026
10258
|
`fs.rmSync(${resolved})`
|
|
9027
10259
|
);
|
|
9028
10260
|
try { shellSafeGitSync(['worktree', 'prune'], { cwd: gitRoot, timeout: 10000 }); } catch {}
|
|
9029
|
-
|
|
9030
|
-
bumpWorktreeGcMetric('success');
|
|
9031
|
-
if (attempt > 1) bumpWorktreeGcMetric('successAfterRetry');
|
|
9032
|
-
return true;
|
|
10261
|
+
return _settleRemoval('fs.rmSync', attempt > 1 ? 'successAfterRetry' : null);
|
|
9033
10262
|
} catch (rmErr) {
|
|
9034
10263
|
// Windows: try cmd /c rd /s /q for any error — handles reserved device names,
|
|
9035
10264
|
// locked files, and partially-deleted directories (not just EPERM)
|
|
@@ -9040,23 +10269,13 @@ function removeWorktree(wtPath, gitRoot, worktreeRoot, opts = {}) {
|
|
|
9040
10269
|
// argv element rather than interpolated into a shell string.
|
|
9041
10270
|
_execFileSync('cmd', ['/c', 'rd', '/s', '/q', resolved], { stdio: 'pipe', timeout: 15000, windowsHide: true });
|
|
9042
10271
|
try { shellSafeGitSync(['worktree', 'prune'], { cwd: gitRoot, timeout: 10000 }); } catch {}
|
|
9043
|
-
|
|
9044
|
-
bumpWorktreeGcMetric('success');
|
|
9045
|
-
return true;
|
|
10272
|
+
return _settleRemoval('rd /s /q');
|
|
9046
10273
|
} catch (rdErr) {
|
|
9047
10274
|
log('warn', `removeWorktree: rd /s /q fallback failed for ${wtPath}: ${rdErr.message}`);
|
|
9048
10275
|
}
|
|
9049
10276
|
}
|
|
9050
10277
|
_restoreWorktreeOwnerMarkerAfterFailedRemoval(resolved, hadOwnerMarker);
|
|
9051
|
-
|
|
9052
|
-
fail.count++;
|
|
9053
|
-
fail.lastAttempt = Date.now();
|
|
9054
|
-
fail.lastError = rmErr && rmErr.message ? rmErr.message : 'unknown';
|
|
9055
|
-
_removeWorktreeFailures.set(resolved, fail);
|
|
9056
|
-
_pruneRemoveWorktreeFailures();
|
|
9057
|
-
bumpWorktreeGcMetric('totalFailure');
|
|
9058
|
-
if (fail.count <= 3) log('warn', `removeWorktree: failed for ${wtPath} (attempt ${fail.count}/3): ${rmErr.message}`);
|
|
9059
|
-
return false;
|
|
10278
|
+
return _recordRemovalFailure(rmErr && rmErr.message ? rmErr.message : 'unknown');
|
|
9060
10279
|
}
|
|
9061
10280
|
}
|
|
9062
10281
|
}
|
|
@@ -9107,6 +10326,19 @@ function hasWorktreeOwnerMarker(worktreePath) {
|
|
|
9107
10326
|
}
|
|
9108
10327
|
}
|
|
9109
10328
|
|
|
10329
|
+
// Async twin of `hasWorktreeOwnerMarker`, for read-only reporting that must not
|
|
10330
|
+
// hold the event loop for one syscall per directory. Identical fail-closed
|
|
10331
|
+
// contract — keep the two in step.
|
|
10332
|
+
async function hasWorktreeOwnerMarkerAsync(worktreePath) {
|
|
10333
|
+
if (!worktreePath) return false;
|
|
10334
|
+
try {
|
|
10335
|
+
await fs.promises.access(path.join(path.resolve(worktreePath), WORKTREE_OWNER_MARKER));
|
|
10336
|
+
return true;
|
|
10337
|
+
} catch {
|
|
10338
|
+
return false;
|
|
10339
|
+
}
|
|
10340
|
+
}
|
|
10341
|
+
|
|
9110
10342
|
function hasProtectedQuarantineMarker(worktreePath) {
|
|
9111
10343
|
if (!worktreePath) return false;
|
|
9112
10344
|
try {
|
|
@@ -9125,6 +10357,34 @@ function hasProtectedQuarantineMarker(worktreePath) {
|
|
|
9125
10357
|
}
|
|
9126
10358
|
}
|
|
9127
10359
|
|
|
10360
|
+
// Async twin of `hasProtectedQuarantineMarker`, including the fail-SAFE `true`
|
|
10361
|
+
// on error: an unreadable ancestor chain must read as "protected", never as
|
|
10362
|
+
// "free to delete". Keep the two in step — note the sync twin probes with
|
|
10363
|
+
// `fs.existsSync`, which NEVER throws, so an unreadable ancestor reads as
|
|
10364
|
+
// "marker absent" and the walk continues. Matching that exactly is what keeps
|
|
10365
|
+
// the async pre-read from classifying a directory differently than the
|
|
10366
|
+
// synchronous fallback would.
|
|
10367
|
+
async function hasProtectedQuarantineMarkerAsync(worktreePath) {
|
|
10368
|
+
if (!worktreePath) return false;
|
|
10369
|
+
try {
|
|
10370
|
+
let current = path.resolve(worktreePath);
|
|
10371
|
+
while (true) {
|
|
10372
|
+
if (path.basename(current).includes(WORKTREE_QUARANTINE_PROTECTED_PATH_TOKEN)) return true;
|
|
10373
|
+
let markerExists = false;
|
|
10374
|
+
try {
|
|
10375
|
+
await fs.promises.access(path.join(current, WORKTREE_QUARANTINE_PROTECTION_MARKER));
|
|
10376
|
+
markerExists = true;
|
|
10377
|
+
} catch { markerExists = false; }
|
|
10378
|
+
if (markerExists) return true;
|
|
10379
|
+
const parent = path.dirname(current);
|
|
10380
|
+
if (parent === current) return false;
|
|
10381
|
+
current = parent;
|
|
10382
|
+
}
|
|
10383
|
+
} catch {
|
|
10384
|
+
return true;
|
|
10385
|
+
}
|
|
10386
|
+
}
|
|
10387
|
+
|
|
9128
10388
|
function _restoreWorktreeOwnerMarkerAfterFailedRemoval(worktreePath, hadOwnerMarker) {
|
|
9129
10389
|
if (!hadOwnerMarker || !worktreePath) return false;
|
|
9130
10390
|
let exists = false;
|
|
@@ -9326,7 +10586,7 @@ function createThrottleTracker({ label, baseBackoffMs = 60000, maxBackoffMs = 32
|
|
|
9326
10586
|
|
|
9327
10587
|
// ── Backoff Tracker Factory ─────────────────────────────────────────────────
|
|
9328
10588
|
// Generic graduated-backoff tracker for retryable *acquisition* failures (e.g.
|
|
9329
|
-
// ADO token minting via engine/ado
|
|
10589
|
+
// ADO token minting via engine/ado/token.js). Distinct from
|
|
9330
10590
|
// createThrottleTracker on three points so callers don't reach for the wrong
|
|
9331
10591
|
// one: (1) the FIRST failure backs off `baseMs`, not `baseMs*2` — a single
|
|
9332
10592
|
// transient blip must not blackout for the full cap; (2) recordSuccess RESETS
|
|
@@ -9370,7 +10630,7 @@ function createBackoffTracker({ baseMs = 30000, maxMs = 10 * 60 * 1000 } = {}) {
|
|
|
9370
10630
|
|
|
9371
10631
|
// ── npm package identity (W-ms1j8m54000g4a4a) ───────────────────────────────
|
|
9372
10632
|
// Minions is published to more than one npm distribution channel: the public
|
|
9373
|
-
// `@yemi33/minions` and internal builds renamed by
|
|
10633
|
+
// `@yemi33/minions` and internal builds renamed by scripts/prepare-package.js
|
|
9374
10634
|
// (`INTERNAL_PACKAGE_NAME`, default `@yemishin_microsoft/minions`). Anything
|
|
9375
10635
|
// that shells out to `npm view` / `npm install -g` / `npm uninstall -g` must
|
|
9376
10636
|
// target the package that is ACTUALLY installed — hardcoding the public name
|
|
@@ -9416,6 +10676,37 @@ function resolvePackageName(pkgRoot) {
|
|
|
9416
10676
|
return PUBLIC_PACKAGE_NAME;
|
|
9417
10677
|
}
|
|
9418
10678
|
|
|
10679
|
+
// W-ms6b7prf006tef7f-b: install-channel marker. `minions init` records the
|
|
10680
|
+
// runtime's version (`.minions-version`) and source commit (`.minions-commit`);
|
|
10681
|
+
// this marker records WHICH npm distribution produced the runtime root, so an
|
|
10682
|
+
// internal (`@opg-microsoft/minions`) root and a public (`@yemi33/minions`) one
|
|
10683
|
+
// are distinguishable on disk. Written by bin/minions.js#saveInstalledVersion.
|
|
10684
|
+
const RUNTIME_CHANNEL_MARKER = '.minions-package';
|
|
10685
|
+
|
|
10686
|
+
// Read the install-channel marker from a runtime root. Returns
|
|
10687
|
+
// `{ name, version }` or null. A MISSING marker is "unknown", never an error:
|
|
10688
|
+
// every runtime root initialized before this marker existed has none, and a
|
|
10689
|
+
// legacy root must keep working. A marker whose name we would not be willing to
|
|
10690
|
+
// put on an npm command line is treated as unknown for the same reason
|
|
10691
|
+
// resolvePackageName falls back — a channel identity is only useful if it is
|
|
10692
|
+
// safe to act on.
|
|
10693
|
+
//
|
|
10694
|
+
// The name is validated EXACTLY as read, never trimmed first: this value is
|
|
10695
|
+
// compared against a name that gets interpolated into a shell command, so
|
|
10696
|
+
// normalizing junk into something acceptable is the wrong direction. A marker
|
|
10697
|
+
// name with surrounding whitespace is a marker we do not understand.
|
|
10698
|
+
// The version is advisory (it only ever reaches an operator-facing message), so
|
|
10699
|
+
// an unparseable one degrades to null rather than voiding the channel identity.
|
|
10700
|
+
function readRuntimeChannel(runtimeRoot) {
|
|
10701
|
+
if (typeof runtimeRoot !== 'string' || !runtimeRoot) return null;
|
|
10702
|
+
try {
|
|
10703
|
+
const raw = JSON.parse(fs.readFileSync(path.join(runtimeRoot, RUNTIME_CHANNEL_MARKER), 'utf8'));
|
|
10704
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
10705
|
+
if (!isValidNpmPackageName(raw.name)) return null;
|
|
10706
|
+
return { name: raw.name, version: isValidNpmVersion(raw.version) ? raw.version : null };
|
|
10707
|
+
} catch { return null; }
|
|
10708
|
+
}
|
|
10709
|
+
|
|
9419
10710
|
function assertNpmPackageName(pkgName) {
|
|
9420
10711
|
if (!isValidNpmPackageName(pkgName)) {
|
|
9421
10712
|
throw new Error(`Refusing to run npm against an invalid package name: ${JSON.stringify(pkgName)}`);
|
|
@@ -9439,9 +10730,138 @@ function buildNpmGlobalUninstallCommand(pkgName) {
|
|
|
9439
10730
|
return `npm uninstall -g ${assertNpmPackageName(pkgName)}`;
|
|
9440
10731
|
}
|
|
9441
10732
|
|
|
10733
|
+
// ── Update channel authority (W-ms6b7prf006tef7f-d) ─────────────────────────
|
|
10734
|
+
// `resolvePackageName(PKG_ROOT)` answers "which package would an update
|
|
10735
|
+
// install?" — it does NOT answer "which channel is this runtime on?". Those
|
|
10736
|
+
// diverge in exactly the case that hurts: the opg repo's own package.json is
|
|
10737
|
+
// named `@yemi33/minions`, and a still-public global shim can sit in front of an
|
|
10738
|
+
// internal runtime. Either way PKG_ROOT resolves PUBLIC while the runtime is
|
|
10739
|
+
// INTERNAL, and an unguarded `npm view` → `npm install -g` then drops a stale
|
|
10740
|
+
// public build on top of a newer internal one.
|
|
10741
|
+
//
|
|
10742
|
+
// The RUNTIME ROOT is therefore the channel authority: `minions init` records
|
|
10743
|
+
// the installing distribution in `<runtimeRoot>/.minions-package`, and every
|
|
10744
|
+
// update is gated against it. A root with no marker is a legacy install — it
|
|
10745
|
+
// falls back to the previous behavior with a warning, never a refusal.
|
|
10746
|
+
|
|
10747
|
+
/**
|
|
10748
|
+
* The supported way to move an internal runtime forward. Returned as lines
|
|
10749
|
+
* (never executed here) so the CLI can print them verbatim on a refusal. Every
|
|
10750
|
+
* form pins `--runtime-root` so the installer cannot re-resolve to a different
|
|
10751
|
+
* root than the one we just refused to overwrite.
|
|
10752
|
+
*
|
|
10753
|
+
* The root is always double-quoted: runtime roots routinely live under paths
|
|
10754
|
+
* with spaces (`C:\Users\First Last\.minions`), and an unquoted path would make
|
|
10755
|
+
* the printed command fail as `unknown argument`. `--runtime-root` is a real
|
|
10756
|
+
* flag on all three entry points (`bin/install-internal-minions.js#parseArgs`) —
|
|
10757
|
+
* a refusal must print something the operator can paste and run.
|
|
10758
|
+
*
|
|
10759
|
+
* Each line uses the invocation form that actually RUNS on its platform, and
|
|
10760
|
+
* matches the entry points documented in docs/internal-install.md: POSIX needs
|
|
10761
|
+
* the `./` prefix (a bare `scripts/x.sh` is not resolved from a shell), while
|
|
10762
|
+
* PowerShell takes the native backslash path.
|
|
10763
|
+
*/
|
|
10764
|
+
function buildInternalInstallRemediation({ runtimeRoot, packageName } = {}) {
|
|
10765
|
+
const root = typeof runtimeRoot === 'string' && runtimeRoot ? runtimeRoot : '<runtime root>';
|
|
10766
|
+
const pkg = isValidNpmPackageName(packageName) ? packageName : '<internal package>';
|
|
10767
|
+
const args = `--runtime-root "${root}" --package ${pkg}`;
|
|
10768
|
+
return [
|
|
10769
|
+
`Windows (PowerShell): scripts\\install-internal-minions.ps1 ${args}`,
|
|
10770
|
+
`Linux / macOS: ./scripts/install-internal-minions.sh ${args}`,
|
|
10771
|
+
`Any platform: node bin/install-internal-minions.js ${args}`,
|
|
10772
|
+
];
|
|
10773
|
+
}
|
|
10774
|
+
|
|
10775
|
+
/**
|
|
10776
|
+
* Decide whether `minions update` may install `resolvePackageName(pkgRoot)` on
|
|
10777
|
+
* top of the runtime at `runtimeRoot`. Pure: reads two files, shells out to
|
|
10778
|
+
* nothing, and is safe to call before any npm work.
|
|
10779
|
+
*
|
|
10780
|
+
* Returns `{ pkgName, runtimeName, runtimeVersion, allowed, reason, warning,
|
|
10781
|
+
* remediation }` where `reason` is one of:
|
|
10782
|
+
* - `match` — same channel; proceed silently
|
|
10783
|
+
* - `unknown-runtime-channel` — legacy root, no marker; proceed + warn
|
|
10784
|
+
* - `cross-channel` — channels differ but the target is not
|
|
10785
|
+
* the public package; proceed + warn
|
|
10786
|
+
* - `internal-runtime-public-package` — REFUSE: this is the public downgrade
|
|
10787
|
+
*/
|
|
10788
|
+
function resolveUpdateChannel({ pkgRoot, runtimeRoot } = {}) {
|
|
10789
|
+
const pkgName = resolvePackageName(pkgRoot);
|
|
10790
|
+
const runtime = readRuntimeChannel(runtimeRoot);
|
|
10791
|
+
const runtimeName = runtime ? runtime.name : null;
|
|
10792
|
+
const base = { pkgName, runtimeName, runtimeVersion: runtime ? runtime.version : null, remediation: [] };
|
|
10793
|
+
|
|
10794
|
+
if (!runtimeName) {
|
|
10795
|
+
return {
|
|
10796
|
+
...base,
|
|
10797
|
+
allowed: true,
|
|
10798
|
+
reason: 'unknown-runtime-channel',
|
|
10799
|
+
warning: `Runtime channel is unknown (no ${RUNTIME_CHANNEL_MARKER} marker) — updating as ${pkgName}; re-run \`minions init\` to record the channel.`,
|
|
10800
|
+
};
|
|
10801
|
+
}
|
|
10802
|
+
if (runtimeName === pkgName) {
|
|
10803
|
+
return { ...base, allowed: true, reason: 'match', warning: null };
|
|
10804
|
+
}
|
|
10805
|
+
// The one unrecoverable direction: an internal runtime being overwritten by
|
|
10806
|
+
// the public package. Never attempt it — the public build is a different,
|
|
10807
|
+
// and typically older, distribution of the same `minions` bin.
|
|
10808
|
+
if (runtimeName !== PUBLIC_PACKAGE_NAME && pkgName === PUBLIC_PACKAGE_NAME) {
|
|
10809
|
+
return {
|
|
10810
|
+
...base,
|
|
10811
|
+
allowed: false,
|
|
10812
|
+
reason: 'internal-runtime-public-package',
|
|
10813
|
+
warning: null,
|
|
10814
|
+
remediation: buildInternalInstallRemediation({ runtimeRoot, packageName: runtimeName }),
|
|
10815
|
+
};
|
|
10816
|
+
}
|
|
10817
|
+
// Any other mismatch (public runtime → internal package, or internal →
|
|
10818
|
+
// a different internal) is a legitimate cutover. Proceed, but say so.
|
|
10819
|
+
return {
|
|
10820
|
+
...base,
|
|
10821
|
+
allowed: true,
|
|
10822
|
+
reason: 'cross-channel',
|
|
10823
|
+
warning: `Runtime channel is ${runtimeName} but this update would install ${pkgName} — proceeding with a cross-channel install.`,
|
|
10824
|
+
};
|
|
10825
|
+
}
|
|
10826
|
+
|
|
10827
|
+
/**
|
|
10828
|
+
* In-channel downgrade guard. `npm view <pkg> version` can resolve BACKWARDS off
|
|
10829
|
+
* a stale local packument (same root cause as the post-install version-advance
|
|
10830
|
+
* check), which silently reinstalls an older build over a newer one. Refuse
|
|
10831
|
+
* unless the operator opts in.
|
|
10832
|
+
*
|
|
10833
|
+
* Numeric comparison via compareDottedVersions — '1.0.9' must not look newer
|
|
10834
|
+
* than '1.0.76'. An unknown installed version can never produce a refusal.
|
|
10835
|
+
*/
|
|
10836
|
+
function evaluateUpdateDowngrade({ installedVersion, targetVersion, allowDowngrade = false } = {}) {
|
|
10837
|
+
const known = (v) => typeof v === 'string' && isValidNpmVersion(v);
|
|
10838
|
+
if (!known(installedVersion) || !known(targetVersion)) {
|
|
10839
|
+
return { allowed: true, isDowngrade: false, message: null };
|
|
10840
|
+
}
|
|
10841
|
+
if (compareDottedVersions(targetVersion, installedVersion) >= 0) {
|
|
10842
|
+
return { allowed: true, isDowngrade: false, message: null };
|
|
10843
|
+
}
|
|
10844
|
+
if (allowDowngrade) {
|
|
10845
|
+
return { allowed: true, isDowngrade: true, message: `Installing ${targetVersion} over ${installedVersion} (downgrade allowed by --allow-downgrade).` };
|
|
10846
|
+
}
|
|
10847
|
+
return {
|
|
10848
|
+
allowed: false,
|
|
10849
|
+
isDowngrade: true,
|
|
10850
|
+
message: [
|
|
10851
|
+
`The registry resolved ${targetVersion}, which is OLDER than the installed ${installedVersion}.`,
|
|
10852
|
+
'This usually means npm resolved a stale local registry-metadata cache.',
|
|
10853
|
+
'Try: npm cache clean --force',
|
|
10854
|
+
'Then: minions update',
|
|
10855
|
+
'To install the older version anyway: minions update --allow-downgrade',
|
|
10856
|
+
].join('\n'),
|
|
10857
|
+
};
|
|
10858
|
+
}
|
|
10859
|
+
|
|
9442
10860
|
module.exports = {
|
|
10861
|
+
ROOT_POINTER_PATH,
|
|
9443
10862
|
MINIONS_DIR,
|
|
9444
10863
|
ENGINE_DIR,
|
|
10864
|
+
resolveDefinitionDir,
|
|
9445
10865
|
CRASH_REPORTS_DIR,
|
|
9446
10866
|
getEngineCrashDiagnosticsEnv,
|
|
9447
10867
|
pruneCrashDiagnosticsReports,
|
|
@@ -9476,9 +10896,16 @@ module.exports = {
|
|
|
9476
10896
|
isValidNpmPackageName,
|
|
9477
10897
|
isValidNpmVersion,
|
|
9478
10898
|
resolvePackageName,
|
|
10899
|
+
// Install-channel marker (W-ms6b7prf006tef7f-b)
|
|
10900
|
+
RUNTIME_CHANNEL_MARKER,
|
|
10901
|
+
readRuntimeChannel,
|
|
9479
10902
|
buildNpmViewVersionCommand,
|
|
9480
10903
|
buildNpmGlobalInstallCommand,
|
|
9481
10904
|
buildNpmGlobalUninstallCommand,
|
|
10905
|
+
// Update channel authority (W-ms6b7prf006tef7f-d)
|
|
10906
|
+
buildInternalInstallRemediation,
|
|
10907
|
+
resolveUpdateChannel,
|
|
10908
|
+
evaluateUpdateDowngrade,
|
|
9482
10909
|
nodeSupportsBuiltinSqlite,
|
|
9483
10910
|
nodeSqliteRemediationLine,
|
|
9484
10911
|
log,
|
|
@@ -9554,6 +10981,7 @@ module.exports = {
|
|
|
9554
10981
|
run,
|
|
9555
10982
|
runFile,
|
|
9556
10983
|
cleanChildEnv,
|
|
10984
|
+
applyAgentTestLoadSafetyEnv,
|
|
9557
10985
|
gitEnv,
|
|
9558
10986
|
parseStreamJsonOutput,
|
|
9559
10987
|
isStaleResumeSessionSignal, // W-ms0vk9nv000z985b — stale `--resume` session/cwd detection
|
|
@@ -9563,24 +10991,36 @@ module.exports = {
|
|
|
9563
10991
|
classifyInboxItem,
|
|
9564
10992
|
isEngineSystemAlert,
|
|
9565
10993
|
ENGINE_DEFAULTS,
|
|
10994
|
+
QUARANTINE_REF_RETENTION_DAYS, // W-ms87lj8j018j2572 — single source of truth; quarantine-refs.js re-exports these
|
|
10995
|
+
QUARANTINE_REF_RETENTION_MS,
|
|
10996
|
+
resolveQuarantineRetentionMs, // W-ms87rbe601a66d13 — the one seam both quarantine sweeps read the operator override through
|
|
9566
10997
|
CODEX_SANDBOX_MODES,
|
|
9567
10998
|
validateCodexSandbox,
|
|
9568
10999
|
resolveCodexCcSandbox,
|
|
9569
11000
|
resolvePollFlag, // P-c4d8e1a3 — granular per-poller flag resolution
|
|
9570
11001
|
resolveAgentCli, resolveCcCli, resolveCcUseWorkerPool, resolveAgentUseWorkerPool, resolveAgentAcpPoolSize, resolveAgentModel, resolveCcModel,
|
|
11002
|
+
resolveAcpHandshakeTimeoutMs, ACP_HANDSHAKE_TIMEOUT_MIN_MS, ACP_HANDSHAKE_TIMEOUT_MAX_MS,
|
|
9571
11003
|
resolveAgentAllowedTools, resolveAgentMaxBudget, resolveAgentBareMode, resolvePropagateClaudeMdForNonClaudeRuntimes,
|
|
9572
11004
|
resolveCreatePrsAsDraft,
|
|
11005
|
+
resolveAutoQaCompletedPrds,
|
|
9573
11006
|
applyLegacyCcModelMigration, _resetLegacyCcModelMigrationFlag,
|
|
9574
11007
|
runtimeConfigWarnings,
|
|
9575
11008
|
projectWorkSourceWarnings,
|
|
9576
11009
|
backfillProjectWorkSourceDefaults,
|
|
9577
11010
|
WORK_ITEM_PRIORITY, WORK_ITEM_PRIORITY_VALUES, WORK_ITEM_PRIORITY_ERROR_CODE, validateWorkItemPriority,
|
|
9578
|
-
WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, isPrdArchived, isDefunctPrd, WORK_TYPE, isFixLikeWorkType, WORKTREE_REQUIRING_TYPES, LIVE_VALIDATION_WORK_TYPES, VALID_WORK_TYPES, resolveWorkItemTypeFromPrdItem, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, PR_POLLABLE_STATUSES, PR_PENDING_REASON, BUILD_STATUS, REVIEW_STATUS, FETCH_TIMEOUT_MS, RETRY_DELAY_MS, ADO_TOKEN_REFRESH_MAX_RETRIES, DISPATCH_RESULT, mutateMetrics, mutateWatches, mutateScheduleRuns, mutatePipelineRuns, mutateManagedProcesses, mutateWorktreePool, mutateQaRuns, mutateQaSessions, trackReviewMetric, queuePlanToPrd, extractPlanDeclaredProject, extractPlanTargetProjects,
|
|
11011
|
+
WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, isPrdArchived, isDefunctPrd, WORK_TYPE, isFixLikeWorkType, WORKTREE_REQUIRING_TYPES, LIVE_VALIDATION_WORK_TYPES, VALID_WORK_TYPES, resolveWorkItemTypeFromPrdItem, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, PR_POLLABLE_STATUSES, isPrPollable, resolveAbandonedPrRepollWindowMs, getPrAbandonedAtMs, stampPrAbandonedAt, clearPrAbandonedAt, PR_PENDING_REASON, BUILD_STATUS, REVIEW_STATUS, FETCH_TIMEOUT_MS, RETRY_DELAY_MS, ADO_TOKEN_REFRESH_MAX_RETRIES, DISPATCH_RESULT, mutateMetrics, mutateWatches, mutateScheduleRuns, mutatePipelineRuns, mutateManagedProcesses, mutateWorktreePool, mutateQaRuns, mutateQaSessions, trackReviewMetric, queuePlanToPrd, extractPlanDeclaredProject, extractPlanTargetProjects,
|
|
11012
|
+
// W-ms5tb6ha016fda10 — explicit, fail-closed control-plane execution surface
|
|
11013
|
+
EXECUTION_SURFACE, EXECUTION_SURFACE_VALUES, CONTROL_PLANE_ELIGIBLE_TYPES, CLOUD_ELIGIBLE_TYPES,
|
|
11014
|
+
isControlPlaneEligibleType, validateExecutionSurface, readDeclaredExecutionSurface, isControlPlaneExecution,
|
|
11015
|
+
isCloudEligibleType, isCloudExecution,
|
|
11016
|
+
// W-ms5tb6ha016fda10 — shared remote-ref refresh serialization + classification
|
|
11017
|
+
isGitRefLockError, withSerializedRemoteRefRefresh, _resetRemoteRefRefreshChainsForTesting,
|
|
9579
11018
|
WATCH_STATUS, WATCH_TARGET_TYPE, WATCH_CONDITION, WATCH_ABSOLUTE_CONDITIONS, WATCH_ACTION_TYPE,
|
|
9580
11019
|
WATCH_STALLED_DEFAULT_TICKS, WATCH_STUCK_STAGE_DEFAULT_TICKS,
|
|
9581
11020
|
PIPELINE_STATUS, STAGE_TYPE, MEETING_STATUS, AGENT_STATUS,
|
|
9582
11021
|
FAILURE_CLASS, COMPLETION_FIELDS,
|
|
9583
11022
|
resolveTerminalDispatchResult, resolveTerminalWorkerState,
|
|
11023
|
+
DISPATCH_COMPLETION_PATH, acceptCompletionReportForNonce,
|
|
9584
11024
|
claimDispatchFinalization, releaseDispatchFinalization, waitForDispatchFinalization,
|
|
9585
11025
|
DEFAULT_AGENT_METRICS,
|
|
9586
11026
|
DEFAULT_AGENTS,
|
|
@@ -9607,6 +11047,10 @@ module.exports = {
|
|
|
9607
11047
|
initializeProjectState,
|
|
9608
11048
|
sameResolvedPath,
|
|
9609
11049
|
realPathForComparison, // exported for testing
|
|
11050
|
+
realPathForComparisonAsync,
|
|
11051
|
+
pathForComparison,
|
|
11052
|
+
pathForComparisonAsync,
|
|
11053
|
+
mapWithConcurrency,
|
|
9610
11054
|
getPrLinks,
|
|
9611
11055
|
addPrLink,
|
|
9612
11056
|
normalizePrScopeSegment, // exported for testing
|
|
@@ -9637,12 +11081,20 @@ module.exports = {
|
|
|
9637
11081
|
findPrRecord,
|
|
9638
11082
|
isPlaceholderPrTitle,
|
|
9639
11083
|
snapshotPrRecord,
|
|
11084
|
+
PR_AGENT_HUMAN,
|
|
11085
|
+
buildGithubAuthorIdentity,
|
|
11086
|
+
buildAdoAuthorIdentity,
|
|
11087
|
+
normalizePrAuthorIdentity,
|
|
11088
|
+
mergePrAuthorIdentity,
|
|
11089
|
+
prAuthorLabel,
|
|
9640
11090
|
applyPrFieldDelta,
|
|
9641
11091
|
normalizePrRecord,
|
|
9642
11092
|
normalizePrRecords,
|
|
9643
11093
|
normalizePrLinkItems, // exported for testing
|
|
9644
11094
|
mergePrLinkItems, // exported for testing
|
|
9645
11095
|
isContextOnlyPrRecord,
|
|
11096
|
+
isPrAutoFixEnabled,
|
|
11097
|
+
isPrAutoReviewEnabled,
|
|
9646
11098
|
shouldAdoptPlatformCreated, // W-mrezh0yb0007f733 — platform creationDate authority
|
|
9647
11099
|
upsertPullRequestRecord,
|
|
9648
11100
|
collapseDuplicatePrRecords, // P-e9f0a2b4 — one-time repair helper
|
|
@@ -9663,6 +11115,11 @@ module.exports = {
|
|
|
9663
11115
|
|
|
9664
11116
|
computeSourcePlanContentHash, // W-mqfevwr60018bd09 — sha256 of source plan markdown body
|
|
9665
11117
|
isSourcePlanContentStale, // W-mqfevwr60018bd09 — gate destructive PRD resync on actual content change
|
|
11118
|
+
PRD_REVISION_STATE, // W-msa0mrus00mh466f — canonical revision-requested ⇄ planStale state machine
|
|
11119
|
+
findPrdRevisionWorkItem,
|
|
11120
|
+
derivePrdRevisionState,
|
|
11121
|
+
resolvePrdRevisionFeedback,
|
|
11122
|
+
applyPrdRevisionCompletion,
|
|
9666
11123
|
sanitizeBranch,
|
|
9667
11124
|
getOperatorLogin,
|
|
9668
11125
|
deriveWorkItemBranchName,
|
|
@@ -9671,6 +11128,7 @@ module.exports = {
|
|
|
9671
11128
|
isWorktreeRootInfraEntry,
|
|
9672
11129
|
WORKTREE_SCRATCH_DIR_NAME,
|
|
9673
11130
|
isPathInside,
|
|
11131
|
+
isResolvedPathInsideOrEqual,
|
|
9674
11132
|
isPathInsideOrEqual,
|
|
9675
11133
|
pathsOverlap,
|
|
9676
11134
|
parseWorktreePorcelain,
|
|
@@ -9731,6 +11189,7 @@ module.exports = {
|
|
|
9731
11189
|
clearWorktreeFailureCache,
|
|
9732
11190
|
removeWorktree,
|
|
9733
11191
|
isWorktreePathLive,
|
|
11192
|
+
listLiveWorktreePaths,
|
|
9734
11193
|
getWorktreeBlockingDispatchInfo,
|
|
9735
11194
|
WORKTREE_OWNER_MARKER,
|
|
9736
11195
|
WORKTREE_RETRY_PATH_MARKER,
|
|
@@ -9738,8 +11197,11 @@ module.exports = {
|
|
|
9738
11197
|
WORKTREE_QUARANTINE_PROTECTED_PATH_TOKEN,
|
|
9739
11198
|
writeWorktreeOwnerMarker,
|
|
9740
11199
|
hasWorktreeOwnerMarker,
|
|
11200
|
+
hasWorktreeOwnerMarkerAsync,
|
|
9741
11201
|
hasProtectedQuarantineMarker,
|
|
11202
|
+
hasProtectedQuarantineMarkerAsync,
|
|
9742
11203
|
_restoreWorktreeOwnerMarkerAfterFailedRemoval, // exported for testing
|
|
11204
|
+
_verifyWorktreeRemoved, // exported for testing
|
|
9743
11205
|
isWorktreeOwnerMarkerStatusLine,
|
|
9744
11206
|
_normalizeWorktreePath, // exported for testing
|
|
9745
11207
|
_writeWorktreeSkipLiveInboxNote, // exported for testing
|
|
@@ -9765,7 +11227,7 @@ module.exports = {
|
|
|
9765
11227
|
backfillPrPrdItems,
|
|
9766
11228
|
};
|
|
9767
11229
|
|
|
9768
|
-
// Re-export process-management helpers extracted to engine/process-utils.js
|
|
11230
|
+
// Re-export process-management helpers extracted to engine/processes/process-utils.js
|
|
9769
11231
|
// (P-mrb8yg9x001k1401-b) so every existing require('./shared') call keeps
|
|
9770
11232
|
// working unchanged.
|
|
9771
11233
|
module.exports = { ...module.exports, ...processUtils };
|