@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/dispatch.js — Dispatch queue management: add, complete, mutate, alerts.
|
|
2
|
+
* engine/orchestration/dispatch.js — Dispatch queue management: add, complete, mutate, alerts.
|
|
3
3
|
* Extracted from engine.js for modularity. No logic changes.
|
|
4
4
|
*
|
|
5
5
|
* ## Dispatch meta schema — optional fields (M006)
|
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
* completing agent concluded before handing off.
|
|
14
14
|
*
|
|
15
15
|
* These fields are optional and additive. Dispatches without them are unaffected.
|
|
16
|
-
* When present, engine/playbook.js injects a "Handoff Context" section into the
|
|
16
|
+
* When present, engine/agents/playbook.js injects a "Handoff Context" section into the
|
|
17
17
|
* rendered prompt so the receiving agent has explicit orientation.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
const fs = require('fs');
|
|
21
21
|
const path = require('path');
|
|
22
|
-
const shared = require('
|
|
23
|
-
const queries = require('
|
|
22
|
+
const shared = require('../core/shared');
|
|
23
|
+
const queries = require('../core/queries');
|
|
24
24
|
const { setCooldown, setCooldownFailure } = require('./cooldown');
|
|
25
25
|
const dispatchEvents = require('./dispatch-events');
|
|
26
26
|
|
|
@@ -32,6 +32,11 @@ const { getConfig, INBOX_DIR } = queries;
|
|
|
32
32
|
|
|
33
33
|
const MINIONS_DIR = shared.MINIONS_DIR;
|
|
34
34
|
|
|
35
|
+
// W-ms9tcs3o01y03383 — closed vocabulary for the `completionPath` observability
|
|
36
|
+
// field written by completeDispatch. Sourced from the shared constant so the
|
|
37
|
+
// engine, timeout watchdog, and any future finalizer cannot invent new values.
|
|
38
|
+
const COMPLETION_PATH_VALUES = new Set(Object.values(shared.DISPATCH_COMPLETION_PATH || {}));
|
|
39
|
+
|
|
35
40
|
// Dispatch types that do not push code to a PR branch. These remain dispatchable
|
|
36
41
|
// against context-only PRs because casting a review vote, posting a
|
|
37
42
|
// comment, asking a question, or running a read-only explore never mutates the
|
|
@@ -110,7 +115,7 @@ function _sidecarChangedPrompts(dispatch, prevSnap) {
|
|
|
110
115
|
// they round-trip automatically. SQLite failures propagate; the CLI
|
|
111
116
|
// shim in bin/minions.js guarantees node:sqlite is loadable.
|
|
112
117
|
function mutateDispatch(mutator) {
|
|
113
|
-
const store = require('
|
|
118
|
+
const store = require('../persistence/dispatch-store');
|
|
114
119
|
const { wrote, result } = store.applyDispatchMutation((dispatch) => {
|
|
115
120
|
dispatch.pending = Array.isArray(dispatch.pending) ? dispatch.pending : [];
|
|
116
121
|
dispatch.active = Array.isArray(dispatch.active) ? dispatch.active : [];
|
|
@@ -124,8 +129,9 @@ function mutateDispatch(mutator) {
|
|
|
124
129
|
return next;
|
|
125
130
|
});
|
|
126
131
|
if (wrote) {
|
|
127
|
-
try { require('
|
|
128
|
-
try { require('
|
|
132
|
+
try { require('../core/queries').invalidateDispatchCache(); } catch {}
|
|
133
|
+
try { require('../core/queries').invalidateWorkItemsCache(); } catch {}
|
|
134
|
+
try { require('../persistence/db-events').emitStateEvent('dispatch'); } catch { /* optional */ }
|
|
129
135
|
}
|
|
130
136
|
return result;
|
|
131
137
|
}
|
|
@@ -445,7 +451,7 @@ function _isPrdSourcedAndVetted(wi) {
|
|
|
445
451
|
const sourcePlan = wi && wi.sourcePlan;
|
|
446
452
|
if (!sourcePlan || typeof sourcePlan !== 'string') return false;
|
|
447
453
|
try {
|
|
448
|
-
const plan = require('
|
|
454
|
+
const plan = require('../planning/prd-store').readPrd(sourcePlan);
|
|
449
455
|
if (!plan || !plan.missing_features) return false;
|
|
450
456
|
const status = plan.status || shared.PLAN_STATUS.ACTIVE;
|
|
451
457
|
return status === shared.PLAN_STATUS.APPROVED || status === shared.PLAN_STATUS.ACTIVE;
|
|
@@ -697,13 +703,13 @@ async function _fetchChangedFilesForPrunedEntry(entry, project) {
|
|
|
697
703
|
const host = String(project.repoHost || '').toLowerCase();
|
|
698
704
|
try {
|
|
699
705
|
if (host === 'github') {
|
|
700
|
-
const github = require('
|
|
706
|
+
const github = require('../providers/github');
|
|
701
707
|
const slug = github.getRepoSlug(project);
|
|
702
708
|
if (!slug) return null;
|
|
703
709
|
return await github.getPrChangedFiles(slug, prNumber);
|
|
704
710
|
}
|
|
705
711
|
if (host === 'ado' || !host) {
|
|
706
|
-
const ado = require('
|
|
712
|
+
const ado = require('../ado');
|
|
707
713
|
return await ado.getPrChangedFiles(project, prNumber);
|
|
708
714
|
}
|
|
709
715
|
} catch (e) {
|
|
@@ -847,7 +853,9 @@ function isRetryableFailureReason(reason = '', failureClass = '') {
|
|
|
847
853
|
FAILURE_CLASS.LIVE_CHECKOUT_STALE_BASE, // W-mr98op8w000ma4ad — live-checkout STALE-BASE GUARD refused to fork a new branch off a local mainRef that has diverged from origin/<mainRef> with unpushed commits; contaminated local base does not change on retry (operator must reconcile the local base branch — the engine never resets it — then re-dispatch)
|
|
848
854
|
FAILURE_CLASS.LIVE_CHECKOUT_WRONG_BASE, // W-mr3lunnq000o9f41 — live-checkout refused to fork a new branch because the operator HEAD is on an unrelated branch (not the project base) and no local base ref was usable; forking off it would bake that branch's commits into the new PR (scope contamination). Deterministic — mechanical retry reproduces it (operator must `git checkout <mainRef>`, then re-dispatch)
|
|
849
855
|
FAILURE_CLASS.OUTPUT_TRUNCATED, // P-8e4c2a17 — agent stdout exceeded the hard capture cap before the terminal result event; mechanical retry just reproduces the overflow (agent must reduce output volume or the task must be split)
|
|
850
|
-
FAILURE_CLASS.PRE_DISPATCH_EVAL_STUCK, // W-mrcrh5hj0017d22f — pre-dispatch-eval rejected the SAME unchanged description ENGINE_DEFAULTS.preDispatchEvalMaxRejections times in a row; non-retryable until the description changes (engine/shared.js:4986)
|
|
856
|
+
FAILURE_CLASS.PRE_DISPATCH_EVAL_STUCK, // W-mrcrh5hj0017d22f — pre-dispatch-eval rejected the SAME unchanged description ENGINE_DEFAULTS.preDispatchEvalMaxRejections times in a row; non-retryable until the description changes (engine/core/shared.js:4986)
|
|
857
|
+
FAILURE_CLASS.INVALID_EXECUTION_SURFACE, // W-ms5tb6ha016fda10 — the WI declared a meta.executionSurface that failed fail-closed validation (unknown value, `control-plane` on a repo-reading/mutating type, or `cloud` on a type whose deliverable is not a PR); re-running with the same declaration reproduces it (operator must fix or remove the field)
|
|
858
|
+
FAILURE_CLASS.CLOUD_PROVIDER_UNAVAILABLE, // Cloud dispatch surface — the WI validly declared `cloud` but no registered+configured provider resolved (or provider resolution threw). Structural: the same dispatch reproduces it identically until an operator configures a provider or drops the surface declaration. Deliberately NOT retryable so a misconfiguration cannot silently burn the item's retry budget — and note the engine never downgrades the refusal to a local dispatch.
|
|
851
859
|
]);
|
|
852
860
|
if (neverRetry.has(failureClass)) return false;
|
|
853
861
|
}
|
|
@@ -979,6 +987,7 @@ function markCompletedSourceWorkItem(item) {
|
|
|
979
987
|
delete wi.failReason;
|
|
980
988
|
delete wi.failedAt;
|
|
981
989
|
delete wi._retryCount;
|
|
990
|
+
delete wi._infrastructureRetryCount;
|
|
982
991
|
if (!wi.completedAt) wi.completedAt = ts();
|
|
983
992
|
if (item.agent && !wi.dispatched_to) wi.dispatched_to = item.agent;
|
|
984
993
|
terminal = true;
|
|
@@ -1021,6 +1030,47 @@ function deferDispatchForWorktreeProtection(id, protection = {}, terminal = {})
|
|
|
1021
1030
|
return deferred;
|
|
1022
1031
|
}
|
|
1023
1032
|
|
|
1033
|
+
// W-mrzqfozm00035365 — Return an active (or pending) dispatch to a RECOVERABLE
|
|
1034
|
+
// pending gate without recording a failure or consuming a retry. Used when a
|
|
1035
|
+
// dependency-branch merge conflict is detected in engine.js#spawnAgent BEFORE
|
|
1036
|
+
// the agent starts: the downstream work item is not at fault, so it must not
|
|
1037
|
+
// burn its retry budget or go terminal-`failed` while its deterministic
|
|
1038
|
+
// conflict-repair WI is worked. This ONLY tears down the dispatch record
|
|
1039
|
+
// cleanly (drop prompt sidecar, clear the completed dedupe key so the item can
|
|
1040
|
+
// re-dispatch once the gate clears); the caller stamps the work item's
|
|
1041
|
+
// status/_pendingReason/_dependencyConflict marker.
|
|
1042
|
+
function deferDispatchToPending(id, reason = '') {
|
|
1043
|
+
let item = null;
|
|
1044
|
+
mutateDispatch((dispatch) => {
|
|
1045
|
+
let idx = (dispatch.active || []).findIndex(d => d.id === id);
|
|
1046
|
+
if (idx >= 0) {
|
|
1047
|
+
item = dispatch.active.splice(idx, 1)[0];
|
|
1048
|
+
} else {
|
|
1049
|
+
idx = (dispatch.pending || []).findIndex(d => d.id === id);
|
|
1050
|
+
if (idx >= 0) item = dispatch.pending.splice(idx, 1)[0];
|
|
1051
|
+
}
|
|
1052
|
+
if (!item) return dispatch;
|
|
1053
|
+
try { deleteDispatchPromptSidecar(item); } catch { /* best-effort */ }
|
|
1054
|
+
delete item.prompt;
|
|
1055
|
+
delete item._promptFile;
|
|
1056
|
+
delete item._promptBytes;
|
|
1057
|
+
delete item._worktreeProtectionDeferred;
|
|
1058
|
+
// Clear the dedupe key so the item is free to re-dispatch on a later tick
|
|
1059
|
+
// once the conflict-repair WI clears the gate. No `completed` record is
|
|
1060
|
+
// pushed — this is not a completion, it is a deferral.
|
|
1061
|
+
if (item.meta?.dispatchKey) {
|
|
1062
|
+
dispatch.completed = Array.isArray(dispatch.completed)
|
|
1063
|
+
? dispatch.completed.filter(d => d.meta?.dispatchKey !== item.meta.dispatchKey)
|
|
1064
|
+
: [];
|
|
1065
|
+
}
|
|
1066
|
+
return dispatch;
|
|
1067
|
+
});
|
|
1068
|
+
if (item) {
|
|
1069
|
+
log('info', `Deferred dispatch ${id} back to pending (recoverable gate)${reason ? ': ' + reason : ''}`);
|
|
1070
|
+
}
|
|
1071
|
+
return !!item;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1024
1074
|
function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', resultSummary = '', opts = {}) {
|
|
1025
1075
|
const {
|
|
1026
1076
|
processWorkItemFailure = true,
|
|
@@ -1028,6 +1078,7 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
1028
1078
|
countAgentRetryFailure = true,
|
|
1029
1079
|
failureClass,
|
|
1030
1080
|
} = opts;
|
|
1081
|
+
const infrastructureRetry = opts.retryBudget === 'infrastructure';
|
|
1031
1082
|
const agentRetryable = normalizeRetryableDecision(opts.agentRetryable ?? opts.retryable);
|
|
1032
1083
|
let item = null;
|
|
1033
1084
|
let completedSourceWorkItem = false;
|
|
@@ -1057,6 +1108,14 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
1057
1108
|
if (reason) item.reason = reason;
|
|
1058
1109
|
if (resultSummary) item.resultSummary = resultSummary;
|
|
1059
1110
|
if (failureClass && result === DISPATCH_RESULT.ERROR) item.failureClass = failureClass;
|
|
1111
|
+
// W-ms9tcs3o01y03383 — record HOW the dispatch reached its terminal state so
|
|
1112
|
+
// a normal close, an immediate report recovery, and a genuinely stalled live
|
|
1113
|
+
// resume stay distinguishable on the completed record. Values come from
|
|
1114
|
+
// shared.DISPATCH_COMPLETION_PATH; unknown strings are ignored rather than
|
|
1115
|
+
// persisted so the field stays a closed vocabulary.
|
|
1116
|
+
if (opts.completionPath && COMPLETION_PATH_VALUES.has(opts.completionPath)) {
|
|
1117
|
+
item.completionPath = opts.completionPath;
|
|
1118
|
+
}
|
|
1060
1119
|
item.meta = item.meta && typeof item.meta === 'object' ? item.meta : {};
|
|
1061
1120
|
if (opts.completionReportPath && !item.meta.completionReportPath) {
|
|
1062
1121
|
item.meta.completionReportPath = opts.completionReportPath;
|
|
@@ -1195,14 +1254,17 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
1195
1254
|
}
|
|
1196
1255
|
} else {
|
|
1197
1256
|
let retries = (item.meta.item._retryCount || 0);
|
|
1257
|
+
if (infrastructureRetry) retries = item.meta.item._infrastructureRetryCount || 0;
|
|
1198
1258
|
try {
|
|
1199
1259
|
const wi = liveWi || readLiveWorkItem(item.meta);
|
|
1200
1260
|
if (wi) retries = wi._retryCount || 0;
|
|
1261
|
+
if (wi && infrastructureRetry) retries = wi._infrastructureRetryCount || 0;
|
|
1201
1262
|
} catch (e) { log('warn', 'read retry count: ' + e.message); }
|
|
1202
1263
|
const maxRetries = ENGINE_DEFAULTS.maxRetries;
|
|
1203
1264
|
const withinSafetyCap = retries < maxRetries;
|
|
1204
1265
|
if (retryableFailure && withinSafetyCap) {
|
|
1205
|
-
|
|
1266
|
+
const retryLabel = infrastructureRetry ? 'infrastructure recovery' : 'auto-retry';
|
|
1267
|
+
log('info', `Dispatch error for ${item.meta.item.id} — ${retryLabel} ${retries + 1}/${maxRetries}${failureClass ? ' [' + failureClass + ']' : ''}`);
|
|
1206
1268
|
lifecycle().updateWorkItemStatus(item.meta, WI_STATUS.PENDING, '');
|
|
1207
1269
|
// Remove this dispatch key from completed so dedupe doesn't block immediate redispatch.
|
|
1208
1270
|
if (item.meta?.dispatchKey) {
|
|
@@ -1220,7 +1282,8 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
1220
1282
|
mutateWorkItems(wiScope, items => {
|
|
1221
1283
|
const wi = items.find(i => i.id === item.meta.item.id);
|
|
1222
1284
|
if (wi && wi.status !== WI_STATUS.PAUSED && wi.status !== WI_STATUS.DONE && !wi.completedAt) {
|
|
1223
|
-
wi.
|
|
1285
|
+
if (infrastructureRetry) wi._infrastructureRetryCount = retries + 1;
|
|
1286
|
+
else wi._retryCount = retries + 1;
|
|
1224
1287
|
wi.status = WI_STATUS.PENDING;
|
|
1225
1288
|
wi._lastRetryReason = reason || '';
|
|
1226
1289
|
wi._lastRetryAt = ts();
|
|
@@ -1229,13 +1292,19 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
1229
1292
|
// when the previous attempt was bounced for MODEL_UNAVAILABLE
|
|
1230
1293
|
// (overloaded_error / 503). Empty string clears any stale
|
|
1231
1294
|
// value from an earlier failure cycle.
|
|
1232
|
-
|
|
1295
|
+
if (infrastructureRetry) {
|
|
1296
|
+
wi._lastInfrastructureFailureClass = failureClass || '';
|
|
1297
|
+
} else {
|
|
1298
|
+
wi._lastFailureClass = failureClass || '';
|
|
1299
|
+
}
|
|
1233
1300
|
// W-mpmwxn1j — Bump per-agent retry count so the next dispatch
|
|
1234
1301
|
// can reassign to a different eligible agent once the same
|
|
1235
1302
|
// agent hits the threshold. Skip when no agent is resolvable
|
|
1236
1303
|
// (anonymous failures shouldn't corrupt the map shape).
|
|
1237
1304
|
const failedAgent = item.agent || wi.dispatched_to;
|
|
1238
|
-
if (failedAgent && countAgentRetryFailure)
|
|
1305
|
+
if (!infrastructureRetry && failedAgent && countAgentRetryFailure) {
|
|
1306
|
+
shared.bumpAgentRetryCount(wi, failedAgent);
|
|
1307
|
+
}
|
|
1239
1308
|
delete wi.failReason;
|
|
1240
1309
|
delete wi.failedAt;
|
|
1241
1310
|
delete wi.dispatched_at;
|
|
@@ -1260,6 +1329,7 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
1260
1329
|
[FAILURE_CLASS.DEPENDENCY_MERGE_SETUP]: 'dependency merge setup failed',
|
|
1261
1330
|
[FAILURE_CLASS.MAX_TURNS]: 'reached max turn limit',
|
|
1262
1331
|
[FAILURE_CLASS.TIMEOUT]: 'timed out waiting for agent',
|
|
1332
|
+
[FAILURE_CLASS.ENGINE_RESTART]: 'engine restart lost dispatch process ownership',
|
|
1263
1333
|
[FAILURE_CLASS.SPAWN_ERROR]: 'agent process failed to start',
|
|
1264
1334
|
[FAILURE_CLASS.NETWORK_ERROR]: 'network or API error',
|
|
1265
1335
|
[FAILURE_CLASS.OUT_OF_CONTEXT]: 'context window exhausted',
|
|
@@ -1283,7 +1353,9 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
1283
1353
|
const classSuffix = failureClass ? ` [${failureClass.toUpperCase().replace(/-/g, '_')}]` : '';
|
|
1284
1354
|
const finalReason = !retryableFailure
|
|
1285
1355
|
? `Non-retryable failure: ${effectiveReason}${classSuffix}`
|
|
1286
|
-
: (
|
|
1356
|
+
: (infrastructureRetry
|
|
1357
|
+
? `Failed after ${maxRetries} infrastructure recovery attempts: ${effectiveReason}${classSuffix}`
|
|
1358
|
+
: (reason || `Failed after ${maxRetries} retries${classSuffix}`));
|
|
1287
1359
|
lifecycle().updateWorkItemStatus(item.meta, WI_STATUS.FAILED, finalReason);
|
|
1288
1360
|
// Surface blocked dependents in logs without creating failure inbox noise.
|
|
1289
1361
|
try {
|
|
@@ -1471,6 +1543,42 @@ function cancelPendingDispatchesForPr(prId) {
|
|
|
1471
1543
|
return cancelled;
|
|
1472
1544
|
}
|
|
1473
1545
|
|
|
1546
|
+
/**
|
|
1547
|
+
* Stop runtime processes owned by active dispatch entries. Pooled entries are
|
|
1548
|
+
* cancelled by the engine's tracked-process reconciliation on the next tick;
|
|
1549
|
+
* their PID belongs to a shared worker and must never be killed here.
|
|
1550
|
+
*/
|
|
1551
|
+
function stopDispatchProcesses(entries) {
|
|
1552
|
+
const processesToKill = [];
|
|
1553
|
+
for (const entry of entries || []) {
|
|
1554
|
+
if (!entry || entry.pooled) continue;
|
|
1555
|
+
const pidPath = shared.findDispatchPidFile(entry);
|
|
1556
|
+
if (!pidPath) continue;
|
|
1557
|
+
try {
|
|
1558
|
+
processesToKill.push({
|
|
1559
|
+
pid: shared.validatePid(fs.readFileSync(pidPath, 'utf8').trim()),
|
|
1560
|
+
observedAt: fs.statSync(pidPath).mtimeMs,
|
|
1561
|
+
});
|
|
1562
|
+
} catch { /* PID file may not exist */ }
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
const processSnapshot = processesToKill.length > 0 ? shared.listAllProcesses() : [];
|
|
1566
|
+
let stopped = 0;
|
|
1567
|
+
for (const processToKill of processesToKill) {
|
|
1568
|
+
try {
|
|
1569
|
+
const safePid = shared.validatePid(processToKill.pid);
|
|
1570
|
+
if (!shared.terminateProcess('engine.dispatch.cancel', { pid: safePid }, {
|
|
1571
|
+
graceMs: 5000,
|
|
1572
|
+
recordedAtMs: processToKill.observedAt,
|
|
1573
|
+
processes: processSnapshot,
|
|
1574
|
+
skewMs: 2000,
|
|
1575
|
+
})) continue;
|
|
1576
|
+
stopped++;
|
|
1577
|
+
} catch { /* may already be dead */ }
|
|
1578
|
+
}
|
|
1579
|
+
return stopped;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1474
1582
|
/**
|
|
1475
1583
|
* Remove dispatch entries matching a predicate from pending/active/completed.
|
|
1476
1584
|
* For matched active entries, kills the agent process and deletes its
|
|
@@ -1483,7 +1591,7 @@ function cancelPendingDispatchesForPr(prId) {
|
|
|
1483
1591
|
function cleanDispatchEntries(matchFn) {
|
|
1484
1592
|
const tmpDir = path.join(MINIONS_DIR, 'engine', 'tmp');
|
|
1485
1593
|
let removed = 0;
|
|
1486
|
-
const
|
|
1594
|
+
const activeEntriesToStop = [];
|
|
1487
1595
|
const filesToDelete = [];
|
|
1488
1596
|
const dispatchDirsToRemove = [];
|
|
1489
1597
|
try {
|
|
@@ -1499,13 +1607,7 @@ function cleanDispatchEntries(matchFn) {
|
|
|
1499
1607
|
// back to legacy flat layout for active dispatches that pre-date
|
|
1500
1608
|
// the per-dispatch-dir layout. shared.findDispatchPidFile honors
|
|
1501
1609
|
// the same resolution order so we agree on which PID to kill.
|
|
1502
|
-
|
|
1503
|
-
if (pidPath) {
|
|
1504
|
-
try {
|
|
1505
|
-
const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim());
|
|
1506
|
-
if (pid) pidsToKill.push(pid);
|
|
1507
|
-
} catch { /* PID file may not exist */ }
|
|
1508
|
-
}
|
|
1610
|
+
activeEntriesToStop.push(d);
|
|
1509
1611
|
if (d.tmpDir && shared.validateDispatchTmpDir(d.tmpDir)) {
|
|
1510
1612
|
dispatchDirsToRemove.push(d.tmpDir);
|
|
1511
1613
|
} else {
|
|
@@ -1523,18 +1625,7 @@ function cleanDispatchEntries(matchFn) {
|
|
|
1523
1625
|
return dispatch;
|
|
1524
1626
|
});
|
|
1525
1627
|
} catch { return 0; }
|
|
1526
|
-
|
|
1527
|
-
for (const pid of pidsToKill) {
|
|
1528
|
-
try {
|
|
1529
|
-
const safePid = shared.validatePid(pid);
|
|
1530
|
-
if (process.platform === 'win32') {
|
|
1531
|
-
const { execFileSync } = require('child_process');
|
|
1532
|
-
execFileSync('taskkill', ['/PID', String(safePid), '/F', '/T'], { stdio: 'pipe', timeout: 5000, windowsHide: true });
|
|
1533
|
-
} else {
|
|
1534
|
-
shared.killGracefully({ pid: safePid }, 5000);
|
|
1535
|
-
}
|
|
1536
|
-
} catch { /* may already be dead */ }
|
|
1537
|
-
}
|
|
1628
|
+
stopDispatchProcesses(activeEntriesToStop);
|
|
1538
1629
|
for (const fp of filesToDelete) {
|
|
1539
1630
|
try { fs.unlinkSync(fp); } catch { /* may not exist */ }
|
|
1540
1631
|
}
|
|
@@ -1573,6 +1664,7 @@ module.exports = {
|
|
|
1573
1664
|
getPrDispatchDedupeKey,
|
|
1574
1665
|
isRetryableFailureReason,
|
|
1575
1666
|
deferDispatchForWorktreeProtection,
|
|
1667
|
+
deferDispatchToPending,
|
|
1576
1668
|
completeDispatch,
|
|
1577
1669
|
writeInboxAlert,
|
|
1578
1670
|
updateAgentStatus,
|
|
@@ -1580,6 +1672,7 @@ module.exports = {
|
|
|
1580
1672
|
pruneStalePrDispatches,
|
|
1581
1673
|
pruneStalePrDispatchesAsync, // W-mr9jo2n2000667ad — scope-overlap-verified variant; engine.js tick should await this
|
|
1582
1674
|
cancelPendingDispatchesForPr,
|
|
1675
|
+
stopDispatchProcesses,
|
|
1583
1676
|
cleanDispatchEntries,
|
|
1584
1677
|
cancelPendingWorkItems,
|
|
1585
1678
|
// exported for testing
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// engine/orchestration/failed-scheduled-cleanup.js — W-msckn1lp002xdf6b
|
|
2
|
+
//
|
|
3
|
+
// Conservative, low-frequency (daily) retention for FAILED scheduled work
|
|
4
|
+
// items so machine-generated scheduled-run failures (hourly review/fetch
|
|
5
|
+
// sweeps, etc.) do not accumulate indefinitely in the work-item store.
|
|
6
|
+
//
|
|
7
|
+
// Contract (see docs/auto-discovery.md → "Scheduled-failure retention"):
|
|
8
|
+
// - Targets ONLY provably machine-generated scheduled runs, identified by
|
|
9
|
+
// structured provenance (`createdBy === 'scheduler'` + `_scheduleId`) or the
|
|
10
|
+
// canonical legacy `sched-<scheduleId>-<ts>` id prefix. Never by title/prose.
|
|
11
|
+
// - Groups failures by their originating schedule. The NEWEST failed
|
|
12
|
+
// occurrence per schedule is retained long enough to diagnose the current
|
|
13
|
+
// failure; older SUPERSEDED failures are pruned once they age past the
|
|
14
|
+
// diagnostic window (24h). A later successful run lets the newest failure be
|
|
15
|
+
// pruned too, once that window has elapsed. An absolute threshold prunes
|
|
16
|
+
// stale failures regardless.
|
|
17
|
+
// - Non-scheduled / human-authored failed items are NEVER touched.
|
|
18
|
+
// - Fails open: ambiguous provenance, lineage links, or an active dispatch
|
|
19
|
+
// reference cause the item to be skipped, not deleted.
|
|
20
|
+
//
|
|
21
|
+
// The policy is intentionally a documented shared default rather than an
|
|
22
|
+
// `engine.*` knob — there is nothing an operator needs to tune, and adding a
|
|
23
|
+
// config flag would require Settings UI + CLAUDE.md parity for no benefit.
|
|
24
|
+
|
|
25
|
+
const shared = require('../core/shared');
|
|
26
|
+
const { WI_STATUS, DONE_STATUSES, log } = shared;
|
|
27
|
+
|
|
28
|
+
// Retain the newest failure and any failure younger than this so the current
|
|
29
|
+
// failure stays diagnosable; superseded failures older than this are pruned.
|
|
30
|
+
const DIAGNOSTIC_RETENTION_MS = 24 * 60 * 60 * 1000; // 24h
|
|
31
|
+
// Absolute stale threshold — even the newest failure is pruned past this so a
|
|
32
|
+
// long-abandoned schedule does not pin a failure forever.
|
|
33
|
+
const ABSOLUTE_RETENTION_MS = 7 * 24 * 60 * 60 * 1000; // 7d
|
|
34
|
+
// Low-frequency cadence: the sweep itself runs at most once per this window,
|
|
35
|
+
// even though runCleanup fires every ~10 minutes.
|
|
36
|
+
const PRUNE_INTERVAL_MS = 24 * 60 * 60 * 1000; // daily
|
|
37
|
+
// Persisted last-run marker key in the engine small-state store.
|
|
38
|
+
const LAST_RUN_KEY = 'lastFailedScheduledPruneAt';
|
|
39
|
+
|
|
40
|
+
// ─── Provenance ─────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
// True iff the item is a provably machine-generated scheduled run. Structured
|
|
43
|
+
// provenance wins; the `sched-` id prefix is the documented legacy fallback for
|
|
44
|
+
// items created before `createdBy`/`_scheduleId` were consistently stamped.
|
|
45
|
+
function isScheduledProvenance(item) {
|
|
46
|
+
if (!item || typeof item !== 'object') return false;
|
|
47
|
+
if (item.createdBy === 'scheduler' && item._scheduleId) return true;
|
|
48
|
+
if (typeof item.id === 'string' && /^sched-.+-\d+$/.test(item.id)) return true;
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The originating schedule id for grouping. Prefers structured `_scheduleId`;
|
|
53
|
+
// falls back to parsing the canonical `sched-<scheduleId>-<timestamp>` id.
|
|
54
|
+
function scheduleKey(item) {
|
|
55
|
+
if (item && item._scheduleId) return String(item._scheduleId);
|
|
56
|
+
const m = /^sched-(.+)-\d+$/.exec(String(item && item.id || ''));
|
|
57
|
+
return m ? m[1] : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// An item wired into a plan/PRD/decomposition/followup lineage must not be
|
|
61
|
+
// pruned here — its lifecycle is owned elsewhere. Scheduled sweep items carry
|
|
62
|
+
// none of these, so a scheduled-provenance item that DOES is treated as
|
|
63
|
+
// ambiguous and skipped (fail-safe).
|
|
64
|
+
function hasLineage(item) {
|
|
65
|
+
return !!(item && (item.parent_id || item.parentId || item._prdItemId || item.prdItemId));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function _parseMs(v) {
|
|
69
|
+
if (v == null) return NaN;
|
|
70
|
+
if (typeof v === 'number') return Number.isFinite(v) ? v : NaN;
|
|
71
|
+
const ms = Date.parse(v);
|
|
72
|
+
return Number.isFinite(ms) ? ms : NaN;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Best-effort failure timestamp — `failedAt` is stamped when a WI fails.
|
|
76
|
+
function failureTimestampMs(item) {
|
|
77
|
+
for (const cand of [item.failedAt, item.updatedAt, item.completedAt, item.dispatched_at, item.created]) {
|
|
78
|
+
const ms = _parseMs(cand);
|
|
79
|
+
if (Number.isFinite(ms)) return ms;
|
|
80
|
+
}
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Best-effort success timestamp — `completedAt` is stamped when a WI completes.
|
|
85
|
+
function successTimestampMs(item) {
|
|
86
|
+
for (const cand of [item.completedAt, item.completed_at, item.updatedAt, item.dispatched_at, item.created]) {
|
|
87
|
+
const ms = _parseMs(cand);
|
|
88
|
+
if (Number.isFinite(ms)) return ms;
|
|
89
|
+
}
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── Pure retention planner (unit-tested directly) ──────────────────────────
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Decide which failed scheduled work items to prune.
|
|
97
|
+
*
|
|
98
|
+
* @param {Array<object>} items Flat list of work items across all scopes. Each
|
|
99
|
+
* item may carry a `_scope` hint (defaults to 'central').
|
|
100
|
+
* @param {number} now Epoch ms "now".
|
|
101
|
+
* @param {object} [opts]
|
|
102
|
+
* @param {Set<string>} [opts.protectedIds] WI ids referenced by a live
|
|
103
|
+
* dispatch — never pruned.
|
|
104
|
+
* @param {number} [opts.diagnosticRetentionMs]
|
|
105
|
+
* @param {number} [opts.absoluteRetentionMs]
|
|
106
|
+
* @returns {{ toPrune: Array<{id,scope,scheduleKey}>, groupCount: number }}
|
|
107
|
+
*/
|
|
108
|
+
function planFailedScheduledPrune(items, now = Date.now(), opts = {}) {
|
|
109
|
+
const diagnosticMs = Number.isFinite(opts.diagnosticRetentionMs) ? opts.diagnosticRetentionMs : DIAGNOSTIC_RETENTION_MS;
|
|
110
|
+
const absoluteMs = Number.isFinite(opts.absoluteRetentionMs) ? opts.absoluteRetentionMs : ABSOLUTE_RETENTION_MS;
|
|
111
|
+
const protectedIds = opts.protectedIds instanceof Set ? opts.protectedIds : new Set();
|
|
112
|
+
|
|
113
|
+
const groups = new Map(); // scheduleKey -> { failures: [], latestSuccessMs }
|
|
114
|
+
for (const item of Array.isArray(items) ? items : []) {
|
|
115
|
+
if (!isScheduledProvenance(item)) continue;
|
|
116
|
+
const key = scheduleKey(item);
|
|
117
|
+
if (!key) continue;
|
|
118
|
+
let g = groups.get(key);
|
|
119
|
+
if (!g) { g = { failures: [], latestSuccessMs: 0 }; groups.set(key, g); }
|
|
120
|
+
if (item.status === WI_STATUS.FAILED) {
|
|
121
|
+
g.failures.push(item);
|
|
122
|
+
} else if (DONE_STATUSES.has(item.status)) {
|
|
123
|
+
const ms = successTimestampMs(item);
|
|
124
|
+
if (ms > g.latestSuccessMs) g.latestSuccessMs = ms;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const toPrune = [];
|
|
129
|
+
for (const [key, g] of groups) {
|
|
130
|
+
if (g.failures.length === 0) continue;
|
|
131
|
+
// Newest failure first so index 0 is the diagnostic keeper.
|
|
132
|
+
const failures = g.failures.slice().sort((a, b) => failureTimestampMs(b) - failureTimestampMs(a));
|
|
133
|
+
for (let i = 0; i < failures.length; i++) {
|
|
134
|
+
const f = failures[i];
|
|
135
|
+
if (protectedIds.has(String(f.id))) continue; // live dispatch reference
|
|
136
|
+
if (hasLineage(f)) continue; // plan/PRD/followup lineage
|
|
137
|
+
const age = now - failureTimestampMs(f);
|
|
138
|
+
const isNewest = i === 0;
|
|
139
|
+
let prune = false;
|
|
140
|
+
if (isNewest) {
|
|
141
|
+
// Keep the current failure to diagnose it — unless a later success has
|
|
142
|
+
// superseded it (and the diagnostic window elapsed), or it is stale
|
|
143
|
+
// past the absolute threshold.
|
|
144
|
+
const supersededBySuccess = g.latestSuccessMs > failureTimestampMs(f);
|
|
145
|
+
if (age > absoluteMs) prune = true;
|
|
146
|
+
else if (supersededBySuccess && age > diagnosticMs) prune = true;
|
|
147
|
+
} else {
|
|
148
|
+
// Superseded by a newer failure — prune once past the diagnostic window.
|
|
149
|
+
if (age > diagnosticMs) prune = true;
|
|
150
|
+
}
|
|
151
|
+
if (prune) toPrune.push({ id: String(f.id), scope: f._scope || 'central', scheduleKey: key });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return { toPrune, groupCount: groups.size };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─── Orchestration entrypoint (wired into runCleanup) ───────────────────────
|
|
158
|
+
|
|
159
|
+
// Collect WI ids referenced by a live (pending/active/review) dispatch so we
|
|
160
|
+
// never delete a work item something is actively working. Fails open.
|
|
161
|
+
function _collectProtectedWiIds() {
|
|
162
|
+
const ids = new Set();
|
|
163
|
+
try {
|
|
164
|
+
const dispatch = require('../core/queries').getDispatch();
|
|
165
|
+
for (const seg of [dispatch.pending, dispatch.active, dispatch.review]) {
|
|
166
|
+
for (const e of Array.isArray(seg) ? seg : []) {
|
|
167
|
+
const wi = e && e.meta && e.meta.item && e.meta.item.id;
|
|
168
|
+
if (wi) ids.add(String(wi));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch { /* fail open — no protections known */ }
|
|
172
|
+
return ids;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Daily sweep that prunes superseded failed scheduled work items. Safe to call
|
|
177
|
+
* every cleanup tick — the cadence gate (persisted last-run marker) skips work
|
|
178
|
+
* until PRUNE_INTERVAL_MS has elapsed. Concurrency-safe (the gate is claimed
|
|
179
|
+
* inside a single small-state transaction) and idempotent (the deletion mutator
|
|
180
|
+
* re-validates provenance/status against fresh SQL rows).
|
|
181
|
+
*
|
|
182
|
+
* @returns {{ skipped:boolean, reason?:string, pruned:number, bySchedule?:object }}
|
|
183
|
+
*/
|
|
184
|
+
function pruneSupersededFailedScheduledItems(config, opts = {}) {
|
|
185
|
+
const now = Number.isFinite(opts.now) ? opts.now : Date.now();
|
|
186
|
+
const force = opts.force === true;
|
|
187
|
+
const intervalMs = Number.isFinite(opts.intervalMs) ? opts.intervalMs : PRUNE_INTERVAL_MS;
|
|
188
|
+
|
|
189
|
+
// Cadence gate — claim the run atomically so overlapping ticks / a restart
|
|
190
|
+
// race cannot double-run the sweep in the same day.
|
|
191
|
+
if (!force) {
|
|
192
|
+
let claimed = false;
|
|
193
|
+
try {
|
|
194
|
+
shared.mutateEngineState((state) => {
|
|
195
|
+
const last = Number(state[LAST_RUN_KEY]) || 0;
|
|
196
|
+
if (last && (now - last) < intervalMs) return state; // not due yet
|
|
197
|
+
state[LAST_RUN_KEY] = now;
|
|
198
|
+
claimed = true;
|
|
199
|
+
return state;
|
|
200
|
+
});
|
|
201
|
+
} catch (e) {
|
|
202
|
+
log('warn', `failed-scheduled prune: cadence claim failed — ${e.message}`);
|
|
203
|
+
return { skipped: true, reason: 'cadence-claim-error', pruned: 0 };
|
|
204
|
+
}
|
|
205
|
+
if (!claimed) return { skipped: true, reason: 'cadence', pruned: 0 };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Gather scheduled items across every scope (central + each project).
|
|
209
|
+
const scopes = ['central', ...shared.getProjects(config || {})];
|
|
210
|
+
const allItems = [];
|
|
211
|
+
for (const scope of scopes) {
|
|
212
|
+
let items;
|
|
213
|
+
try { items = shared.readWorkItems(scope); } catch { continue; }
|
|
214
|
+
for (const it of Array.isArray(items) ? items : []) {
|
|
215
|
+
if (it && typeof it === 'object') { it._scope = scope; allItems.push(it); }
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const protectedIds = _collectProtectedWiIds();
|
|
220
|
+
const { toPrune } = planFailedScheduledPrune(allItems, now, { protectedIds });
|
|
221
|
+
if (toPrune.length === 0) return { skipped: false, pruned: 0, bySchedule: {} };
|
|
222
|
+
|
|
223
|
+
const idsByScope = new Map();
|
|
224
|
+
for (const p of toPrune) {
|
|
225
|
+
if (!idsByScope.has(p.scope)) idsByScope.set(p.scope, new Set());
|
|
226
|
+
idsByScope.get(p.scope).add(p.id);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let pruned = 0;
|
|
230
|
+
const bySchedule = {};
|
|
231
|
+
const idToSchedule = new Map(toPrune.map(p => [p.id, p.scheduleKey]));
|
|
232
|
+
for (const [scope, ids] of idsByScope) {
|
|
233
|
+
try {
|
|
234
|
+
shared.mutateWorkItems(scope, (items) => {
|
|
235
|
+
if (!Array.isArray(items)) return items;
|
|
236
|
+
return items.filter((w) => {
|
|
237
|
+
if (!w || !ids.has(String(w.id))) return true;
|
|
238
|
+
// Re-validate against the fresh row — status/provenance may have
|
|
239
|
+
// changed since planning (idempotency + race safety).
|
|
240
|
+
if (w.status !== WI_STATUS.FAILED) return true;
|
|
241
|
+
if (!isScheduledProvenance(w)) return true;
|
|
242
|
+
if (hasLineage(w)) return true;
|
|
243
|
+
const key = idToSchedule.get(String(w.id)) || scheduleKey(w) || 'unknown';
|
|
244
|
+
bySchedule[key] = (bySchedule[key] || 0) + 1;
|
|
245
|
+
pruned++;
|
|
246
|
+
return false; // delete
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
} catch (e) {
|
|
250
|
+
log('warn', `failed-scheduled prune (scope=${scope}): ${e.message}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (pruned > 0) {
|
|
255
|
+
const summary = Object.entries(bySchedule).map(([k, n]) => `${k}:${n}`).join(', ');
|
|
256
|
+
log('info', `cleanup: pruned ${pruned} superseded failed scheduled work item(s) across ${Object.keys(bySchedule).length} schedule(s) — ${summary}`);
|
|
257
|
+
}
|
|
258
|
+
return { skipped: false, pruned, bySchedule };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = {
|
|
262
|
+
pruneSupersededFailedScheduledItems,
|
|
263
|
+
planFailedScheduledPrune,
|
|
264
|
+
// Exported for unit tests / reuse:
|
|
265
|
+
isScheduledProvenance,
|
|
266
|
+
scheduleKey,
|
|
267
|
+
hasLineage,
|
|
268
|
+
failureTimestampMs,
|
|
269
|
+
successTimestampMs,
|
|
270
|
+
DIAGNOSTIC_RETENTION_MS,
|
|
271
|
+
ABSOLUTE_RETENTION_MS,
|
|
272
|
+
PRUNE_INTERVAL_MS,
|
|
273
|
+
LAST_RUN_KEY,
|
|
274
|
+
};
|