@yemi33/minions 0.1.2379 → 0.1.2381
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/minions.js +19 -9
- package/dashboard/js/refresh.js +3 -4
- package/dashboard/js/render-other.js +43 -23
- package/dashboard/js/render-prd.js +1 -1
- package/dashboard/js/render-work-items.js +13 -6
- package/dashboard/js/settings.js +40 -17
- package/dashboard.js +435 -768
- package/docs/architecture-review-2026-07-09.md +2 -4
- package/docs/auto-discovery.md +36 -49
- package/docs/blog-first-successful-dispatch.md +1 -1
- package/docs/branch-derivation.md +2 -2
- package/docs/command-center.md +1 -1
- package/docs/completion-reports.md +14 -4
- package/docs/constellation-bridge.md +59 -10
- package/docs/constellation-style-telemetry.md +6 -6
- package/docs/cooldown-merge-semantics.md +4 -0
- package/docs/copilot-cli-schema.md +3 -3
- package/docs/cross-repo-plans.md +17 -17
- package/docs/deprecated.json +2 -2
- package/docs/design-state-storage.md +1 -1
- package/docs/documentation-audit-2026-07-09.md +2 -2
- package/docs/engine-restart.md +20 -8
- package/docs/harness-mode.md +1 -1
- package/docs/live-checkout-mode.md +45 -26
- package/docs/managed-spawn.md +4 -4
- package/docs/onboarding.md +1 -2
- package/docs/pr-comment-followup.md +3 -3
- package/docs/pr-review-fix-loop.md +1 -1
- package/docs/proposals/repo-pool-for-live-checkout.md +1 -2
- package/docs/qa-runbook-lifecycle.md +4 -4
- package/docs/qa-runbooks.md +2 -2
- package/docs/rfc-completion-json.md +4 -1
- package/docs/runtime-adapters.md +1 -1
- package/docs/self-improvement.md +4 -5
- package/docs/shared-lifecycle-module-map.md +3 -1
- package/docs/slim-ux/architecture-suggestions.md +5 -6
- package/docs/slim-ux/concepts.md +23 -25
- package/docs/watches.md +7 -7
- package/docs/workspace-manifests.md +1 -1
- package/docs/worktree-lifecycle.md +1 -1
- package/engine/abandoned-pr-reconciliation.js +4 -5
- package/engine/ado-status.js +5 -5
- package/engine/ado.js +20 -25
- package/engine/agent-worker-pool.js +58 -1
- package/engine/bridge.js +260 -5
- package/engine/cleanup.js +48 -131
- package/engine/cli.js +125 -83
- package/engine/cooldown.js +9 -16
- package/engine/db/index.js +22 -9
- package/engine/db/migrations/009-qa.js +1 -1
- package/engine/db/migrations/020-qa-session-scopes.js +23 -0
- package/engine/db/migrations/021-archived-work-items.js +72 -0
- package/engine/db/migrations/022-global-cc-session.js +31 -0
- package/engine/db/migrations/023-engine-state.js +30 -0
- package/engine/db/migrations/024-prd-ghost-collisions.js +53 -0
- package/engine/db/migrations/025-malformed-work-item-phantoms.js +33 -0
- package/engine/dispatch-store.js +2 -7
- package/engine/dispatch.js +39 -44
- package/engine/github.js +20 -27
- package/engine/lifecycle.js +279 -354
- package/engine/live-checkout.js +193 -149
- package/engine/llm.js +1 -1
- package/engine/logs-store.js +2 -2
- package/engine/managed-spawn.js +2 -23
- package/engine/meeting.js +6 -6
- package/engine/metrics-store.js +2 -2
- package/engine/note-link-backfill.js +6 -11
- package/engine/pipeline.js +18 -36
- package/engine/playbook.js +13 -16
- package/engine/prd-store.js +73 -54
- package/engine/preflight.js +2 -5
- package/engine/projects.js +15 -62
- package/engine/pull-requests-store.js +0 -17
- package/engine/qa-runbooks.js +2 -2
- package/engine/qa-runs.js +1 -8
- package/engine/qa-sessions.js +41 -64
- package/engine/queries.js +120 -219
- package/engine/routing.js +4 -6
- package/engine/scheduler.js +0 -4
- package/engine/shared-branch-pr-reconcile.js +2 -3
- package/engine/shared.js +268 -699
- package/engine/small-state-store.js +89 -10
- package/engine/state-operations.js +16 -4
- package/engine/stdio-timestamps.js +1 -1
- package/engine/timeout.js +5 -12
- package/engine/watch-actions.js +20 -22
- package/engine/watches-store.js +1 -1
- package/engine/watches.js +6 -10
- package/engine/work-item-validation.js +52 -0
- package/engine/work-items-store.js +127 -29
- package/engine/worktree-gc.js +2 -2
- package/engine/worktree-pool.js +8 -18
- package/engine.js +197 -358
- package/minions.js +2 -2
- package/package.json +1 -1
- package/playbooks/plan-to-prd.md +2 -2
- package/playbooks/shared-rules.md +3 -3
- package/playbooks/templates/followup-dispatch.md +1 -1
- package/playbooks/verify.md +1 -1
- package/prompts/cc-system.md +9 -9
package/engine.js
CHANGED
|
@@ -57,7 +57,7 @@ const IDENTITY_DIR = path.join(MINIONS_DIR, 'identity');
|
|
|
57
57
|
|
|
58
58
|
// Re-export from queries for internal use (avoid changing every call site)
|
|
59
59
|
const { CONFIG_PATH, NOTES_PATH, AGENTS_DIR, ENGINE_DIR, CONTROL_PATH,
|
|
60
|
-
|
|
60
|
+
INBOX_DIR, KNOWLEDGE_DIR, PLANS_DIR, PRD_DIR } = queries;
|
|
61
61
|
|
|
62
62
|
// ─── Multi-Project Support ──────────────────────────────────────────────────
|
|
63
63
|
// Config can have either:
|
|
@@ -102,7 +102,7 @@ function validateConfig(config) {
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
const { getProjects, projectRoot, projectStateDir,
|
|
105
|
+
const { getProjects, projectRoot, projectStateDir, getAdoOrgBase, sanitizeBranch, parseSkillFrontmatter, safeReadDir,
|
|
106
106
|
logTs, dateStamp, log } = shared;
|
|
107
107
|
|
|
108
108
|
// ─── Utilities ──────────────────────────────────────────────────────────────
|
|
@@ -110,12 +110,13 @@ const { getProjects, projectRoot, projectStateDir, projectWorkItemsPath, project
|
|
|
110
110
|
const safeJson = shared.safeJson;
|
|
111
111
|
const safeJsonArr = shared.safeJsonArr;
|
|
112
112
|
const safeJsonObj = shared.safeJsonObj;
|
|
113
|
-
const safeJsonNoRestore = shared.safeJsonNoRestore;
|
|
114
113
|
const safeRead = shared.safeRead;
|
|
115
114
|
const safeWrite = shared.safeWrite;
|
|
116
115
|
const safeUnlink = shared.safeUnlink;
|
|
117
116
|
const mutateJsonFileLocked = shared.mutateJsonFileLocked;
|
|
118
117
|
const mutateControl = shared.mutateControl;
|
|
118
|
+
const readWorkItems = shared.readWorkItems;
|
|
119
|
+
const readPullRequests = shared.readPullRequests;
|
|
119
120
|
const mutateWorkItems = shared.mutateWorkItems;
|
|
120
121
|
const mutatePullRequests = shared.mutatePullRequests;
|
|
121
122
|
const withFileLock = shared.withFileLock;
|
|
@@ -175,7 +176,7 @@ const ghToken = require('./engine/gh-token');
|
|
|
175
176
|
const { runPostCompletionHooks, updateWorkItemStatus, syncPrdItemStatus, reconcilePrdStatuses, handlePostMerge, checkPlanCompletion,
|
|
176
177
|
syncPrsFromOutput, updatePrAfterReview, updatePrAfterFix, checkForLearnings, extractSkillsFromOutput,
|
|
177
178
|
updateAgentHistory, updateMetrics, createReviewFeedbackForAuthor, parseAgentOutput, syncPrdFromPrs, persistVerifyPrsToPrd,
|
|
178
|
-
isItemCompleted, classifyFailure: classifyFailureFallback, diagnoseEmptyOutput, processPendingRebases,
|
|
179
|
+
isItemCompleted, classifyFailure: classifyFailureFallback, diagnoseEmptyOutput, processPendingRebases, resolveWorkItemScope,
|
|
179
180
|
mergeArtifactNotes, promoteCompletionArtifacts, pruneScopeMismatchDuplicatePrs, collapseAllDuplicatePrRecords,
|
|
180
181
|
recordWorktreeHeldPause, clearWorktreeHeldPause } = require('./engine/lifecycle');
|
|
181
182
|
|
|
@@ -701,8 +702,7 @@ function resolveDependencyBranches(depIds, sourcePlan, project, config) {
|
|
|
701
702
|
|
|
702
703
|
// Find PR branches for each dependency work item
|
|
703
704
|
for (const p of projects) {
|
|
704
|
-
const
|
|
705
|
-
const prs = safeJsonArr(prPath);
|
|
705
|
+
const prs = readPullRequests(p);
|
|
706
706
|
for (const pr of prs) {
|
|
707
707
|
if (!pr.branch || pr.status !== PR_STATUS.ACTIVE) continue;
|
|
708
708
|
const linked = (pr.prdItems || []).some(id =>
|
|
@@ -2159,7 +2159,7 @@ function _tryAutoResolveLiveCheckoutWorktreeConflict({ conflictPath, gitRoot, en
|
|
|
2159
2159
|
async function spawnAgent(dispatchItem, config) {
|
|
2160
2160
|
const { id, agent: agentId, type, meta } = dispatchItem;
|
|
2161
2161
|
// Resolve prompt — prefers sidecar file when dispatchItem._promptFile is set
|
|
2162
|
-
// (large prompts are written to engine/contexts/<id>.md to keep dispatch
|
|
2162
|
+
// (large prompts are written to engine/contexts/<id>.md to keep dispatch rows
|
|
2163
2163
|
// small — see shared.sidecarDispatchPrompt / #1167).
|
|
2164
2164
|
let taskPrompt = shared.resolveDispatchPrompt(dispatchItem);
|
|
2165
2165
|
const claudeConfig = config.claude || {};
|
|
@@ -2415,12 +2415,8 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2415
2415
|
// dir via shared.dispatchPidCandidates / findDispatchPidFile. Best-effort —
|
|
2416
2416
|
// backward-compat fallbacks still scan dispatch-<safeId>-* dirs by id.
|
|
2417
2417
|
//
|
|
2418
|
-
// W-mq9b7lor (H1):
|
|
2419
|
-
//
|
|
2420
|
-
// mutateDispatch call because engine/dispatch.js#mutateDispatch regenerates
|
|
2421
|
-
// dispatch.json from SQL after every successful mutation (see
|
|
2422
|
-
// engine/dispatch.js:98-101). Without the SQL write, the tmpDir pointer is
|
|
2423
|
-
// lost and orphan-reap/kill/cleanup paths can't find the prompt directory.
|
|
2418
|
+
// W-mq9b7lor (H1): persist through mutateDispatch so the tmpDir pointer
|
|
2419
|
+
// survives and orphan-reap/kill/cleanup paths can find the prompt directory.
|
|
2424
2420
|
try {
|
|
2425
2421
|
dispatchItem.tmpDir = dispatchTmpDir;
|
|
2426
2422
|
mutateDispatch((dispatch) => {
|
|
@@ -2519,7 +2515,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2519
2515
|
if ((meta?.source === 'pr' || meta?.source === 'pr-human-feedback') && meta?.pr) {
|
|
2520
2516
|
try {
|
|
2521
2517
|
const pauseProject = meta.project || project;
|
|
2522
|
-
mutatePullRequests(
|
|
2518
|
+
mutatePullRequests(pauseProject, (prs) => {
|
|
2523
2519
|
if (!Array.isArray(prs)) return prs;
|
|
2524
2520
|
const target = shared.findPrRecord(prs, meta.pr, pauseProject);
|
|
2525
2521
|
if (!target) return prs;
|
|
@@ -2581,7 +2577,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2581
2577
|
// validation WI auto-created by the post-completion hook that carries a PR
|
|
2582
2578
|
// reference but no explicit branch), resolve the target branch from the work
|
|
2583
2579
|
// item's PR reference so prepareLiveCheckout can check out the correct PR
|
|
2584
|
-
// source branch. resolveWorkItemPrRecord loads
|
|
2580
|
+
// source branch. resolveWorkItemPrRecord loads tracked PR state and finds the
|
|
2585
2581
|
// PR record; resolvePrBranch extracts the source branch from it. Both helpers
|
|
2586
2582
|
// are hoisted function declarations so the order-of-definition is safe.
|
|
2587
2583
|
if (liveMode && !branchName && !READ_ONLY_ROOT_TASK_TYPES.has(type)) {
|
|
@@ -2682,9 +2678,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2682
2678
|
wiId: _wiIdForAlert,
|
|
2683
2679
|
log: (msg, lvl) => log(lvl || 'info', msg),
|
|
2684
2680
|
clearDirtyStamp: () => {
|
|
2685
|
-
const
|
|
2686
|
-
if (
|
|
2687
|
-
|
|
2681
|
+
const _wiScope = resolveWorkItemScope(dispatchItem.meta);
|
|
2682
|
+
if (_wiScope && dispatchItem.meta?.item?.id) {
|
|
2683
|
+
mutateWorkItems(_wiScope, (data) => {
|
|
2688
2684
|
if (!Array.isArray(data)) return data;
|
|
2689
2685
|
const wi = data.find(i => i && i.id === dispatchItem.meta.item.id);
|
|
2690
2686
|
if (wi && wi._pendingReason === 'live_checkout_dirty') delete wi._pendingReason;
|
|
@@ -2715,7 +2711,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2715
2711
|
// auto-stash is disabled, or because `applyLiveCheckoutAutoStash` itself
|
|
2716
2712
|
// reported `outcome:'unchanged'` (the `git stash push` call failed) — and
|
|
2717
2713
|
// the fleet/project auto-reset policy resolves true, NOW perform the
|
|
2718
|
-
// destructive `
|
|
2714
|
+
// destructive `reset --hard HEAD` + `clean -fd` WIP cleanup as a fallback, reusing
|
|
2719
2715
|
// prepareLiveCheckout's existing reset logic by re-invoking it with an
|
|
2720
2716
|
// explicit `autoReset: true` (bypassing config resolution, which we've
|
|
2721
2717
|
// already evaluated). This only fires when the first prepareLiveCheckout
|
|
@@ -2765,6 +2761,27 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2765
2761
|
if (_liveResult && _liveResult.ok === false && _liveResult.reason === 'dirty') {
|
|
2766
2762
|
const _dirtyFiles = Array.isArray(_liveResult.dirtyFiles) ? _liveResult.dirtyFiles : [];
|
|
2767
2763
|
const _branchInfo = typeof _liveResult.branchInfo === 'string' ? _liveResult.branchInfo : '';
|
|
2764
|
+
const _autoResetAttempt = _liveResult.autoResetAttempt;
|
|
2765
|
+
const _resetAuditSlug = typeof _autoResetAttempt?.auditSlug === 'string'
|
|
2766
|
+
? _autoResetAttempt.auditSlug
|
|
2767
|
+
: `live-checkout-autoreset-${id}`;
|
|
2768
|
+
let _cleanAttemptStatus = 'not run';
|
|
2769
|
+
let _statusCheckStatus = 'failed';
|
|
2770
|
+
if (_autoResetAttempt?.cleanAttempted) {
|
|
2771
|
+
_cleanAttemptStatus = _autoResetAttempt.cleanSucceeded ? 'succeeded' : 'failed or partially applied';
|
|
2772
|
+
}
|
|
2773
|
+
if (_autoResetAttempt?.recheckSucceeded) {
|
|
2774
|
+
_statusCheckStatus = `${_dirtyFiles.length} dirty path(s) remain`;
|
|
2775
|
+
}
|
|
2776
|
+
const _resetAttemptSummary = _autoResetAttempt
|
|
2777
|
+
? [
|
|
2778
|
+
'Automatic reset cleanup was attempted before this refusal:',
|
|
2779
|
+
`- \`git reset --hard HEAD\`: ${_autoResetAttempt.resetSucceeded ? 'succeeded' : 'failed'}`,
|
|
2780
|
+
`- \`git clean -fd\`: ${_cleanAttemptStatus}`,
|
|
2781
|
+
`- post-cleanup status check: ${_statusCheckStatus}`,
|
|
2782
|
+
`See the \`${_resetAuditSlug}\` inbox note for the full before/after audit.`,
|
|
2783
|
+
].join('\n')
|
|
2784
|
+
: 'No automatic reset/clean recovery was attempted before this refusal.';
|
|
2768
2785
|
// #329 + PL-live-checkout-reliability-hardening: two-strike dirty guard.
|
|
2769
2786
|
// First dirty failure is retried once (a transient or engine-owned dirty
|
|
2770
2787
|
// state — e.g. an agent's own leftover WIP, which dispatch-end restore now
|
|
@@ -2804,7 +2821,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2804
2821
|
`**Dispatch:** ${id}`,
|
|
2805
2822
|
...(_branchInfo ? ['', `**Current checkout:** \`${_branchInfo}\``] : []),
|
|
2806
2823
|
'',
|
|
2807
|
-
`The engine refused to spawn the agent because \`${cwd}\` has uncommitted changes. Live-checkout mode runs in your project directly
|
|
2824
|
+
`The engine refused to spawn the agent because \`${cwd}\` has uncommitted changes. Live-checkout mode runs in your project directly.`,
|
|
2808
2825
|
'',
|
|
2809
2826
|
'## Dirty files (`git status --porcelain=v1 -b`)',
|
|
2810
2827
|
'',
|
|
@@ -2812,6 +2829,8 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2812
2829
|
...(_dirtyFiles.length > 0 ? _liveCheckout.capDirtyFileLines(_dirtyFiles) : ['(none reported)']),
|
|
2813
2830
|
'```',
|
|
2814
2831
|
'',
|
|
2832
|
+
_resetAttemptSummary,
|
|
2833
|
+
'',
|
|
2815
2834
|
_autoCleanupEnabled
|
|
2816
2835
|
? 'Auto-cleanup (`liveCheckoutAutoReset`/`liveCheckoutAutoStash`) is active — the engine re-cleans the tree on each attempt, so this dispatch will be retried automatically (subject to the normal retry cap).'
|
|
2817
2836
|
: (_alreadyDirtyFailed
|
|
@@ -2821,9 +2840,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2821
2840
|
try { writeInboxAlert(`live-checkout-dirty-${_wiIdForAlert}`, _alertBody); }
|
|
2822
2841
|
catch (e) { log('warn', `live-checkout: writeInboxAlert failed: ${e.message}`); }
|
|
2823
2842
|
try {
|
|
2824
|
-
const
|
|
2825
|
-
if (
|
|
2826
|
-
|
|
2843
|
+
const _wiScope = resolveWorkItemScope(dispatchItem.meta);
|
|
2844
|
+
if (_wiScope && dispatchItem.meta?.item?.id) {
|
|
2845
|
+
mutateWorkItems(_wiScope, (data) => {
|
|
2827
2846
|
if (!Array.isArray(data)) return data;
|
|
2828
2847
|
const wi = data.find(i => i && i.id === dispatchItem.meta.item.id);
|
|
2829
2848
|
if (wi) {
|
|
@@ -2892,9 +2911,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2892
2911
|
catch (e) { log('warn', `live-checkout: writeInboxAlert failed: ${e.message}`); }
|
|
2893
2912
|
const _pendingReason = _isMidOp ? 'live_checkout_mid_operation' : 'live_checkout_detached_head';
|
|
2894
2913
|
try {
|
|
2895
|
-
const
|
|
2896
|
-
if (
|
|
2897
|
-
|
|
2914
|
+
const _wiScope = resolveWorkItemScope(dispatchItem.meta);
|
|
2915
|
+
if (_wiScope && dispatchItem.meta?.item?.id) {
|
|
2916
|
+
mutateWorkItems(_wiScope, (data) => {
|
|
2898
2917
|
if (!Array.isArray(data)) return data;
|
|
2899
2918
|
const wi = data.find(i => i && i.id === dispatchItem.meta.item.id);
|
|
2900
2919
|
if (wi) wi._pendingReason = _pendingReason;
|
|
@@ -2966,9 +2985,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2966
2985
|
try { writeInboxAlert(`live-checkout-blocked-${_wiIdForAlert}`, _alertBody); }
|
|
2967
2986
|
catch (e) { log('warn', `live-checkout: writeInboxAlert failed: ${e.message}`); }
|
|
2968
2987
|
try {
|
|
2969
|
-
const
|
|
2970
|
-
if (
|
|
2971
|
-
|
|
2988
|
+
const _wiScope = resolveWorkItemScope(dispatchItem.meta);
|
|
2989
|
+
if (_wiScope && dispatchItem.meta?.item?.id) {
|
|
2990
|
+
mutateWorkItems(_wiScope, (data) => {
|
|
2972
2991
|
if (!Array.isArray(data)) return data;
|
|
2973
2992
|
const wi = data.find(i => i && i.id === dispatchItem.meta.item.id);
|
|
2974
2993
|
if (wi) wi._pendingReason = 'live_checkout_wrong_base';
|
|
@@ -3025,9 +3044,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3025
3044
|
try { writeInboxAlert(`live-checkout-blocked-${_wiIdForAlert}`, _alertBody); }
|
|
3026
3045
|
catch (e) { log('warn', `live-checkout: writeInboxAlert failed: ${e.message}`); }
|
|
3027
3046
|
try {
|
|
3028
|
-
const
|
|
3029
|
-
if (
|
|
3030
|
-
|
|
3047
|
+
const _wiScope = resolveWorkItemScope(dispatchItem.meta);
|
|
3048
|
+
if (_wiScope && dispatchItem.meta?.item?.id) {
|
|
3049
|
+
mutateWorkItems(_wiScope, (data) => {
|
|
3031
3050
|
if (!Array.isArray(data)) return data;
|
|
3032
3051
|
const wi = data.find(i => i && i.id === dispatchItem.meta.item.id);
|
|
3033
3052
|
if (wi) wi._pendingReason = 'live_checkout_blob_fetch';
|
|
@@ -3129,9 +3148,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3129
3148
|
try { writeInboxAlert(`live-checkout-worktree-conflict-${_wiIdForAlert}`, _alertBody); }
|
|
3130
3149
|
catch (e) { log('warn', `live-checkout: writeInboxAlert failed: ${e.message}`); }
|
|
3131
3150
|
try {
|
|
3132
|
-
const
|
|
3133
|
-
if (
|
|
3134
|
-
|
|
3151
|
+
const _wiScope = resolveWorkItemScope(dispatchItem.meta);
|
|
3152
|
+
if (_wiScope && dispatchItem.meta?.item?.id) {
|
|
3153
|
+
mutateWorkItems(_wiScope, (data) => {
|
|
3135
3154
|
if (!Array.isArray(data)) return data;
|
|
3136
3155
|
const wi = data.find(i => i && i.id === dispatchItem.meta.item.id);
|
|
3137
3156
|
if (wi) wi._pendingReason = 'live_checkout_worktree_conflict';
|
|
@@ -3215,9 +3234,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3215
3234
|
try { writeInboxAlert(`live-checkout-stale-base-${_wiIdForAlert}`, _alertBody); }
|
|
3216
3235
|
catch (e) { log('warn', `live-checkout: writeInboxAlert failed: ${e.message}`); }
|
|
3217
3236
|
try {
|
|
3218
|
-
const
|
|
3219
|
-
if (
|
|
3220
|
-
|
|
3237
|
+
const _wiScope = resolveWorkItemScope(dispatchItem.meta);
|
|
3238
|
+
if (_wiScope && dispatchItem.meta?.item?.id) {
|
|
3239
|
+
mutateWorkItems(_wiScope, (data) => {
|
|
3221
3240
|
if (!Array.isArray(data)) return data;
|
|
3222
3241
|
const wi = data.find(i => i && i.id === dispatchItem.meta.item.id);
|
|
3223
3242
|
if (wi) wi._pendingReason = 'live_checkout_stale_base';
|
|
@@ -3244,9 +3263,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3244
3263
|
// second-strike on its next dirty encounter. Only write when the stamp is set.
|
|
3245
3264
|
if (dispatchItem.meta?.item?._liveCheckoutDirtyAttempts) {
|
|
3246
3265
|
try {
|
|
3247
|
-
const
|
|
3248
|
-
if (
|
|
3249
|
-
|
|
3266
|
+
const _wiScope = resolveWorkItemScope(dispatchItem.meta);
|
|
3267
|
+
if (_wiScope && dispatchItem.meta?.item?.id) {
|
|
3268
|
+
mutateWorkItems(_wiScope, (data) => {
|
|
3250
3269
|
if (!Array.isArray(data)) return data;
|
|
3251
3270
|
const wi = data.find(i => i && i.id === dispatchItem.meta.item.id);
|
|
3252
3271
|
if (wi) delete wi._liveCheckoutDirtyAttempts;
|
|
@@ -3905,7 +3924,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3905
3924
|
// Fetch all dependency branches in parallel (git fetches are independent)
|
|
3906
3925
|
const fetchable = depBranches.filter(d => !_failedRefCache.has(d.branch));
|
|
3907
3926
|
const unfetchable = depBranches.filter(d => _failedRefCache.has(d.branch));
|
|
3908
|
-
const allPrsForDeps = unfetchable.length > 0 ? shared.getProjects(config).reduce((acc, p) => acc.concat(
|
|
3927
|
+
const allPrsForDeps = unfetchable.length > 0 ? shared.getProjects(config).reduce((acc, p) => acc.concat(readPullRequests(p)), []) : [];
|
|
3909
3928
|
for (const { branch: depBranch, prId } of unfetchable) {
|
|
3910
3929
|
const pr = allPrsForDeps.find(p => p.id === prId);
|
|
3911
3930
|
if (pr && (pr.status === PR_STATUS.MERGED || pr.status === PR_STATUS.CLOSED)) {
|
|
@@ -3938,7 +3957,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3938
3957
|
})
|
|
3939
3958
|
);
|
|
3940
3959
|
const hasFetchFailures = fetchResults.some(r => r.status === 'rejected');
|
|
3941
|
-
const allPrsForFetch = hasFetchFailures ? shared.getProjects(config).reduce((acc, p) => acc.concat(
|
|
3960
|
+
const allPrsForFetch = hasFetchFailures ? shared.getProjects(config).reduce((acc, p) => acc.concat(readPullRequests(p)), []) : [];
|
|
3942
3961
|
// Track branches recovered by local-only push so they can be merged
|
|
3943
3962
|
const recoveredBranches = new Set();
|
|
3944
3963
|
for (let i = 0; i < fetchResults.length; i++) {
|
|
@@ -4291,9 +4310,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
4291
4310
|
// so commits land on the dep's existing PR branch (W-mpcwojgr000a0244).
|
|
4292
4311
|
if (depConflictBranch && meta?.item?.id && project) {
|
|
4293
4312
|
try {
|
|
4294
|
-
const
|
|
4295
|
-
? projectWorkItemsPath(project)
|
|
4296
|
-
: path.join(MINIONS_DIR, 'work-items.json');
|
|
4313
|
+
const wiScope = project.name ? project : 'central';
|
|
4297
4314
|
const newItem = buildDepConflictFixItem({
|
|
4298
4315
|
depConflictBranch,
|
|
4299
4316
|
depConflictFiles,
|
|
@@ -4303,7 +4320,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
4303
4320
|
blockedItem: meta.item,
|
|
4304
4321
|
projectName: project.name || null,
|
|
4305
4322
|
});
|
|
4306
|
-
mutateWorkItems(
|
|
4323
|
+
mutateWorkItems(wiScope, items => {
|
|
4307
4324
|
// Don't create duplicate conflict-fix items
|
|
4308
4325
|
const existing = items.find(i => i.id === newItem.id && i.status !== WI_STATUS.DONE && i.status !== WI_STATUS.FAILED && i.status !== WI_STATUS.CANCELLED);
|
|
4309
4326
|
if (existing) return;
|
|
@@ -4400,7 +4417,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
4400
4417
|
// agent never gets a chance to push over the rebased tip.
|
|
4401
4418
|
// Read-only and non-fix dispatches are out of scope — implement tasks cut
|
|
4402
4419
|
// their own branch from main, and review/verify don't push.
|
|
4403
|
-
if (type
|
|
4420
|
+
if (shared.isFixLikeWorkType(type) && branchName && worktreePath && cwd === worktreePath) {
|
|
4404
4421
|
_phaseT.staleHeadStart = Date.now();
|
|
4405
4422
|
try {
|
|
4406
4423
|
const guard = await assertStaleHeadOk({
|
|
@@ -4538,7 +4555,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
4538
4555
|
log('info', `Spawning agent: ${agentId} (${id}) in ${cwd}`);
|
|
4539
4556
|
log('info', `Task type: ${type} | Branch: ${branchName || 'none'}`);
|
|
4540
4557
|
|
|
4541
|
-
// Agent status is derived from dispatch
|
|
4558
|
+
// Agent status is derived from dispatch state.
|
|
4542
4559
|
|
|
4543
4560
|
// Spawn the claude process
|
|
4544
4561
|
const childEnv = shared.cleanChildEnv();
|
|
@@ -5412,7 +5429,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
5412
5429
|
// agent described in its sidecar. This gate (a) rejects malformed
|
|
5413
5430
|
// sidecars as a hard non-retryable failure with a dedicated failure
|
|
5414
5431
|
// class + inbox alert, and (b) on success spawns each spec detached and
|
|
5415
|
-
// batch-records them in
|
|
5432
|
+
// batch-records them in SQL managed-process state. Healthcheck loops
|
|
5416
5433
|
// + dispatch ERROR-on-healthcheck-failure land in the follow-up item;
|
|
5417
5434
|
// for now a spec that spawns successfully is recorded with
|
|
5418
5435
|
// healthy:false, alive:true and the engine sweep / item-3 healthcheck
|
|
@@ -5911,9 +5928,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
5911
5928
|
// engine/managed-logs/.
|
|
5912
5929
|
if (managedSpawnHealthcheckFailure && managedSpawnHealthcheckFailure.annotation && dispatchItem.meta?.item?.id) {
|
|
5913
5930
|
try {
|
|
5914
|
-
const
|
|
5915
|
-
if (
|
|
5916
|
-
mutateWorkItems(
|
|
5931
|
+
const wiScope = resolveWorkItemScope(dispatchItem.meta);
|
|
5932
|
+
if (wiScope) {
|
|
5933
|
+
mutateWorkItems(wiScope, items => {
|
|
5917
5934
|
const wi = items.find(i => i.id === dispatchItem.meta.item.id);
|
|
5918
5935
|
if (!wi) return items;
|
|
5919
5936
|
wi._managedSpawnPartial = managedSpawnHealthcheckFailure.annotation;
|
|
@@ -5930,9 +5947,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
5930
5947
|
// rather than a queue gate.
|
|
5931
5948
|
if (keepProcessesAcceptanceFailure && dispatchItem.meta?.item?.id) {
|
|
5932
5949
|
try {
|
|
5933
|
-
const
|
|
5934
|
-
if (
|
|
5935
|
-
|
|
5950
|
+
const wiScope = resolveWorkItemScope(dispatchItem.meta);
|
|
5951
|
+
if (wiScope) {
|
|
5952
|
+
mutateWorkItems(wiScope, data => {
|
|
5936
5953
|
if (!Array.isArray(data)) return data;
|
|
5937
5954
|
const wi = data.find(i => i.id === dispatchItem.meta.item.id);
|
|
5938
5955
|
if (!wi) return data;
|
|
@@ -5949,7 +5966,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
5949
5966
|
// Cleanup temp files (whole per-dispatch dir, including PID/sysprompt
|
|
5950
5967
|
// tmp/prompt — P-f6-tmp-toctou). removeDispatchTmpDir validates the path
|
|
5951
5968
|
// resolves under engine/tmp/dispatch-* before rmSync, so a corrupted
|
|
5952
|
-
// dispatch
|
|
5969
|
+
// dispatch field cannot redirect this to an arbitrary path.
|
|
5953
5970
|
try { shared.removeDispatchTmpDir(dispatchTmpDir); } catch { /* cleanup */ }
|
|
5954
5971
|
|
|
5955
5972
|
log('info', `Agent ${agentId} completed. Output saved to ${archivePath}`);
|
|
@@ -6051,7 +6068,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
6051
6068
|
item.started_at = startedAt;
|
|
6052
6069
|
// Persist the resolved runtime so post-completion hooks (lifecycle.js) can
|
|
6053
6070
|
// route output parsing through the right adapter. Also surfaces the choice
|
|
6054
|
-
// in dispatch
|
|
6071
|
+
// in dispatch state for debugging multi-runtime fleets.
|
|
6055
6072
|
item.runtimeName = runtimeName;
|
|
6056
6073
|
// P-3f5b7d26 — persist pooled on the DISK record (activeProcesses/_pooled
|
|
6057
6074
|
// is wiped on restart); cli.js's reattach path needs this since a pooled
|
|
@@ -6078,14 +6095,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
6078
6095
|
// Atomically stamp dispatched_to/dispatched_at on the originating work item (#402).
|
|
6079
6096
|
if (meta?.item?.id) {
|
|
6080
6097
|
try {
|
|
6081
|
-
|
|
6082
|
-
if (
|
|
6083
|
-
|
|
6084
|
-
} else if (meta.source === 'work-item' && meta.project?.name) {
|
|
6085
|
-
wiPath = projectWorkItemsPath({ name: meta.project.name, localPath: meta.project.localPath });
|
|
6086
|
-
}
|
|
6087
|
-
if (wiPath) {
|
|
6088
|
-
mutateJsonFileLocked(wiPath, (items) => {
|
|
6098
|
+
const wiScope = resolveWorkItemScope(meta);
|
|
6099
|
+
if (wiScope) {
|
|
6100
|
+
mutateWorkItems(wiScope, (items) => {
|
|
6089
6101
|
if (!Array.isArray(items)) return items;
|
|
6090
6102
|
const wi = items.find(i => i.id === meta.item.id);
|
|
6091
6103
|
if (wi) {
|
|
@@ -6094,7 +6106,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
6094
6106
|
if (wi.status !== WI_STATUS.DISPATCHED) wi.status = WI_STATUS.DISPATCHED;
|
|
6095
6107
|
}
|
|
6096
6108
|
return items;
|
|
6097
|
-
}
|
|
6109
|
+
});
|
|
6098
6110
|
}
|
|
6099
6111
|
} catch (e) { log('warn', `stamp dispatched_to on work item ${meta.item.id}: ${e.message}`); }
|
|
6100
6112
|
}
|
|
@@ -6119,12 +6131,9 @@ function areDependenciesMet(item, config) {
|
|
|
6119
6131
|
for (const depId of deps) {
|
|
6120
6132
|
const depItem = allWorkItems.find(w => w.id === depId);
|
|
6121
6133
|
if (!depItem) {
|
|
6122
|
-
// Fallback: check PRD
|
|
6123
|
-
// safeJsonNoRestore — if the PRD has been archived, treat the dep as
|
|
6124
|
-
// unmet rather than resurrecting the active PRD from .backup
|
|
6125
|
-
// (W-mouptdh1000h9f39).
|
|
6134
|
+
// Fallback: check the PRD store — plan-to-prd agents may pre-set items to done.
|
|
6126
6135
|
try {
|
|
6127
|
-
const plan =
|
|
6136
|
+
const plan = prdStore.readPrd(sourcePlan);
|
|
6128
6137
|
const prdItem = (plan?.missing_features || []).find(f => f.id === depId);
|
|
6129
6138
|
if (prdItem && PRD_MET_STATUSES.has(prdItem.status)) continue; // PRD says done — treat as met
|
|
6130
6139
|
} catch (e) { log('warn', 'check PRD dep status: ' + e.message); }
|
|
@@ -6196,8 +6205,8 @@ function detectDependencyCycles(items) {
|
|
|
6196
6205
|
}
|
|
6197
6206
|
|
|
6198
6207
|
// Reconciles work items against known PRs.
|
|
6199
|
-
// Primary linkage comes from
|
|
6200
|
-
// uses
|
|
6208
|
+
// Primary linkage comes from tracked PR prdItems; fallback linkage
|
|
6209
|
+
// uses SQL PR links so matching does not depend on branch/title parsing.
|
|
6201
6210
|
// onlyIds: if provided, only items whose ID is in this Set are eligible.
|
|
6202
6211
|
function reconcileItemsWithPrs(items, allPrs, { onlyIds } = {}) {
|
|
6203
6212
|
const prLinks = shared.getPrLinks();
|
|
@@ -6289,7 +6298,7 @@ function updateSnapshot(config) {
|
|
|
6289
6298
|
|
|
6290
6299
|
// ─── Cooldowns (extracted to engine/cooldown.js) ─────────────────────────────
|
|
6291
6300
|
|
|
6292
|
-
const {
|
|
6301
|
+
const { dispatchCooldowns, loadCooldowns, saveCooldowns,
|
|
6293
6302
|
isOnCooldown, setCooldown, setCooldownWithContext, drainCoalescedContexts,
|
|
6294
6303
|
setCooldownFailure, clearCooldown, getPrReviewCooldownKey, clearLegacyPrReviewCooldown,
|
|
6295
6304
|
isAlreadyDispatched, isBranchActive } = require('./engine/cooldown');
|
|
@@ -6297,12 +6306,11 @@ const { COOLDOWN_PATH, dispatchCooldowns, loadCooldowns, saveCooldowns,
|
|
|
6297
6306
|
// Auto-clean pending/failed work items for a PRD so they re-materialize with updated plan data
|
|
6298
6307
|
function autoCleanPrdWorkItems(prdFile, config) {
|
|
6299
6308
|
const allProjects = getProjects(config);
|
|
6300
|
-
const
|
|
6301
|
-
for (const proj of allProjects) wiPaths.push(projectWorkItemsPath(proj));
|
|
6309
|
+
const workItemScopes = ['central', ...allProjects];
|
|
6302
6310
|
const deletedIds = [];
|
|
6303
|
-
for (const
|
|
6311
|
+
for (const scope of workItemScopes) {
|
|
6304
6312
|
try {
|
|
6305
|
-
mutateWorkItems(
|
|
6313
|
+
mutateWorkItems(scope, items => {
|
|
6306
6314
|
const filtered = items.filter(w => {
|
|
6307
6315
|
if (w.sourcePlan === prdFile && (w.status === WI_STATUS.PENDING || w.status === WI_STATUS.FAILED)) {
|
|
6308
6316
|
deletedIds.push(w.id); return false;
|
|
@@ -6345,51 +6353,6 @@ function safePrdProjectSlug(projectName) {
|
|
|
6345
6353
|
return slug || 'project';
|
|
6346
6354
|
}
|
|
6347
6355
|
|
|
6348
|
-
/**
|
|
6349
|
-
* Atomically reserve a unique PRD filename in `prdDir` (P-9b7e5d3c).
|
|
6350
|
-
*
|
|
6351
|
-
* Replaces the racy "fs.existsSync(...) then write" pattern: two parallel
|
|
6352
|
-
* materializations on the same plan slug previously both observed a 'free'
|
|
6353
|
-
* filename and silently overwrote each other. This helper uses
|
|
6354
|
-
* `fs.openSync(path, 'wx')` so the OS rejects duplicate creates atomically.
|
|
6355
|
-
*
|
|
6356
|
-
* Attempt schedule (100 attempts total):
|
|
6357
|
-
* attempt = 0 → baseFileName (e.g. "slug.json")
|
|
6358
|
-
* attempt = 1 → "${stem}-1${ext}" (e.g. "slug-1.json")
|
|
6359
|
-
* attempt = 2 → "${stem}-2${ext}"
|
|
6360
|
-
* ...
|
|
6361
|
-
* attempt = 99 → "${stem}-99${ext}"
|
|
6362
|
-
*
|
|
6363
|
-
* Behavior:
|
|
6364
|
-
* - On EEXIST: increment attempt and retry.
|
|
6365
|
-
* - Any other openSync error propagates immediately.
|
|
6366
|
-
* - After 100 EEXIST failures: throw
|
|
6367
|
-
* 'Could not reserve unique PRD filename after 100 attempts'.
|
|
6368
|
-
* - On success: write `content` to the reserved file, close fd, return basename.
|
|
6369
|
-
*/
|
|
6370
|
-
function reservePrdFilename(prdDir, baseFileName, content = '') {
|
|
6371
|
-
const ext = path.extname(baseFileName);
|
|
6372
|
-
const stem = ext ? baseFileName.slice(0, -ext.length) : baseFileName;
|
|
6373
|
-
for (let attempt = 0; attempt < 100; attempt++) {
|
|
6374
|
-
const candidateName = attempt === 0 ? baseFileName : `${stem}-${attempt}${ext}`;
|
|
6375
|
-
const candidatePath = path.join(prdDir, candidateName);
|
|
6376
|
-
let fd;
|
|
6377
|
-
try {
|
|
6378
|
-
fd = fs.openSync(candidatePath, 'wx');
|
|
6379
|
-
} catch (err) {
|
|
6380
|
-
if (err && err.code === 'EEXIST') continue;
|
|
6381
|
-
throw err;
|
|
6382
|
-
}
|
|
6383
|
-
try {
|
|
6384
|
-
if (content) fs.writeSync(fd, content);
|
|
6385
|
-
} finally {
|
|
6386
|
-
fs.closeSync(fd);
|
|
6387
|
-
}
|
|
6388
|
-
return candidateName;
|
|
6389
|
-
}
|
|
6390
|
-
throw new Error('Could not reserve unique PRD filename after 100 attempts');
|
|
6391
|
-
}
|
|
6392
|
-
|
|
6393
6356
|
// Fleet branches the engine/CC create. A worktree-mode operator checkout should
|
|
6394
6357
|
// NEVER sit on one of these (or any branch); when it does, a flow ran raw
|
|
6395
6358
|
// `git checkout`/`merge` in the live checkout instead of a worktree.
|
|
@@ -6458,16 +6421,15 @@ function restoreHijackedOperatorCheckouts(config) {
|
|
|
6458
6421
|
}
|
|
6459
6422
|
|
|
6460
6423
|
function materializePlansAsWorkItems(config) {
|
|
6461
|
-
if (!fs.existsSync(PRD_DIR)) { try { fs.mkdirSync(PRD_DIR, { recursive: true }); } catch (e) { log('warn', 'create PRD directory: ' + e.message); } }
|
|
6462
6424
|
const writePrdLocked = (fileName, data) => {
|
|
6463
|
-
return
|
|
6425
|
+
return prdStore.writePrd(fileName, data);
|
|
6464
6426
|
};
|
|
6465
|
-
const mutatePrdLocked = (fileName, fallback, mutator
|
|
6466
|
-
return
|
|
6427
|
+
const mutatePrdLocked = (fileName, fallback, mutator) => {
|
|
6428
|
+
return prdStore.mutatePrd(fileName, (current) => {
|
|
6467
6429
|
if (!current?.missing_features && fallback?.missing_features) current = fallback;
|
|
6468
6430
|
if (!current || Array.isArray(current) || typeof current !== 'object') current = {};
|
|
6469
6431
|
return mutator(current) || current;
|
|
6470
|
-
}, { defaultValue: fallback || {}
|
|
6432
|
+
}, { defaultValue: fallback || {} });
|
|
6471
6433
|
};
|
|
6472
6434
|
const enforceDeclaredPlanProject = (fileName, currentPlan) => {
|
|
6473
6435
|
if (!currentPlan?.source_plan) return { fileName, plan: currentPlan };
|
|
@@ -6526,7 +6488,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
6526
6488
|
const jsonName = mf.replace(/\.md$/, '.json');
|
|
6527
6489
|
// Atomic open ('wx' + retry) so two parallel migrations on the
|
|
6528
6490
|
// same slug don't both overwrite a single PRD (P-9b7e5d3c).
|
|
6529
|
-
const reserved =
|
|
6491
|
+
const reserved = prdStore.createPrdUnique(jsonName, parsed);
|
|
6530
6492
|
try { fs.unlinkSync(path.join(checkDir, mf)); } catch { /* cleanup */ }
|
|
6531
6493
|
log('info', `Plan enforcement: moved ${mf} → prd/${reserved} (PRDs must be .json in prd/)`);
|
|
6532
6494
|
}
|
|
@@ -6543,7 +6505,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
6543
6505
|
if (parsed?.missing_features) {
|
|
6544
6506
|
// Atomic open ('wx' + retry) so two parallel migrations on the
|
|
6545
6507
|
// same slug don't both overwrite a single PRD (P-9b7e5d3c).
|
|
6546
|
-
const reserved =
|
|
6508
|
+
const reserved = prdStore.createPrdUnique(jf, parsed);
|
|
6547
6509
|
try { fs.unlinkSync(path.join(PLANS_DIR, jf)); } catch { /* cleanup */ }
|
|
6548
6510
|
log('info', `Auto-migrated PRD ${jf} from plans/ to prd/${reserved}`);
|
|
6549
6511
|
}
|
|
@@ -6563,10 +6525,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
6563
6525
|
const SEQUENTIAL_ID_RE = /^[A-Za-z]+-?\d+$/;
|
|
6564
6526
|
|
|
6565
6527
|
for (let file of planFiles) {
|
|
6566
|
-
|
|
6567
|
-
// read, do not auto-resurrect it from a stale .backup sidecar
|
|
6568
|
-
// (W-mouptdh1000h9f39).
|
|
6569
|
-
let plan = safeJsonNoRestore(path.join(PRD_DIR, file));
|
|
6528
|
+
let plan = prdStore.readPrd(file);
|
|
6570
6529
|
if (!plan?.missing_features) continue;
|
|
6571
6530
|
// Resurrection-re-execution guard ("old PRDs came back"): never (re)materialize
|
|
6572
6531
|
// a defunct PRD — archived, or completed with its source plan gone from plans/.
|
|
@@ -6713,7 +6672,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
6713
6672
|
const defaultProjectName = plan.project || file.replace(/-\d{4}-\d{2}-\d{2}\.json$/, '');
|
|
6714
6673
|
const allProjects = getProjects(config);
|
|
6715
6674
|
const defaultProject = shared.resolveProjectSource(defaultProjectName, allProjects, { allowCentral: false }).project;
|
|
6716
|
-
// No project found — use central
|
|
6675
|
+
// No project found — use central state (engine works without projects).
|
|
6717
6676
|
const useCentral = !defaultProject;
|
|
6718
6677
|
|
|
6719
6678
|
const statusFilter = PRD_MATERIALIZABLE;
|
|
@@ -6737,7 +6696,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
6737
6696
|
);
|
|
6738
6697
|
|
|
6739
6698
|
// Group items by target project (per-item project field overrides plan-level project)
|
|
6740
|
-
// When no projects are configured, all items
|
|
6699
|
+
// When no projects are configured, all items use the central scope.
|
|
6741
6700
|
const itemsByProject = new Map(); // projectName -> { project, items: [] }
|
|
6742
6701
|
for (const item of items) {
|
|
6743
6702
|
if (item.project) {
|
|
@@ -6787,19 +6746,19 @@ function materializePlansAsWorkItems(config) {
|
|
|
6787
6746
|
// A pause/reject/awaiting-approval signal may arrive between the outer-loop initial
|
|
6788
6747
|
// read and this point (e.g. operator pauses via dashboard while the tick is running).
|
|
6789
6748
|
// Re-reading here prevents phantom WI creation on a paused, rejected, or unapproved plan.
|
|
6790
|
-
const freshPrd = prdStore.readPrd(
|
|
6749
|
+
const freshPrd = prdStore.readPrd(file);
|
|
6791
6750
|
const freshStatus = (freshPrd || {}).status || ((freshPrd || {}).requires_approval ? 'awaiting-approval' : null);
|
|
6792
6751
|
if (freshStatus === PLAN_STATUS.PAUSED || freshStatus === PLAN_STATUS.REJECTED || freshStatus === 'awaiting-approval') {
|
|
6793
6752
|
log('warn', `PRD ${file}: status changed to '${freshStatus}' mid-tick — skipping WI batch for ${projName} (P-f1a3b5c7)`);
|
|
6794
6753
|
break;
|
|
6795
6754
|
}
|
|
6796
6755
|
|
|
6797
|
-
const
|
|
6756
|
+
const wiScope = project || 'central';
|
|
6798
6757
|
let created = 0;
|
|
6799
6758
|
const newlyCreatedIds = new Set(); // tracks IDs created in this pass for reconciliation scoping
|
|
6800
6759
|
const deferredReopens = []; // cross-project re-opens executed after this lock releases
|
|
6801
6760
|
|
|
6802
|
-
mutateWorkItems(
|
|
6761
|
+
mutateWorkItems(wiScope, existingItems => {
|
|
6803
6762
|
for (const item of projItems) {
|
|
6804
6763
|
// Re-open: 'updated' or 'missing' re-opens a done work item (#906)
|
|
6805
6764
|
// Use composite (sourcePlan, id) key so a WI from another PRD with the
|
|
@@ -6821,7 +6780,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
6821
6780
|
if (!alreadyExists) {
|
|
6822
6781
|
for (const p of allProjects) {
|
|
6823
6782
|
if (String(p.name || '').toLowerCase() === String(projName || '').toLowerCase()) continue;
|
|
6824
|
-
const otherItems =
|
|
6783
|
+
const otherItems = readWorkItems(p);
|
|
6825
6784
|
const otherWi = otherItems.find(w => w.id === item.id && w.sourcePlan === file);
|
|
6826
6785
|
if (otherWi) {
|
|
6827
6786
|
if (DONE_STATUSES.has(otherWi.status) && shouldReopen) {
|
|
@@ -6867,7 +6826,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
6867
6826
|
|
|
6868
6827
|
if (created > 0) {
|
|
6869
6828
|
// Reconciliation: exact prdItems match only, scoped to newly created items
|
|
6870
|
-
const allPrsForReconcile = allProjects.flatMap(p =>
|
|
6829
|
+
const allPrsForReconcile = allProjects.flatMap(p => readPullRequests(p));
|
|
6871
6830
|
const reconciled = reconcileItemsWithPrs(existingItems, allPrsForReconcile, { onlyIds: newlyCreatedIds });
|
|
6872
6831
|
if (reconciled > 0) log('info', `Plan reconciliation: marked ${reconciled} item(s) as done → ${projName}`);
|
|
6873
6832
|
|
|
@@ -6901,8 +6860,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
6901
6860
|
for (const { itemId, projectName: rProjName, item: rItem } of deferredReopens) {
|
|
6902
6861
|
const rProject = shared.resolveProjectSource(rProjName, allProjects, { allowCentral: false }).project;
|
|
6903
6862
|
if (!rProject) continue;
|
|
6904
|
-
|
|
6905
|
-
mutateWorkItems(rPath, items => {
|
|
6863
|
+
mutateWorkItems(rProject, items => {
|
|
6906
6864
|
const target = items.find(w => w.id === itemId);
|
|
6907
6865
|
if (target && DONE_STATUSES.has(target.status)) {
|
|
6908
6866
|
shared.reopenWorkItem(target);
|
|
@@ -6982,8 +6940,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
6982
6940
|
function clearPendingHumanFeedbackFlag(projectMeta, prId) {
|
|
6983
6941
|
if (!prId) return;
|
|
6984
6942
|
try {
|
|
6985
|
-
|
|
6986
|
-
mutatePullRequests(prsPath, prs => {
|
|
6943
|
+
mutatePullRequests(projectMeta, prs => {
|
|
6987
6944
|
const target = shared.findPrRecord(prs, prId, projectMeta);
|
|
6988
6945
|
if (!target?.humanFeedback?.pendingFix) return;
|
|
6989
6946
|
target.humanFeedback.pendingFix = false;
|
|
@@ -7058,7 +7015,7 @@ async function isWorktreeHeldPauseSuppressed(pr, project) {
|
|
|
7058
7015
|
}
|
|
7059
7016
|
if (!stillHeld) {
|
|
7060
7017
|
try {
|
|
7061
|
-
mutatePullRequests(
|
|
7018
|
+
mutatePullRequests(project, (prs) => {
|
|
7062
7019
|
if (!Array.isArray(prs)) return prs;
|
|
7063
7020
|
const target = shared.findPrRecord(prs, pr, project);
|
|
7064
7021
|
if (!target) return prs;
|
|
@@ -7110,7 +7067,7 @@ function resolvePrBranch(pr) {
|
|
|
7110
7067
|
function updatePrBranchResolutionState(project, pr, { branch = '', reason = '' } = {}) {
|
|
7111
7068
|
let changed = false;
|
|
7112
7069
|
try {
|
|
7113
|
-
mutatePullRequests(
|
|
7070
|
+
mutatePullRequests(project, prs => {
|
|
7114
7071
|
const target = shared.findPrRecord(prs, pr, project);
|
|
7115
7072
|
if (!target) return;
|
|
7116
7073
|
if (branch) {
|
|
@@ -7363,19 +7320,24 @@ function _resetAutoFixPausedLogState() {
|
|
|
7363
7320
|
// persist — the warning is for the operator at engine startup/run time).
|
|
7364
7321
|
const _warnedSilentDiscovery = new Set();
|
|
7365
7322
|
|
|
7366
|
-
function _warnSilentDiscoveryOnce(kind, project,
|
|
7323
|
+
function _warnSilentDiscoveryOnce(kind, project, config) {
|
|
7367
7324
|
const key = `${kind}:${project?.name || project?.localPath || 'default'}`;
|
|
7368
7325
|
if (_warnedSilentDiscovery.has(key)) return;
|
|
7369
7326
|
let count = 0;
|
|
7370
|
-
try {
|
|
7371
|
-
|
|
7327
|
+
try {
|
|
7328
|
+
const records = kind === 'pullRequests'
|
|
7329
|
+
? shared.readPullRequests(project)
|
|
7330
|
+
: shared.readWorkItems(project);
|
|
7331
|
+
if (Array.isArray(records)) count = records.length;
|
|
7332
|
+
} catch {}
|
|
7333
|
+
if (count <= 0) return;
|
|
7372
7334
|
_warnedSilentDiscovery.add(key);
|
|
7373
7335
|
const projName = project?.name || '(unnamed)';
|
|
7374
7336
|
const hasBlock = !!(project?.workSources?.[kind] || config?.workSources?.[kind]);
|
|
7375
7337
|
const hint = hasBlock
|
|
7376
7338
|
? `Toggle in Dashboard → Settings → Project, or set \`workSources.${kind}.enabled: true\` in config.json.`
|
|
7377
7339
|
: `Run \`minions doctor\` to inspect, \`minions init\` if you cloned the repo without setup, or re-link the project: \`minions add ${project?.localPath || projName}\`.`;
|
|
7378
|
-
log('warn', `Silent-discovery footgun: project "${projName}" has ${count} record(s)
|
|
7340
|
+
log('warn', `Silent-discovery footgun: project "${projName}" has ${count} ${kind} record(s) but workSources.${kind}.enabled is not true — engine will not pick them up. ${hint}`);
|
|
7379
7341
|
}
|
|
7380
7342
|
|
|
7381
7343
|
/**
|
|
@@ -7396,12 +7358,12 @@ function _logPrDispatchSkipOnce(pr, reason) {
|
|
|
7396
7358
|
}
|
|
7397
7359
|
|
|
7398
7360
|
/**
|
|
7399
|
-
* Scan
|
|
7361
|
+
* Scan tracked PR state for records needing review or fixes.
|
|
7400
7362
|
*/
|
|
7401
7363
|
async function discoverFromPrs(config, project) {
|
|
7402
7364
|
const src = project?.workSources?.pullRequests || config.workSources?.pullRequests;
|
|
7403
7365
|
if (!src?.enabled) {
|
|
7404
|
-
_warnSilentDiscoveryOnce('pullRequests', project,
|
|
7366
|
+
_warnSilentDiscoveryOnce('pullRequests', project, config);
|
|
7405
7367
|
return [];
|
|
7406
7368
|
}
|
|
7407
7369
|
|
|
@@ -7549,7 +7511,7 @@ async function discoverFromPrs(config, project) {
|
|
|
7549
7511
|
if (pr.reviewStatus !== REVIEW_STATUS.APPROVED) pr.reviewStatus = liveStatus;
|
|
7550
7512
|
// Persist so next tick doesn't re-check
|
|
7551
7513
|
try {
|
|
7552
|
-
|
|
7514
|
+
mutatePullRequests(project, data => {
|
|
7553
7515
|
if (!Array.isArray(data)) return data;
|
|
7554
7516
|
const target = shared.findPrRecord(data, pr, project);
|
|
7555
7517
|
if (target && target.reviewStatus !== REVIEW_STATUS.APPROVED) target.reviewStatus = liveStatus;
|
|
@@ -7765,7 +7727,7 @@ async function discoverFromPrs(config, project) {
|
|
|
7765
7727
|
log('info', `Pre-dispatch vote check: ${pr.id} is ${liveStatus} (cached was waiting) — skipping re-review`);
|
|
7766
7728
|
if (pr.reviewStatus !== REVIEW_STATUS.APPROVED) pr.reviewStatus = liveStatus;
|
|
7767
7729
|
try {
|
|
7768
|
-
|
|
7730
|
+
mutatePullRequests(project, data => {
|
|
7769
7731
|
if (!Array.isArray(data)) return data;
|
|
7770
7732
|
const target = shared.findPrRecord(data, pr, project);
|
|
7771
7733
|
if (target && target.reviewStatus !== REVIEW_STATUS.APPROVED) target.reviewStatus = liveStatus;
|
|
@@ -7940,7 +7902,7 @@ async function discoverFromPrs(config, project) {
|
|
|
7940
7902
|
const live = await checkBcFn(pr, project);
|
|
7941
7903
|
if (live?.buildStatusStale) {
|
|
7942
7904
|
try {
|
|
7943
|
-
mutatePullRequests(
|
|
7905
|
+
mutatePullRequests(project, prs => {
|
|
7944
7906
|
const target = shared.findPrRecord(prs, pr, project);
|
|
7945
7907
|
if (!target) return;
|
|
7946
7908
|
target._buildStatusStale = true;
|
|
@@ -7952,7 +7914,7 @@ async function discoverFromPrs(config, project) {
|
|
|
7952
7914
|
log('info', `Pre-dispatch build check: ${pr.id} build is ${live.buildStatus} (cached was failing) — skipping build-fix`);
|
|
7953
7915
|
// Persist the fresh status so subsequent ticks don't re-check on every pass
|
|
7954
7916
|
try {
|
|
7955
|
-
mutatePullRequests(
|
|
7917
|
+
mutatePullRequests(project, prs => {
|
|
7956
7918
|
const target = shared.findPrRecord(prs, pr, project);
|
|
7957
7919
|
if (!target) return;
|
|
7958
7920
|
target.buildStatus = live.buildStatus;
|
|
@@ -8016,8 +7978,7 @@ async function discoverFromPrs(config, project) {
|
|
|
8016
7978
|
if (pr.agent && !pr._buildFailNotified) {
|
|
8017
7979
|
// Mark noted to prevent repeated processing; the fix dispatch and PR state carry the failure details.
|
|
8018
7980
|
try {
|
|
8019
|
-
|
|
8020
|
-
mutatePullRequests(prPath, prs => {
|
|
7981
|
+
mutatePullRequests(project, prs => {
|
|
8021
7982
|
const target = shared.findPrRecord(prs, pr, project);
|
|
8022
7983
|
if (target) {
|
|
8023
7984
|
target._buildFailNotified = true;
|
|
@@ -8082,7 +8043,7 @@ async function discoverFromPrs(config, project) {
|
|
|
8082
8043
|
if (live && live.mergeConflict === false) {
|
|
8083
8044
|
log('info', `Pre-dispatch conflict check: ${pr.id} reports clean merge (cached was conflict) — skipping conflict-fix`);
|
|
8084
8045
|
try {
|
|
8085
|
-
mutatePullRequests(
|
|
8046
|
+
mutatePullRequests(project, prs => {
|
|
8086
8047
|
const target = shared.findPrRecord(prs, pr, project);
|
|
8087
8048
|
if (!target) return;
|
|
8088
8049
|
delete target._mergeConflict;
|
|
@@ -8111,7 +8072,7 @@ async function discoverFromPrs(config, project) {
|
|
|
8111
8072
|
newWork.push(item);
|
|
8112
8073
|
// Record dispatch timestamp so re-dispatch is suppressed during ADO lag window
|
|
8113
8074
|
try {
|
|
8114
|
-
mutatePullRequests(
|
|
8075
|
+
mutatePullRequests(project, prs => {
|
|
8115
8076
|
const target = shared.findPrRecord(prs, pr, project);
|
|
8116
8077
|
if (target) target._conflictFixedAt = new Date().toISOString();
|
|
8117
8078
|
});
|
|
@@ -8211,7 +8172,7 @@ function _buildRunnerBriefVars(item, project) {
|
|
|
8211
8172
|
}
|
|
8212
8173
|
|
|
8213
8174
|
/**
|
|
8214
|
-
* Scan work-
|
|
8175
|
+
* Scan a project work-item scope for manually queued tasks.
|
|
8215
8176
|
*/
|
|
8216
8177
|
function renderProjectWorkItemPromptForAgent(item, workType, agentId, config, project, root, branchName, options = {}) {
|
|
8217
8178
|
const worktreePath = options.worktreePath || path.resolve(root, config.engine?.worktreeRoot || '../worktrees', `${branchName}`);
|
|
@@ -8425,7 +8386,7 @@ function resolveWorkItemPrRecord(item, project) {
|
|
|
8425
8386
|
if (!project) return null;
|
|
8426
8387
|
const prRef = getWorkItemPrRef(item);
|
|
8427
8388
|
if (!prRef) return null;
|
|
8428
|
-
const prs =
|
|
8389
|
+
const prs = readPullRequests(project);
|
|
8429
8390
|
shared.normalizePrRecords(prs, project);
|
|
8430
8391
|
return shared.findPrRecord(prs, prRef, project);
|
|
8431
8392
|
}
|
|
@@ -8492,7 +8453,7 @@ function refreshDeferredReviewPrompt(item, config) {
|
|
|
8492
8453
|
// head SHA before rebuilding the prompt.
|
|
8493
8454
|
let livePr = pr;
|
|
8494
8455
|
try {
|
|
8495
|
-
const prs =
|
|
8456
|
+
const prs = readPullRequests(project);
|
|
8496
8457
|
const found = shared.findPrRecord(prs, pr, project);
|
|
8497
8458
|
if (found) livePr = found;
|
|
8498
8459
|
} catch (e) { log('warn', `refreshDeferredReviewPrompt: live PR lookup for ${pr.id} failed: ${e.message}`); }
|
|
@@ -8524,12 +8485,12 @@ function refreshDeferredReviewPrompt(item, config) {
|
|
|
8524
8485
|
function discoverFromWorkItems(config, project) {
|
|
8525
8486
|
const src = project?.workSources?.workItems || config.workSources?.workItems;
|
|
8526
8487
|
if (!src?.enabled) {
|
|
8527
|
-
_warnSilentDiscoveryOnce('workItems', project,
|
|
8488
|
+
_warnSilentDiscoveryOnce('workItems', project, config);
|
|
8528
8489
|
return [];
|
|
8529
8490
|
}
|
|
8530
8491
|
|
|
8531
8492
|
const root = project?.localPath ? path.resolve(project.localPath) : path.resolve(MINIONS_DIR, '..');
|
|
8532
|
-
const items =
|
|
8493
|
+
const items = readWorkItems(project);
|
|
8533
8494
|
// W-mq9b7lor (H3): snapshot each item's serializable state BEFORE the
|
|
8534
8495
|
// discover loop runs so we can compute per-item field deltas at the end
|
|
8535
8496
|
// and apply them inside the mutateWorkItems lock — the prior
|
|
@@ -8639,7 +8600,7 @@ function discoverFromWorkItems(config, project) {
|
|
|
8639
8600
|
}
|
|
8640
8601
|
|
|
8641
8602
|
const key = `work-${project?.name || 'default'}-${item.id}`;
|
|
8642
|
-
// Self-heal: collect keys for batched dispatch
|
|
8603
|
+
// Self-heal: collect keys for batched dispatch cleanup.
|
|
8643
8604
|
selfHealKeys.add(key);
|
|
8644
8605
|
dispatchCooldowns.delete(key);
|
|
8645
8606
|
// Cooldown bypass for resumed items
|
|
@@ -8743,7 +8704,7 @@ function discoverFromWorkItems(config, project) {
|
|
|
8743
8704
|
return b && b > 0 && getMonthlySpend(id) >= b && isAgentIdle(id);
|
|
8744
8705
|
});
|
|
8745
8706
|
if (!agentId) {
|
|
8746
|
-
if (!budgetBlocked && !hardPinRequested && workType
|
|
8707
|
+
if (!budgetBlocked && !hardPinRequested && !shared.isFixLikeWorkType(workType)) {
|
|
8747
8708
|
reservedAgentId = resolveAgentReservation(workType, config, { agentHints });
|
|
8748
8709
|
agentId = reservedAgentId && !hasAgentHints ? routing.ANY_AGENT : reservedAgentId;
|
|
8749
8710
|
}
|
|
@@ -8785,7 +8746,7 @@ function discoverFromWorkItems(config, project) {
|
|
|
8785
8746
|
const linkedPr = item.skipPr ? null : resolveWorkItemPrRecord(item, project);
|
|
8786
8747
|
const promptItem = linkedPr ? withWorkItemPrContext(item, linkedPr) : item;
|
|
8787
8748
|
const prBranch = linkedPr?.branch || '';
|
|
8788
|
-
const isPrTargeted = !!(linkedPr && (workType
|
|
8749
|
+
const isPrTargeted = !!(linkedPr && (shared.isFixLikeWorkType(workType) || workType === WORK_TYPE.REVIEW || workType === WORK_TYPE.TEST));
|
|
8789
8750
|
// Issue #607 — `linkedPr` above resolves through the LOOSE
|
|
8790
8751
|
// extractWorkItemPrRef (title/first-paragraph-of-description text scan),
|
|
8791
8752
|
// which is fine for prompt-context enrichment (promptItem, above) that
|
|
@@ -8807,7 +8768,7 @@ function discoverFromWorkItems(config, project) {
|
|
|
8807
8768
|
// create-time stamp path keeps using the loose form — gate uses
|
|
8808
8769
|
// structured-only; stamp uses loose. See engine/shared.js
|
|
8809
8770
|
// extractStructuredWorkItemPrRef for the structured-source list.
|
|
8810
|
-
if (!linkedPr && getStructuredWorkItemPrRef(item) && (workType
|
|
8771
|
+
if (!linkedPr && getStructuredWorkItemPrRef(item) && (shared.isFixLikeWorkType(workType) || workType === WORK_TYPE.REVIEW || workType === WORK_TYPE.TEST)) {
|
|
8811
8772
|
if (item._pendingReason !== 'pr_not_found') { item._pendingReason = 'pr_not_found'; needsWrite = true; }
|
|
8812
8773
|
log('warn', `Work item ${item.id} references PR ${getStructuredWorkItemPrRef(item)} but no tracked PR record was found`);
|
|
8813
8774
|
continue;
|
|
@@ -8945,7 +8906,7 @@ function discoverFromWorkItems(config, project) {
|
|
|
8945
8906
|
}
|
|
8946
8907
|
}
|
|
8947
8908
|
if (patches.size > 0) {
|
|
8948
|
-
mutateWorkItems(
|
|
8909
|
+
mutateWorkItems(project, (current) => {
|
|
8949
8910
|
for (const it of current) {
|
|
8950
8911
|
const patch = patches.get(it.id);
|
|
8951
8912
|
if (!patch) continue;
|
|
@@ -9110,7 +9071,7 @@ function buildWorkItemDispatchVars(item, vars, config, options = {}) {
|
|
|
9110
9071
|
// SHERLOC (P-mqyp0008v022w3x4): inject prior explore context for fix dispatches.
|
|
9111
9072
|
// When a completed explore WI has a reference to this fix WI, surface its
|
|
9112
9073
|
// resultSummary so the fix agent starts with pre-localized fault context.
|
|
9113
|
-
if (workType
|
|
9074
|
+
if (shared.isFixLikeWorkType(workType)) {
|
|
9114
9075
|
vars.prior_explore_context = resolvePriorExploreContext(item, config);
|
|
9115
9076
|
}
|
|
9116
9077
|
|
|
@@ -9211,10 +9172,9 @@ function materializeSpecsAsWorkItems(config, project) {
|
|
|
9211
9172
|
|
|
9212
9173
|
if (recentSpecs.length === 0) return;
|
|
9213
9174
|
|
|
9214
|
-
const wiPath = projectWorkItemsPath(project);
|
|
9215
9175
|
let created = 0;
|
|
9216
9176
|
|
|
9217
|
-
mutateWorkItems(
|
|
9177
|
+
mutateWorkItems(project, existingItems => {
|
|
9218
9178
|
for (const pr of mergedPrs) {
|
|
9219
9179
|
const prBranch = (pr.branch || '').toLowerCase();
|
|
9220
9180
|
const matchedSpecs = recentSpecs.filter(doc => {
|
|
@@ -9301,12 +9261,11 @@ function extractSpecInfo(filePath, projectRoot_) {
|
|
|
9301
9261
|
}
|
|
9302
9262
|
|
|
9303
9263
|
/**
|
|
9304
|
-
* Scan central
|
|
9264
|
+
* Scan central SQL state for project-agnostic tasks.
|
|
9305
9265
|
* Uses the shared work-item.md playbook with multi-project context injected.
|
|
9306
9266
|
*/
|
|
9307
9267
|
function discoverCentralWorkItems(config) {
|
|
9308
|
-
const
|
|
9309
|
-
const items = safeJsonArr(centralPath);
|
|
9268
|
+
const items = readWorkItems('central');
|
|
9310
9269
|
const projects = getProjects(config);
|
|
9311
9270
|
const dispatchProjects = getCentralDispatchProjects(projects);
|
|
9312
9271
|
const newWork = [];
|
|
@@ -9439,7 +9398,7 @@ function discoverCentralWorkItems(config) {
|
|
|
9439
9398
|
const hardPinRequested = !!hardPinnedAgent;
|
|
9440
9399
|
const agentId = hardPinnedAgent
|
|
9441
9400
|
|| resolveAgent(workType, config, { agentHints })
|
|
9442
|
-
|| (!hardPinRequested && workType
|
|
9401
|
+
|| (!hardPinRequested && !shared.isFixLikeWorkType(workType) ? resolveAgentReservation(workType, config, { agentHints }) : null);
|
|
9443
9402
|
if (!agentId) continue;
|
|
9444
9403
|
|
|
9445
9404
|
const agentName = config.agents[agentId]?.name || agentId;
|
|
@@ -9606,7 +9565,6 @@ function discoverCentralWorkItems(config) {
|
|
|
9606
9565
|
// Inject plan-to-prd variables — read the plan file content for the playbook
|
|
9607
9566
|
if (workType === WORK_TYPE.PLAN_TO_PRD && item.planFile) {
|
|
9608
9567
|
if (!fs.existsSync(PLANS_DIR)) fs.mkdirSync(PLANS_DIR, { recursive: true });
|
|
9609
|
-
if (!fs.existsSync(PRD_DIR)) fs.mkdirSync(PRD_DIR, { recursive: true });
|
|
9610
9568
|
if (planFileContent !== null) {
|
|
9611
9569
|
vars.plan_content = planFileContent;
|
|
9612
9570
|
} else {
|
|
@@ -9648,7 +9606,7 @@ function discoverCentralWorkItems(config) {
|
|
|
9648
9606
|
const prdRows = prdStore.listPrdRows();
|
|
9649
9607
|
const prdFiles = prdRows.filter(row => !row.archived).map(row => row.filename);
|
|
9650
9608
|
for (const pf of prdFiles) {
|
|
9651
|
-
const prd = prdStore.readPrd(
|
|
9609
|
+
const prd = prdStore.readPrd(pf);
|
|
9652
9610
|
if (planKey && prd && !shared.isPrdArchived(prd) && shared.prdMatchesSourcePlan(prd.source_plan, planKey)) {
|
|
9653
9611
|
prdFilename = pf;
|
|
9654
9612
|
vars.existing_prd_json = JSON.stringify(prd, null, 2);
|
|
@@ -9747,53 +9705,20 @@ function discoverCentralWorkItems(config) {
|
|
|
9747
9705
|
}
|
|
9748
9706
|
|
|
9749
9707
|
if (mutations.size > 0) {
|
|
9750
|
-
//
|
|
9751
|
-
|
|
9708
|
+
// Apply field patches to a fresh transactional snapshot.
|
|
9709
|
+
mutateWorkItems('central', (freshItems) => {
|
|
9752
9710
|
if (!Array.isArray(freshItems)) freshItems = [];
|
|
9753
9711
|
for (const fi of freshItems) {
|
|
9754
9712
|
const m = mutations.get(fi.id);
|
|
9755
9713
|
if (m) Object.assign(fi, m);
|
|
9756
9714
|
}
|
|
9757
9715
|
return freshItems;
|
|
9758
|
-
}
|
|
9716
|
+
});
|
|
9759
9717
|
}
|
|
9760
9718
|
return newWork;
|
|
9761
9719
|
}
|
|
9762
9720
|
|
|
9763
9721
|
|
|
9764
|
-
/**
|
|
9765
|
-
* Sweep stale `.backup` sidecars in `prd/` whose archived counterpart already
|
|
9766
|
-
* lives in `prd/archive/<name>.json` and the active `prd/<name>.json` is
|
|
9767
|
-
* absent. Pre-neutralize-era archives left these sidecars behind; without this
|
|
9768
|
-
* sweep, any caller that touched `prd/<name>.json` with the restore-enabled
|
|
9769
|
-
* `safeJson` would resurrect the archived PRD as active (W-mouptdh1000h9f39).
|
|
9770
|
-
*
|
|
9771
|
-
* Idempotent and best-effort: readdir / existsSync / unlink failures are
|
|
9772
|
-
* swallowed so a single weird filesystem state never blocks the discovery
|
|
9773
|
-
* tick. Returns the number of sidecars purged (for tests / logging).
|
|
9774
|
-
*/
|
|
9775
|
-
function sweepStaleArchivedPrdBackups(prdDir, prdArchiveDir) {
|
|
9776
|
-
let purged = 0;
|
|
9777
|
-
if (!fs.existsSync(prdArchiveDir)) return purged;
|
|
9778
|
-
let archivedNames;
|
|
9779
|
-
try { archivedNames = fs.readdirSync(prdArchiveDir).filter(f => f.endsWith('.json')); }
|
|
9780
|
-
catch { return purged; }
|
|
9781
|
-
for (const f of archivedNames) {
|
|
9782
|
-
const activePath = path.join(prdDir, f);
|
|
9783
|
-
const backupPath = activePath + '.backup';
|
|
9784
|
-
// Active PRD wins — the dedicated ghost-PRD purge in discoverWork handles
|
|
9785
|
-
// the case where both active.json and active.json.backup are present.
|
|
9786
|
-
if (fs.existsSync(activePath)) continue;
|
|
9787
|
-
if (!fs.existsSync(backupPath)) continue;
|
|
9788
|
-
try {
|
|
9789
|
-
fs.unlinkSync(backupPath);
|
|
9790
|
-
purged++;
|
|
9791
|
-
log('info', `Purged stale .backup sidecar for archived PRD: ${f}`);
|
|
9792
|
-
} catch { /* best-effort */ }
|
|
9793
|
-
}
|
|
9794
|
-
return purged;
|
|
9795
|
-
}
|
|
9796
|
-
|
|
9797
9722
|
/**
|
|
9798
9723
|
* Run all work discovery sources and queue new items
|
|
9799
9724
|
* Priority: fix (0) > ask (1) > review (1) > implement (2) > work-items (3) > central (4)
|
|
@@ -9803,7 +9728,7 @@ async function discoverWork(config) {
|
|
|
9803
9728
|
const projects = getProjects(config);
|
|
9804
9729
|
let persistedFixes = 0, persistedReviews = 0, persistedOther = 0;
|
|
9805
9730
|
|
|
9806
|
-
// Side-effect passes: materialize plans and design docs into work-
|
|
9731
|
+
// Side-effect passes: materialize plans and design docs into SQL work-item state.
|
|
9807
9732
|
// These write to project work queues — picked up by discoverFromWorkItems below.
|
|
9808
9733
|
// Gated by config.engine?.planMaterializationEnabled !== false (P-d6f0a2b5) —
|
|
9809
9734
|
// operators can suppress the PRD reconcile + plan materialization pair
|
|
@@ -9827,10 +9752,10 @@ async function discoverWork(config) {
|
|
|
9827
9752
|
// projects)` loop below leaves this whole async function permanently
|
|
9828
9753
|
// unresolved, so anything only collected into a local array and persisted
|
|
9829
9754
|
// at the very end (the pre-fix allFixes/allReviews/allWorkItems) never
|
|
9830
|
-
// reaches dispatch
|
|
9755
|
+
// reaches dispatch state — silently discarding valid work already
|
|
9831
9756
|
// discovered for unrelated, healthy projects processed earlier in the same
|
|
9832
9757
|
// loop. Persisting per batch means a healthy project's discoveries land in
|
|
9833
|
-
// dispatch
|
|
9758
|
+
// dispatch state immediately, before the loop reaches a slow project.
|
|
9834
9759
|
async function persistBatch(items) {
|
|
9835
9760
|
if (!items || items.length === 0) return;
|
|
9836
9761
|
// Gate reviews and fixes: only when at max concurrency — idle agents should pick up reviews
|
|
@@ -9855,7 +9780,7 @@ async function discoverWork(config) {
|
|
|
9855
9780
|
// type-only filter below would silently start gating those
|
|
9856
9781
|
// previously-ungated sources too. Scope the gate back down to
|
|
9857
9782
|
// `meta.source === 'pr'` to preserve the original behavior exactly.
|
|
9858
|
-
const isGateEligible = (w) => w.meta?.source === 'pr' && (w.type
|
|
9783
|
+
const isGateEligible = (w) => w.meta?.source === 'pr' && (shared.isFixLikeWorkType(w.type) || w.type === WORK_TYPE.REVIEW);
|
|
9859
9784
|
const gated = items.filter(isGateEligible);
|
|
9860
9785
|
if (gated.length > 0) {
|
|
9861
9786
|
log('info', `Gating ${gated.length} fixes/reviews — implement items in progress and no free slots`);
|
|
@@ -9883,7 +9808,7 @@ async function discoverWork(config) {
|
|
|
9883
9808
|
} catch (e) {
|
|
9884
9809
|
// Fail-open contract preserved: any validator throw here means the
|
|
9885
9810
|
// item is dropped from this discovery batch rather than wedging the
|
|
9886
|
-
// whole chunk. The item stays
|
|
9811
|
+
// whole chunk. The item stays pending in SQL and the
|
|
9887
9812
|
// next tick re-discovers it.
|
|
9888
9813
|
log('warn', `pre-dispatch-eval: addToDispatchWithValidation threw for ${item?.meta?.item?.id || item?.id || 'unknown'}: ${e?.message || e}`);
|
|
9889
9814
|
}
|
|
@@ -9897,7 +9822,7 @@ async function discoverWork(config) {
|
|
|
9897
9822
|
}));
|
|
9898
9823
|
}
|
|
9899
9824
|
for (const w of toDispatch) {
|
|
9900
|
-
if (w.type
|
|
9825
|
+
if (shared.isFixLikeWorkType(w.type)) persistedFixes++;
|
|
9901
9826
|
else if (w.type === WORK_TYPE.REVIEW) persistedReviews++;
|
|
9902
9827
|
else persistedOther++;
|
|
9903
9828
|
}
|
|
@@ -9916,7 +9841,7 @@ async function discoverWork(config) {
|
|
|
9916
9841
|
if (config.engine?.prDiscoveryEnabled !== false) {
|
|
9917
9842
|
const prWork = await discoverFromPrs(config, project);
|
|
9918
9843
|
projectWork.push(...prWork.filter(w =>
|
|
9919
|
-
w.type
|
|
9844
|
+
shared.isFixLikeWorkType(w.type) || w.type === WORK_TYPE.REVIEW || w.type === WORK_TYPE.TEST));
|
|
9920
9845
|
}
|
|
9921
9846
|
|
|
9922
9847
|
// Side-effect: specs → work items (picked up below)
|
|
@@ -9959,7 +9884,6 @@ async function discoverWork(config) {
|
|
|
9959
9884
|
const scheduledWork = discoverScheduledWork(config);
|
|
9960
9885
|
if (scheduledWork.length > 0) {
|
|
9961
9886
|
const { createMeeting, getMeetings } = require('./engine/meeting');
|
|
9962
|
-
const centralPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
9963
9887
|
// Separate meetings (no work-items write) from task items
|
|
9964
9888
|
const taskItems = [];
|
|
9965
9889
|
for (const item of scheduledWork) {
|
|
@@ -9973,8 +9897,7 @@ async function discoverWork(config) {
|
|
|
9973
9897
|
}
|
|
9974
9898
|
}
|
|
9975
9899
|
if (taskItems.length > 0) {
|
|
9976
|
-
|
|
9977
|
-
mutateJsonFileLocked(centralPath, (items) => {
|
|
9900
|
+
mutateWorkItems('central', (items) => {
|
|
9978
9901
|
if (!Array.isArray(items)) items = [];
|
|
9979
9902
|
let added = 0;
|
|
9980
9903
|
// Snapshot active dedup keys BEFORE the loop so multiple items in the
|
|
@@ -10010,7 +9933,7 @@ async function discoverWork(config) {
|
|
|
10010
9933
|
log('info', `Scheduled task fired: ${item._scheduleId} → ${item.title}`);
|
|
10011
9934
|
}
|
|
10012
9935
|
return items;
|
|
10013
|
-
}
|
|
9936
|
+
});
|
|
10014
9937
|
}
|
|
10015
9938
|
}
|
|
10016
9939
|
} catch (e) { log('warn', 'discover scheduled work: ' + e.message); }
|
|
@@ -10036,51 +9959,19 @@ async function discoverWork(config) {
|
|
|
10036
9959
|
if (tickCount % (ENGINE_DEFAULTS.planCompletionScanEvery || 60) === 0) {
|
|
10037
9960
|
try {
|
|
10038
9961
|
const lifecycle = require('./engine/lifecycle');
|
|
10039
|
-
const
|
|
10040
|
-
|
|
10041
|
-
|
|
10042
|
-
|
|
10043
|
-
|
|
10044
|
-
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
// Pass 2 — completion sweep. (The ghost-PRD purge that used to live here
|
|
10050
|
-
// was a REDUNDANT + UNSAFE duplicate of the plan-completion scan's
|
|
10051
|
-
// version: it unlinked prd/<f> whenever prd/archive/<f> existed, with NO
|
|
10052
|
-
// `_isGhostPrdRestore` identity check — the exact footgun #7 (#496)
|
|
10053
|
-
// hazard of deleting a legitimately re-opened PRD that shares an archived
|
|
10054
|
-
// basename. The plan-completion scan does the same purge safely (identity-
|
|
10055
|
-
// checked + neutralize); here the isDefunctPrd guard below already skips
|
|
10056
|
-
// archived/defunct PRDs, so this loop never re-runs completion on a ghost.)
|
|
10057
|
-
for (const f of prdStore.listPrdRows().filter(row => !row.archived).map(row => row.filename)) {
|
|
10058
|
-
if (completedPlanCache.has(f)) continue;
|
|
10059
|
-
// safeJsonNoRestore — defense in depth: if the file vanished between
|
|
10060
|
-
// readdir and read (e.g. concurrent archive), do not resurrect it
|
|
10061
|
-
// from a stale .backup sidecar (W-mouptdh1000h9f39).
|
|
10062
|
-
const plan = safeJsonNoRestore(path.join(prdDir, f));
|
|
10063
|
-
if (!plan?.missing_features) continue;
|
|
10064
|
-
// Resurrection guard (mirror of completion-scan-B / #523): never re-run
|
|
10065
|
-
// completion (which re-creates the verify gate) for a defunct PRD —
|
|
10066
|
-
// archived, or completed with its plan gone from plans/.
|
|
10067
|
-
if (shared.isDefunctPrd(plan, PLANS_DIR)) { completedPlanCache.add(f); continue; }
|
|
10068
|
-
// A completed PRD may still be missing its aggregate verify WI — e.g. it
|
|
10069
|
-
// landed on disk pre-completed via the plan-to-prd / pipeline path, so no
|
|
10070
|
-
// agent-completion event ever drove checkPlanCompletion (W-mqk5ld3p). The
|
|
10071
|
-
// function is idempotent (re-checks for an existing verify WI under lock),
|
|
10072
|
-
// so run it for completed plans too and cache only once it reports nothing
|
|
10073
|
-
// left to do (truthy return).
|
|
10074
|
-
if (plan.status === PLAN_STATUS.COMPLETED) {
|
|
10075
|
-
const done = lifecycle.checkPlanCompletion({ item: { sourcePlan: f } }, config);
|
|
10076
|
-
if (done) completedPlanCache.add(f);
|
|
10077
|
-
continue;
|
|
10078
|
-
}
|
|
10079
|
-
if (plan.status !== PLAN_STATUS.APPROVED && plan.status !== PLAN_STATUS.ACTIVE) continue;
|
|
10080
|
-
// Simulate the meta object checkPlanCompletion expects
|
|
10081
|
-
const completed = lifecycle.checkPlanCompletion({ item: { sourcePlan: f } }, config);
|
|
10082
|
-
if (completed) completedPlanCache.add(f);
|
|
9962
|
+
for (const f of prdStore.listPrdRows().filter(row => !row.archived).map(row => row.filename)) {
|
|
9963
|
+
if (completedPlanCache.has(f)) continue;
|
|
9964
|
+
const plan = prdStore.readPrd(f);
|
|
9965
|
+
if (!plan?.missing_features) continue;
|
|
9966
|
+
if (shared.isDefunctPrd(plan, PLANS_DIR)) { completedPlanCache.add(f); continue; }
|
|
9967
|
+
if (plan.status === PLAN_STATUS.COMPLETED) {
|
|
9968
|
+
const done = lifecycle.checkPlanCompletion({ item: { sourcePlan: f } }, config);
|
|
9969
|
+
if (done) completedPlanCache.add(f);
|
|
9970
|
+
continue;
|
|
10083
9971
|
}
|
|
9972
|
+
if (plan.status !== PLAN_STATUS.APPROVED && plan.status !== PLAN_STATUS.ACTIVE) continue;
|
|
9973
|
+
const completed = lifecycle.checkPlanCompletion({ item: { sourcePlan: f } }, config);
|
|
9974
|
+
if (completed) completedPlanCache.add(f);
|
|
10084
9975
|
}
|
|
10085
9976
|
} catch (e) { log('warn', 'plan completion sweep: ' + e.message); }
|
|
10086
9977
|
}
|
|
@@ -10170,7 +10061,7 @@ function persistPendingDispatchAgent(item) {
|
|
|
10170
10061
|
}
|
|
10171
10062
|
|
|
10172
10063
|
function isSoftFixDispatch(item) {
|
|
10173
|
-
return item?.type
|
|
10064
|
+
return shared.isFixLikeWorkType(item?.type) && !routing.isAgentHardPinned(item.meta?.item);
|
|
10174
10065
|
}
|
|
10175
10066
|
|
|
10176
10067
|
function resolveMaxConcurrent(config) {
|
|
@@ -10204,26 +10095,6 @@ const completedPlanCache = new Set();
|
|
|
10204
10095
|
// Dedupe the "Skipping defunct PRD" materializer log so a lingering defunct PRD
|
|
10205
10096
|
// doesn't spam one line per discovery tick; cleared when the file stops being defunct.
|
|
10206
10097
|
const _defunctPrdSkipLogged = new Set();
|
|
10207
|
-
// Filenames where a live PRD shares a basename with an archived PRD but diverges
|
|
10208
|
-
// (different source_plan / introduces new work). We refuse to auto-purge those
|
|
10209
|
-
// and warn once instead of every tick. (RC4 — footgun #7 collision protection.)
|
|
10210
|
-
const _ghostPurgeCollisionWarned = new Set();
|
|
10211
|
-
|
|
10212
|
-
// True when a live PRD that shares a basename with an archived one is merely a
|
|
10213
|
-
// stale echo of the archive (a .backup ghost-restore), safe to purge. False when
|
|
10214
|
-
// it's a genuinely distinct re-opened PRD that must NOT be silently deleted.
|
|
10215
|
-
// Conservative: if either file is unreadable we fall back to the legacy
|
|
10216
|
-
// "purge the ghost" behavior so we don't regress resurrection cleanup.
|
|
10217
|
-
function _isGhostPrdRestore(live, archived) {
|
|
10218
|
-
if (!live || !archived) return true;
|
|
10219
|
-
const ls = live.source_plan || live.sourcePlan;
|
|
10220
|
-
const as = archived.source_plan || archived.sourcePlan;
|
|
10221
|
-
if (ls && as && ls !== as) return false; // different plan identity → real PRD
|
|
10222
|
-
const archivedIds = new Set((archived.missing_features || []).map(f => f && f.id).filter(Boolean));
|
|
10223
|
-
const hasNewWork = (live.missing_features || []).some(f => f && f.id && !archivedIds.has(f.id));
|
|
10224
|
-
if (hasNewWork) return false; // introduces work the archive never had → real PRD
|
|
10225
|
-
return true; // same identity, no new work → stale echo → ghost
|
|
10226
|
-
}
|
|
10227
10098
|
let lastWatchCheckAt = 0;
|
|
10228
10099
|
let lastPrStatusPollAt = 0;
|
|
10229
10100
|
let lastPrCommentsPollAt = 0;
|
|
@@ -10451,7 +10322,7 @@ async function tickInner() {
|
|
|
10451
10322
|
}
|
|
10452
10323
|
|
|
10453
10324
|
// 2.53. managed-spawn TTL/dead-PID sweep + log rotation (P-8a4d6f29). Walks
|
|
10454
|
-
//
|
|
10325
|
+
// SQL managed-process state, kills TTL-expired specs, drops dead-PID
|
|
10455
10326
|
// rows, rotates managed-logs/<name>.log past ENGINE_DEFAULTS.managedSpawn
|
|
10456
10327
|
// .logRotateBytes. Mirrors the keep-processes sweep cadence (sweepEvery=180)
|
|
10457
10328
|
// so the engine never iterates per-spec on every tick. Healthcheck loops
|
|
@@ -10498,16 +10369,12 @@ async function tickInner() {
|
|
|
10498
10369
|
safe('checkWatches', () => {
|
|
10499
10370
|
const { checkWatches } = require('./engine/watches');
|
|
10500
10371
|
const projects = getProjects(config);
|
|
10501
|
-
const
|
|
10502
|
-
|
|
10503
|
-
|
|
10504
|
-
|
|
10505
|
-
const workItems = projects.flatMap(p => {
|
|
10506
|
-
const wiPath = path.join(MINIONS_DIR, 'projects', p.name, 'work-items.json');
|
|
10507
|
-
return safeJsonArr(wiPath);
|
|
10508
|
-
});
|
|
10372
|
+
const pullRequestStore = require('./engine/pull-requests-store');
|
|
10373
|
+
const workItemStore = require('./engine/work-items-store');
|
|
10374
|
+
const pullRequests = projects.flatMap(p => pullRequestStore.readPullRequestsForScope(p.name));
|
|
10375
|
+
const workItems = projects.flatMap(p => workItemStore.readWorkItemsForScope(p.name));
|
|
10509
10376
|
// Also include central work items
|
|
10510
|
-
const centralWi =
|
|
10377
|
+
const centralWi = workItemStore.readWorkItemsForScope('central');
|
|
10511
10378
|
|
|
10512
10379
|
// Gather state for the new generalized target types. Each block is
|
|
10513
10380
|
// best-effort — if a module/file is missing the watch evaluator will
|
|
@@ -10515,23 +10382,11 @@ async function tickInner() {
|
|
|
10515
10382
|
let meetings = [];
|
|
10516
10383
|
try { meetings = require('./engine/meeting').getMeetings(); } catch { /* optional */ }
|
|
10517
10384
|
|
|
10518
|
-
|
|
10519
|
-
|
|
10520
|
-
// carries its filename in _source for target lookup.
|
|
10521
|
-
const plans = [];
|
|
10522
|
-
try {
|
|
10523
|
-
const prdDir = path.join(MINIONS_DIR, 'prd');
|
|
10524
|
-
for (const dir of [prdDir, path.join(prdDir, 'archive')]) {
|
|
10525
|
-
if (!fs.existsSync(dir)) continue;
|
|
10526
|
-
for (const f of fs.readdirSync(dir).filter(x => x.endsWith('.json'))) {
|
|
10527
|
-
const plan = safeJson(path.join(dir, f));
|
|
10528
|
-
if (plan) plans.push({ ...plan, _source: f, _sourcePlan: plan.source_plan || '' });
|
|
10529
|
-
}
|
|
10530
|
-
}
|
|
10531
|
-
} catch { /* optional */ }
|
|
10385
|
+
const plans = require('./engine/prd-store').listPrdRows()
|
|
10386
|
+
.map(({ filename, plan }) => ({ ...plan, _source: filename, _sourcePlan: plan.source_plan || '' }));
|
|
10532
10387
|
|
|
10533
10388
|
let scheduleRuns = {};
|
|
10534
|
-
try { scheduleRuns =
|
|
10389
|
+
try { scheduleRuns = require('./engine/small-state-store').readScheduleRuns(); } catch { /* optional */ }
|
|
10535
10390
|
|
|
10536
10391
|
let pipelineRuns = {};
|
|
10537
10392
|
try { pipelineRuns = require('./engine/pipeline').getPipelineRuns(); } catch { /* optional */ }
|
|
@@ -10631,29 +10486,7 @@ async function tickInner() {
|
|
|
10631
10486
|
const prdFiles = prdStore.listPrdRows().filter(row => !row.archived).map(row => row.filename);
|
|
10632
10487
|
for (const file of prdFiles) {
|
|
10633
10488
|
if (completedPlanCache.has(file)) continue;
|
|
10634
|
-
|
|
10635
|
-
// A live PRD basename also exists in the archive. Historically this was
|
|
10636
|
-
// unconditionally treated as an orphaned .backup ghost-restore and purged
|
|
10637
|
-
// — but a re-opened plan can legitimately create a NEW live PRD sharing
|
|
10638
|
-
// an archived basename (footgun #7). Identity-check before deleting so we
|
|
10639
|
-
// never silently destroy a real divergent PRD.
|
|
10640
|
-
const liveP = safeJsonNoRestore(path.join(PRD_DIR, file));
|
|
10641
|
-
const archP = safeJsonNoRestore(path.join(PRD_DIR, 'archive', file));
|
|
10642
|
-
if (_isGhostPrdRestore(liveP, archP)) {
|
|
10643
|
-
// Orphaned backup restore — plan is already archived. Purge the ghost copy.
|
|
10644
|
-
try { fs.unlinkSync(path.join(PRD_DIR, file)); } catch { }
|
|
10645
|
-
shared.neutralizeJsonBackupSidecar(path.join(PRD_DIR, file));
|
|
10646
|
-
completedPlanCache.add(file);
|
|
10647
|
-
continue;
|
|
10648
|
-
}
|
|
10649
|
-
// Divergent live PRD — do NOT delete. Warn once and fall through to the
|
|
10650
|
-
// normal completion handling below so it's treated as a real PRD.
|
|
10651
|
-
if (!_ghostPurgeCollisionWarned.has(file)) {
|
|
10652
|
-
log('warn', `PRD ${file} shares a basename with an archived PRD but diverges (different source_plan or new work) — NOT purging; resolve the collision manually`);
|
|
10653
|
-
_ghostPurgeCollisionWarned.add(file);
|
|
10654
|
-
}
|
|
10655
|
-
}
|
|
10656
|
-
const plan = safeJson(path.join(PRD_DIR, file));
|
|
10489
|
+
const plan = prdStore.readPrd(file);
|
|
10657
10490
|
// Resurrection-re-execution guard: a defunct PRD (archived, or completed
|
|
10658
10491
|
// with its source plan gone from plans/) must NOT have its verify gate
|
|
10659
10492
|
// re-created. This scan deliberately re-runs completion for completed
|
|
@@ -10767,13 +10600,12 @@ async function tickInner() {
|
|
|
10767
10600
|
// Check for failed items blocking pending items
|
|
10768
10601
|
for (const project of projects) {
|
|
10769
10602
|
try {
|
|
10770
|
-
const wiPath = projectWorkItemsPath(project);
|
|
10771
10603
|
// Collect keys to clear AFTER work-items lock is released (avoid nested locks)
|
|
10772
10604
|
const dispatchKeysToClear = [];
|
|
10773
10605
|
const cooldownKeysToClear = [];
|
|
10774
10606
|
|
|
10775
10607
|
if (_isTickStale(myGeneration)) return;
|
|
10776
|
-
mutateWorkItems(
|
|
10608
|
+
mutateWorkItems(project, items => {
|
|
10777
10609
|
let changed = false;
|
|
10778
10610
|
const failedIds = new Set(items.filter(w => w.status === WI_STATUS.FAILED).map(w => w.id));
|
|
10779
10611
|
const pendingWithBlockedDeps = items.filter(w =>
|
|
@@ -11036,7 +10868,7 @@ async function tickInner() {
|
|
|
11036
10868
|
assignPendingDispatchAgent(item, altAgent, config);
|
|
11037
10869
|
pendingAgents.add(altAgent);
|
|
11038
10870
|
log('info', `Reassigning ${item.id} from unspawned temp ${prevAgent} to ${altAgent} — temp agent never spawned`);
|
|
11039
|
-
// Persist reassignment
|
|
10871
|
+
// Persist reassignment so it survives restarts/ticks.
|
|
11040
10872
|
persistPendingDispatchAgent(item);
|
|
11041
10873
|
}
|
|
11042
10874
|
}
|
|
@@ -11136,7 +10968,15 @@ async function tickInner() {
|
|
|
11136
10968
|
skipDirtyCheck: !!_precheckProjCfg.skipLiveCheckoutDirtyCheck,
|
|
11137
10969
|
});
|
|
11138
10970
|
} catch (e) { log('warn', `live-checkout precheck (dirty) failed for ${item.id}: ${e.message}`); }
|
|
11139
|
-
|
|
10971
|
+
const _autoStashEnabled = _liveCheckoutPrecheck.resolveLiveCheckoutAutoStash({
|
|
10972
|
+
project: _precheckProjCfg,
|
|
10973
|
+
engine: config.engine || {},
|
|
10974
|
+
});
|
|
10975
|
+
const _autoResetEnabled = shared.resolveLiveCheckoutAutoReset(
|
|
10976
|
+
_precheckProjCfg,
|
|
10977
|
+
config.engine || {},
|
|
10978
|
+
);
|
|
10979
|
+
if (_dirtyResult && _dirtyResult.dirty && !_autoStashEnabled && !_autoResetEnabled) {
|
|
11140
10980
|
_persistPendingReason('live_checkout_dirty');
|
|
11141
10981
|
try {
|
|
11142
10982
|
const _alertBody = [
|
|
@@ -11272,12 +11112,10 @@ async function tickInner() {
|
|
|
11272
11112
|
// Defensive: ensure the work item is re-queued if completeDispatch didn't fire
|
|
11273
11113
|
if (item.meta?.item?.id) {
|
|
11274
11114
|
try {
|
|
11275
|
-
const
|
|
11276
|
-
|
|
11277
|
-
: item.meta.project?.name ? projectWorkItemsPath({ name: item.meta.project.name, localPath: item.meta.project.localPath }) : null;
|
|
11278
|
-
if (wiPath) {
|
|
11115
|
+
const wiScope = resolveWorkItemScope(item.meta);
|
|
11116
|
+
if (wiScope) {
|
|
11279
11117
|
if (_isTickStale(myGeneration)) return;
|
|
11280
|
-
mutateWorkItems(
|
|
11118
|
+
mutateWorkItems(wiScope, items => {
|
|
11281
11119
|
const wi = items.find(i => i.id === item.meta.item.id);
|
|
11282
11120
|
if (wi && wi.status === WI_STATUS.DISPATCHED) {
|
|
11283
11121
|
// completeDispatch didn't update the work item — re-queue manually
|
|
@@ -11324,11 +11162,12 @@ async function tickInner() {
|
|
|
11324
11162
|
// post-dispatch active list so the annotation loop's reason is consistent
|
|
11325
11163
|
// with whatever actually got spawned this tick.
|
|
11326
11164
|
const postLiveProjectsInUse = new Set();
|
|
11165
|
+
const postProjects = shared.getProjects(config);
|
|
11327
11166
|
for (const d of (postDispatch.active || [])) {
|
|
11328
11167
|
if (READ_ONLY_ROOT_TASK_TYPES.has(d.type)) continue;
|
|
11329
11168
|
const projName = d.project || d.meta?.project?.name || null;
|
|
11330
11169
|
if (!projName) continue;
|
|
11331
|
-
const projCfg = shared.findProjectByName(
|
|
11170
|
+
const projCfg = shared.findProjectByName(postProjects, projName);
|
|
11332
11171
|
if (shared.resolveCheckoutMode(projCfg, d.type) === 'live') {
|
|
11333
11172
|
postLiveProjectsInUse.add(projName);
|
|
11334
11173
|
}
|
|
@@ -11355,7 +11194,10 @@ async function tickInner() {
|
|
|
11355
11194
|
&& !READ_ONLY_ROOT_TASK_TYPES.has(item.type)
|
|
11356
11195
|
&& postLiveProjectsInUse.has(itemProjName)
|
|
11357
11196
|
) {
|
|
11358
|
-
|
|
11197
|
+
const projCfg = shared.findProjectByName(postProjects, itemProjName);
|
|
11198
|
+
if (shared.resolveCheckoutMode(projCfg, item.type) === 'live') {
|
|
11199
|
+
reason = 'live_checkout_busy';
|
|
11200
|
+
}
|
|
11359
11201
|
}
|
|
11360
11202
|
}
|
|
11361
11203
|
}
|
|
@@ -11400,8 +11242,8 @@ async function tickInner() {
|
|
|
11400
11242
|
// clobbered. Previously the mutator callback assigned dp.pending from the
|
|
11401
11243
|
// pre-lock postDispatch snapshot, overwriting any freshly loaded queue
|
|
11402
11244
|
// entries. Sentinel-undefined for _pendingReason / _agentBusySince
|
|
11403
|
-
// mirrors the in-loop `delete` semantics —
|
|
11404
|
-
// undefined-valued keys
|
|
11245
|
+
// mirrors the in-loop `delete` semantics — the SQL store serializes
|
|
11246
|
+
// records without undefined-valued keys.
|
|
11405
11247
|
const patches = new Map();
|
|
11406
11248
|
for (const item of (postDispatch.pending || [])) {
|
|
11407
11249
|
patches.set(item.id, {
|
|
@@ -11517,7 +11359,7 @@ function emitMemoryBaseline(tickN) {
|
|
|
11517
11359
|
module.exports = {
|
|
11518
11360
|
// Paths
|
|
11519
11361
|
MINIONS_DIR, ENGINE_DIR, AGENTS_DIR, PLAYBOOKS_DIR, PLANS_DIR, PRD_DIR,
|
|
11520
|
-
CONTROL_PATH,
|
|
11362
|
+
CONTROL_PATH, INBOX_DIR, KNOWLEDGE_DIR, ARCHIVE_DIR,
|
|
11521
11363
|
IDENTITY_DIR, CONFIG_PATH, ROUTING_PATH, NOTES_PATH, SKILLS_DIR,
|
|
11522
11364
|
|
|
11523
11365
|
// Utilities
|
|
@@ -11542,10 +11384,7 @@ module.exports = {
|
|
|
11542
11384
|
discoverWork, discoverFromPrs, discoverFromWorkItems, discoverCentralWorkItems,
|
|
11543
11385
|
materializePlansAsWorkItems,
|
|
11544
11386
|
materializeSpecsAsWorkItems, // exported for testing (P-f7-git-log)
|
|
11545
|
-
reservePrdFilename, // exported for testing (P-9b7e5d3c)
|
|
11546
11387
|
restoreHijackedOperatorCheckouts, // exported for testing — operator-checkout self-heal
|
|
11547
|
-
sweepStaleArchivedPrdBackups, // exported for testing
|
|
11548
|
-
_isGhostPrdRestore, // exported for testing (RC4 — ghost-purge identity check)
|
|
11549
11388
|
|
|
11550
11389
|
// Shared helpers (used by lifecycle.js and tests)
|
|
11551
11390
|
reconcileItemsWithPrs, detectDependencyCycles,
|