@yemi33/minions 0.1.2379 → 0.1.2380
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 +2 -3
- package/dashboard/js/render-prd.js +1 -1
- package/dashboard/js/render-work-items.js +13 -6
- package/dashboard.js +393 -721
- 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/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 +34 -41
- package/engine/github.js +20 -27
- package/engine/lifecycle.js +221 -283
- 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 +1 -1
- 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 +3 -5
- package/engine/scheduler.js +0 -4
- package/engine/shared-branch-pr-reconcile.js +2 -3
- package/engine/shared.js +132 -637
- 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 +147 -343
- 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 +2 -2
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;
|
|
@@ -2821,9 +2817,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2821
2817
|
try { writeInboxAlert(`live-checkout-dirty-${_wiIdForAlert}`, _alertBody); }
|
|
2822
2818
|
catch (e) { log('warn', `live-checkout: writeInboxAlert failed: ${e.message}`); }
|
|
2823
2819
|
try {
|
|
2824
|
-
const
|
|
2825
|
-
if (
|
|
2826
|
-
|
|
2820
|
+
const _wiScope = resolveWorkItemScope(dispatchItem.meta);
|
|
2821
|
+
if (_wiScope && dispatchItem.meta?.item?.id) {
|
|
2822
|
+
mutateWorkItems(_wiScope, (data) => {
|
|
2827
2823
|
if (!Array.isArray(data)) return data;
|
|
2828
2824
|
const wi = data.find(i => i && i.id === dispatchItem.meta.item.id);
|
|
2829
2825
|
if (wi) {
|
|
@@ -2892,9 +2888,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2892
2888
|
catch (e) { log('warn', `live-checkout: writeInboxAlert failed: ${e.message}`); }
|
|
2893
2889
|
const _pendingReason = _isMidOp ? 'live_checkout_mid_operation' : 'live_checkout_detached_head';
|
|
2894
2890
|
try {
|
|
2895
|
-
const
|
|
2896
|
-
if (
|
|
2897
|
-
|
|
2891
|
+
const _wiScope = resolveWorkItemScope(dispatchItem.meta);
|
|
2892
|
+
if (_wiScope && dispatchItem.meta?.item?.id) {
|
|
2893
|
+
mutateWorkItems(_wiScope, (data) => {
|
|
2898
2894
|
if (!Array.isArray(data)) return data;
|
|
2899
2895
|
const wi = data.find(i => i && i.id === dispatchItem.meta.item.id);
|
|
2900
2896
|
if (wi) wi._pendingReason = _pendingReason;
|
|
@@ -2966,9 +2962,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2966
2962
|
try { writeInboxAlert(`live-checkout-blocked-${_wiIdForAlert}`, _alertBody); }
|
|
2967
2963
|
catch (e) { log('warn', `live-checkout: writeInboxAlert failed: ${e.message}`); }
|
|
2968
2964
|
try {
|
|
2969
|
-
const
|
|
2970
|
-
if (
|
|
2971
|
-
|
|
2965
|
+
const _wiScope = resolveWorkItemScope(dispatchItem.meta);
|
|
2966
|
+
if (_wiScope && dispatchItem.meta?.item?.id) {
|
|
2967
|
+
mutateWorkItems(_wiScope, (data) => {
|
|
2972
2968
|
if (!Array.isArray(data)) return data;
|
|
2973
2969
|
const wi = data.find(i => i && i.id === dispatchItem.meta.item.id);
|
|
2974
2970
|
if (wi) wi._pendingReason = 'live_checkout_wrong_base';
|
|
@@ -3025,9 +3021,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3025
3021
|
try { writeInboxAlert(`live-checkout-blocked-${_wiIdForAlert}`, _alertBody); }
|
|
3026
3022
|
catch (e) { log('warn', `live-checkout: writeInboxAlert failed: ${e.message}`); }
|
|
3027
3023
|
try {
|
|
3028
|
-
const
|
|
3029
|
-
if (
|
|
3030
|
-
|
|
3024
|
+
const _wiScope = resolveWorkItemScope(dispatchItem.meta);
|
|
3025
|
+
if (_wiScope && dispatchItem.meta?.item?.id) {
|
|
3026
|
+
mutateWorkItems(_wiScope, (data) => {
|
|
3031
3027
|
if (!Array.isArray(data)) return data;
|
|
3032
3028
|
const wi = data.find(i => i && i.id === dispatchItem.meta.item.id);
|
|
3033
3029
|
if (wi) wi._pendingReason = 'live_checkout_blob_fetch';
|
|
@@ -3129,9 +3125,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3129
3125
|
try { writeInboxAlert(`live-checkout-worktree-conflict-${_wiIdForAlert}`, _alertBody); }
|
|
3130
3126
|
catch (e) { log('warn', `live-checkout: writeInboxAlert failed: ${e.message}`); }
|
|
3131
3127
|
try {
|
|
3132
|
-
const
|
|
3133
|
-
if (
|
|
3134
|
-
|
|
3128
|
+
const _wiScope = resolveWorkItemScope(dispatchItem.meta);
|
|
3129
|
+
if (_wiScope && dispatchItem.meta?.item?.id) {
|
|
3130
|
+
mutateWorkItems(_wiScope, (data) => {
|
|
3135
3131
|
if (!Array.isArray(data)) return data;
|
|
3136
3132
|
const wi = data.find(i => i && i.id === dispatchItem.meta.item.id);
|
|
3137
3133
|
if (wi) wi._pendingReason = 'live_checkout_worktree_conflict';
|
|
@@ -3215,9 +3211,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3215
3211
|
try { writeInboxAlert(`live-checkout-stale-base-${_wiIdForAlert}`, _alertBody); }
|
|
3216
3212
|
catch (e) { log('warn', `live-checkout: writeInboxAlert failed: ${e.message}`); }
|
|
3217
3213
|
try {
|
|
3218
|
-
const
|
|
3219
|
-
if (
|
|
3220
|
-
|
|
3214
|
+
const _wiScope = resolveWorkItemScope(dispatchItem.meta);
|
|
3215
|
+
if (_wiScope && dispatchItem.meta?.item?.id) {
|
|
3216
|
+
mutateWorkItems(_wiScope, (data) => {
|
|
3221
3217
|
if (!Array.isArray(data)) return data;
|
|
3222
3218
|
const wi = data.find(i => i && i.id === dispatchItem.meta.item.id);
|
|
3223
3219
|
if (wi) wi._pendingReason = 'live_checkout_stale_base';
|
|
@@ -3244,9 +3240,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3244
3240
|
// second-strike on its next dirty encounter. Only write when the stamp is set.
|
|
3245
3241
|
if (dispatchItem.meta?.item?._liveCheckoutDirtyAttempts) {
|
|
3246
3242
|
try {
|
|
3247
|
-
const
|
|
3248
|
-
if (
|
|
3249
|
-
|
|
3243
|
+
const _wiScope = resolveWorkItemScope(dispatchItem.meta);
|
|
3244
|
+
if (_wiScope && dispatchItem.meta?.item?.id) {
|
|
3245
|
+
mutateWorkItems(_wiScope, (data) => {
|
|
3250
3246
|
if (!Array.isArray(data)) return data;
|
|
3251
3247
|
const wi = data.find(i => i && i.id === dispatchItem.meta.item.id);
|
|
3252
3248
|
if (wi) delete wi._liveCheckoutDirtyAttempts;
|
|
@@ -3905,7 +3901,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3905
3901
|
// Fetch all dependency branches in parallel (git fetches are independent)
|
|
3906
3902
|
const fetchable = depBranches.filter(d => !_failedRefCache.has(d.branch));
|
|
3907
3903
|
const unfetchable = depBranches.filter(d => _failedRefCache.has(d.branch));
|
|
3908
|
-
const allPrsForDeps = unfetchable.length > 0 ? shared.getProjects(config).reduce((acc, p) => acc.concat(
|
|
3904
|
+
const allPrsForDeps = unfetchable.length > 0 ? shared.getProjects(config).reduce((acc, p) => acc.concat(readPullRequests(p)), []) : [];
|
|
3909
3905
|
for (const { branch: depBranch, prId } of unfetchable) {
|
|
3910
3906
|
const pr = allPrsForDeps.find(p => p.id === prId);
|
|
3911
3907
|
if (pr && (pr.status === PR_STATUS.MERGED || pr.status === PR_STATUS.CLOSED)) {
|
|
@@ -3938,7 +3934,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3938
3934
|
})
|
|
3939
3935
|
);
|
|
3940
3936
|
const hasFetchFailures = fetchResults.some(r => r.status === 'rejected');
|
|
3941
|
-
const allPrsForFetch = hasFetchFailures ? shared.getProjects(config).reduce((acc, p) => acc.concat(
|
|
3937
|
+
const allPrsForFetch = hasFetchFailures ? shared.getProjects(config).reduce((acc, p) => acc.concat(readPullRequests(p)), []) : [];
|
|
3942
3938
|
// Track branches recovered by local-only push so they can be merged
|
|
3943
3939
|
const recoveredBranches = new Set();
|
|
3944
3940
|
for (let i = 0; i < fetchResults.length; i++) {
|
|
@@ -4291,9 +4287,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
4291
4287
|
// so commits land on the dep's existing PR branch (W-mpcwojgr000a0244).
|
|
4292
4288
|
if (depConflictBranch && meta?.item?.id && project) {
|
|
4293
4289
|
try {
|
|
4294
|
-
const
|
|
4295
|
-
? projectWorkItemsPath(project)
|
|
4296
|
-
: path.join(MINIONS_DIR, 'work-items.json');
|
|
4290
|
+
const wiScope = project.name ? project : 'central';
|
|
4297
4291
|
const newItem = buildDepConflictFixItem({
|
|
4298
4292
|
depConflictBranch,
|
|
4299
4293
|
depConflictFiles,
|
|
@@ -4303,7 +4297,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
4303
4297
|
blockedItem: meta.item,
|
|
4304
4298
|
projectName: project.name || null,
|
|
4305
4299
|
});
|
|
4306
|
-
mutateWorkItems(
|
|
4300
|
+
mutateWorkItems(wiScope, items => {
|
|
4307
4301
|
// Don't create duplicate conflict-fix items
|
|
4308
4302
|
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
4303
|
if (existing) return;
|
|
@@ -4538,7 +4532,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
4538
4532
|
log('info', `Spawning agent: ${agentId} (${id}) in ${cwd}`);
|
|
4539
4533
|
log('info', `Task type: ${type} | Branch: ${branchName || 'none'}`);
|
|
4540
4534
|
|
|
4541
|
-
// Agent status is derived from dispatch
|
|
4535
|
+
// Agent status is derived from dispatch state.
|
|
4542
4536
|
|
|
4543
4537
|
// Spawn the claude process
|
|
4544
4538
|
const childEnv = shared.cleanChildEnv();
|
|
@@ -5412,7 +5406,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
5412
5406
|
// agent described in its sidecar. This gate (a) rejects malformed
|
|
5413
5407
|
// sidecars as a hard non-retryable failure with a dedicated failure
|
|
5414
5408
|
// class + inbox alert, and (b) on success spawns each spec detached and
|
|
5415
|
-
// batch-records them in
|
|
5409
|
+
// batch-records them in SQL managed-process state. Healthcheck loops
|
|
5416
5410
|
// + dispatch ERROR-on-healthcheck-failure land in the follow-up item;
|
|
5417
5411
|
// for now a spec that spawns successfully is recorded with
|
|
5418
5412
|
// healthy:false, alive:true and the engine sweep / item-3 healthcheck
|
|
@@ -5911,9 +5905,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
5911
5905
|
// engine/managed-logs/.
|
|
5912
5906
|
if (managedSpawnHealthcheckFailure && managedSpawnHealthcheckFailure.annotation && dispatchItem.meta?.item?.id) {
|
|
5913
5907
|
try {
|
|
5914
|
-
const
|
|
5915
|
-
if (
|
|
5916
|
-
mutateWorkItems(
|
|
5908
|
+
const wiScope = resolveWorkItemScope(dispatchItem.meta);
|
|
5909
|
+
if (wiScope) {
|
|
5910
|
+
mutateWorkItems(wiScope, items => {
|
|
5917
5911
|
const wi = items.find(i => i.id === dispatchItem.meta.item.id);
|
|
5918
5912
|
if (!wi) return items;
|
|
5919
5913
|
wi._managedSpawnPartial = managedSpawnHealthcheckFailure.annotation;
|
|
@@ -5930,9 +5924,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
5930
5924
|
// rather than a queue gate.
|
|
5931
5925
|
if (keepProcessesAcceptanceFailure && dispatchItem.meta?.item?.id) {
|
|
5932
5926
|
try {
|
|
5933
|
-
const
|
|
5934
|
-
if (
|
|
5935
|
-
|
|
5927
|
+
const wiScope = resolveWorkItemScope(dispatchItem.meta);
|
|
5928
|
+
if (wiScope) {
|
|
5929
|
+
mutateWorkItems(wiScope, data => {
|
|
5936
5930
|
if (!Array.isArray(data)) return data;
|
|
5937
5931
|
const wi = data.find(i => i.id === dispatchItem.meta.item.id);
|
|
5938
5932
|
if (!wi) return data;
|
|
@@ -5949,7 +5943,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
5949
5943
|
// Cleanup temp files (whole per-dispatch dir, including PID/sysprompt
|
|
5950
5944
|
// tmp/prompt — P-f6-tmp-toctou). removeDispatchTmpDir validates the path
|
|
5951
5945
|
// resolves under engine/tmp/dispatch-* before rmSync, so a corrupted
|
|
5952
|
-
// dispatch
|
|
5946
|
+
// dispatch field cannot redirect this to an arbitrary path.
|
|
5953
5947
|
try { shared.removeDispatchTmpDir(dispatchTmpDir); } catch { /* cleanup */ }
|
|
5954
5948
|
|
|
5955
5949
|
log('info', `Agent ${agentId} completed. Output saved to ${archivePath}`);
|
|
@@ -6051,7 +6045,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
6051
6045
|
item.started_at = startedAt;
|
|
6052
6046
|
// Persist the resolved runtime so post-completion hooks (lifecycle.js) can
|
|
6053
6047
|
// route output parsing through the right adapter. Also surfaces the choice
|
|
6054
|
-
// in dispatch
|
|
6048
|
+
// in dispatch state for debugging multi-runtime fleets.
|
|
6055
6049
|
item.runtimeName = runtimeName;
|
|
6056
6050
|
// P-3f5b7d26 — persist pooled on the DISK record (activeProcesses/_pooled
|
|
6057
6051
|
// is wiped on restart); cli.js's reattach path needs this since a pooled
|
|
@@ -6078,14 +6072,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
6078
6072
|
// Atomically stamp dispatched_to/dispatched_at on the originating work item (#402).
|
|
6079
6073
|
if (meta?.item?.id) {
|
|
6080
6074
|
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) => {
|
|
6075
|
+
const wiScope = resolveWorkItemScope(meta);
|
|
6076
|
+
if (wiScope) {
|
|
6077
|
+
mutateWorkItems(wiScope, (items) => {
|
|
6089
6078
|
if (!Array.isArray(items)) return items;
|
|
6090
6079
|
const wi = items.find(i => i.id === meta.item.id);
|
|
6091
6080
|
if (wi) {
|
|
@@ -6094,7 +6083,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
6094
6083
|
if (wi.status !== WI_STATUS.DISPATCHED) wi.status = WI_STATUS.DISPATCHED;
|
|
6095
6084
|
}
|
|
6096
6085
|
return items;
|
|
6097
|
-
}
|
|
6086
|
+
});
|
|
6098
6087
|
}
|
|
6099
6088
|
} catch (e) { log('warn', `stamp dispatched_to on work item ${meta.item.id}: ${e.message}`); }
|
|
6100
6089
|
}
|
|
@@ -6119,12 +6108,9 @@ function areDependenciesMet(item, config) {
|
|
|
6119
6108
|
for (const depId of deps) {
|
|
6120
6109
|
const depItem = allWorkItems.find(w => w.id === depId);
|
|
6121
6110
|
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).
|
|
6111
|
+
// Fallback: check the PRD store — plan-to-prd agents may pre-set items to done.
|
|
6126
6112
|
try {
|
|
6127
|
-
const plan =
|
|
6113
|
+
const plan = prdStore.readPrd(sourcePlan);
|
|
6128
6114
|
const prdItem = (plan?.missing_features || []).find(f => f.id === depId);
|
|
6129
6115
|
if (prdItem && PRD_MET_STATUSES.has(prdItem.status)) continue; // PRD says done — treat as met
|
|
6130
6116
|
} catch (e) { log('warn', 'check PRD dep status: ' + e.message); }
|
|
@@ -6196,8 +6182,8 @@ function detectDependencyCycles(items) {
|
|
|
6196
6182
|
}
|
|
6197
6183
|
|
|
6198
6184
|
// Reconciles work items against known PRs.
|
|
6199
|
-
// Primary linkage comes from
|
|
6200
|
-
// uses
|
|
6185
|
+
// Primary linkage comes from tracked PR prdItems; fallback linkage
|
|
6186
|
+
// uses SQL PR links so matching does not depend on branch/title parsing.
|
|
6201
6187
|
// onlyIds: if provided, only items whose ID is in this Set are eligible.
|
|
6202
6188
|
function reconcileItemsWithPrs(items, allPrs, { onlyIds } = {}) {
|
|
6203
6189
|
const prLinks = shared.getPrLinks();
|
|
@@ -6289,7 +6275,7 @@ function updateSnapshot(config) {
|
|
|
6289
6275
|
|
|
6290
6276
|
// ─── Cooldowns (extracted to engine/cooldown.js) ─────────────────────────────
|
|
6291
6277
|
|
|
6292
|
-
const {
|
|
6278
|
+
const { dispatchCooldowns, loadCooldowns, saveCooldowns,
|
|
6293
6279
|
isOnCooldown, setCooldown, setCooldownWithContext, drainCoalescedContexts,
|
|
6294
6280
|
setCooldownFailure, clearCooldown, getPrReviewCooldownKey, clearLegacyPrReviewCooldown,
|
|
6295
6281
|
isAlreadyDispatched, isBranchActive } = require('./engine/cooldown');
|
|
@@ -6297,12 +6283,11 @@ const { COOLDOWN_PATH, dispatchCooldowns, loadCooldowns, saveCooldowns,
|
|
|
6297
6283
|
// Auto-clean pending/failed work items for a PRD so they re-materialize with updated plan data
|
|
6298
6284
|
function autoCleanPrdWorkItems(prdFile, config) {
|
|
6299
6285
|
const allProjects = getProjects(config);
|
|
6300
|
-
const
|
|
6301
|
-
for (const proj of allProjects) wiPaths.push(projectWorkItemsPath(proj));
|
|
6286
|
+
const workItemScopes = ['central', ...allProjects];
|
|
6302
6287
|
const deletedIds = [];
|
|
6303
|
-
for (const
|
|
6288
|
+
for (const scope of workItemScopes) {
|
|
6304
6289
|
try {
|
|
6305
|
-
mutateWorkItems(
|
|
6290
|
+
mutateWorkItems(scope, items => {
|
|
6306
6291
|
const filtered = items.filter(w => {
|
|
6307
6292
|
if (w.sourcePlan === prdFile && (w.status === WI_STATUS.PENDING || w.status === WI_STATUS.FAILED)) {
|
|
6308
6293
|
deletedIds.push(w.id); return false;
|
|
@@ -6345,51 +6330,6 @@ function safePrdProjectSlug(projectName) {
|
|
|
6345
6330
|
return slug || 'project';
|
|
6346
6331
|
}
|
|
6347
6332
|
|
|
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
6333
|
// Fleet branches the engine/CC create. A worktree-mode operator checkout should
|
|
6394
6334
|
// NEVER sit on one of these (or any branch); when it does, a flow ran raw
|
|
6395
6335
|
// `git checkout`/`merge` in the live checkout instead of a worktree.
|
|
@@ -6458,16 +6398,15 @@ function restoreHijackedOperatorCheckouts(config) {
|
|
|
6458
6398
|
}
|
|
6459
6399
|
|
|
6460
6400
|
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
6401
|
const writePrdLocked = (fileName, data) => {
|
|
6463
|
-
return
|
|
6402
|
+
return prdStore.writePrd(fileName, data);
|
|
6464
6403
|
};
|
|
6465
|
-
const mutatePrdLocked = (fileName, fallback, mutator
|
|
6466
|
-
return
|
|
6404
|
+
const mutatePrdLocked = (fileName, fallback, mutator) => {
|
|
6405
|
+
return prdStore.mutatePrd(fileName, (current) => {
|
|
6467
6406
|
if (!current?.missing_features && fallback?.missing_features) current = fallback;
|
|
6468
6407
|
if (!current || Array.isArray(current) || typeof current !== 'object') current = {};
|
|
6469
6408
|
return mutator(current) || current;
|
|
6470
|
-
}, { defaultValue: fallback || {}
|
|
6409
|
+
}, { defaultValue: fallback || {} });
|
|
6471
6410
|
};
|
|
6472
6411
|
const enforceDeclaredPlanProject = (fileName, currentPlan) => {
|
|
6473
6412
|
if (!currentPlan?.source_plan) return { fileName, plan: currentPlan };
|
|
@@ -6526,7 +6465,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
6526
6465
|
const jsonName = mf.replace(/\.md$/, '.json');
|
|
6527
6466
|
// Atomic open ('wx' + retry) so two parallel migrations on the
|
|
6528
6467
|
// same slug don't both overwrite a single PRD (P-9b7e5d3c).
|
|
6529
|
-
const reserved =
|
|
6468
|
+
const reserved = prdStore.createPrdUnique(jsonName, parsed);
|
|
6530
6469
|
try { fs.unlinkSync(path.join(checkDir, mf)); } catch { /* cleanup */ }
|
|
6531
6470
|
log('info', `Plan enforcement: moved ${mf} → prd/${reserved} (PRDs must be .json in prd/)`);
|
|
6532
6471
|
}
|
|
@@ -6543,7 +6482,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
6543
6482
|
if (parsed?.missing_features) {
|
|
6544
6483
|
// Atomic open ('wx' + retry) so two parallel migrations on the
|
|
6545
6484
|
// same slug don't both overwrite a single PRD (P-9b7e5d3c).
|
|
6546
|
-
const reserved =
|
|
6485
|
+
const reserved = prdStore.createPrdUnique(jf, parsed);
|
|
6547
6486
|
try { fs.unlinkSync(path.join(PLANS_DIR, jf)); } catch { /* cleanup */ }
|
|
6548
6487
|
log('info', `Auto-migrated PRD ${jf} from plans/ to prd/${reserved}`);
|
|
6549
6488
|
}
|
|
@@ -6563,10 +6502,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
6563
6502
|
const SEQUENTIAL_ID_RE = /^[A-Za-z]+-?\d+$/;
|
|
6564
6503
|
|
|
6565
6504
|
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));
|
|
6505
|
+
let plan = prdStore.readPrd(file);
|
|
6570
6506
|
if (!plan?.missing_features) continue;
|
|
6571
6507
|
// Resurrection-re-execution guard ("old PRDs came back"): never (re)materialize
|
|
6572
6508
|
// a defunct PRD — archived, or completed with its source plan gone from plans/.
|
|
@@ -6713,7 +6649,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
6713
6649
|
const defaultProjectName = plan.project || file.replace(/-\d{4}-\d{2}-\d{2}\.json$/, '');
|
|
6714
6650
|
const allProjects = getProjects(config);
|
|
6715
6651
|
const defaultProject = shared.resolveProjectSource(defaultProjectName, allProjects, { allowCentral: false }).project;
|
|
6716
|
-
// No project found — use central
|
|
6652
|
+
// No project found — use central state (engine works without projects).
|
|
6717
6653
|
const useCentral = !defaultProject;
|
|
6718
6654
|
|
|
6719
6655
|
const statusFilter = PRD_MATERIALIZABLE;
|
|
@@ -6737,7 +6673,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
6737
6673
|
);
|
|
6738
6674
|
|
|
6739
6675
|
// Group items by target project (per-item project field overrides plan-level project)
|
|
6740
|
-
// When no projects are configured, all items
|
|
6676
|
+
// When no projects are configured, all items use the central scope.
|
|
6741
6677
|
const itemsByProject = new Map(); // projectName -> { project, items: [] }
|
|
6742
6678
|
for (const item of items) {
|
|
6743
6679
|
if (item.project) {
|
|
@@ -6787,19 +6723,19 @@ function materializePlansAsWorkItems(config) {
|
|
|
6787
6723
|
// A pause/reject/awaiting-approval signal may arrive between the outer-loop initial
|
|
6788
6724
|
// read and this point (e.g. operator pauses via dashboard while the tick is running).
|
|
6789
6725
|
// Re-reading here prevents phantom WI creation on a paused, rejected, or unapproved plan.
|
|
6790
|
-
const freshPrd = prdStore.readPrd(
|
|
6726
|
+
const freshPrd = prdStore.readPrd(file);
|
|
6791
6727
|
const freshStatus = (freshPrd || {}).status || ((freshPrd || {}).requires_approval ? 'awaiting-approval' : null);
|
|
6792
6728
|
if (freshStatus === PLAN_STATUS.PAUSED || freshStatus === PLAN_STATUS.REJECTED || freshStatus === 'awaiting-approval') {
|
|
6793
6729
|
log('warn', `PRD ${file}: status changed to '${freshStatus}' mid-tick — skipping WI batch for ${projName} (P-f1a3b5c7)`);
|
|
6794
6730
|
break;
|
|
6795
6731
|
}
|
|
6796
6732
|
|
|
6797
|
-
const
|
|
6733
|
+
const wiScope = project || 'central';
|
|
6798
6734
|
let created = 0;
|
|
6799
6735
|
const newlyCreatedIds = new Set(); // tracks IDs created in this pass for reconciliation scoping
|
|
6800
6736
|
const deferredReopens = []; // cross-project re-opens executed after this lock releases
|
|
6801
6737
|
|
|
6802
|
-
mutateWorkItems(
|
|
6738
|
+
mutateWorkItems(wiScope, existingItems => {
|
|
6803
6739
|
for (const item of projItems) {
|
|
6804
6740
|
// Re-open: 'updated' or 'missing' re-opens a done work item (#906)
|
|
6805
6741
|
// Use composite (sourcePlan, id) key so a WI from another PRD with the
|
|
@@ -6821,7 +6757,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
6821
6757
|
if (!alreadyExists) {
|
|
6822
6758
|
for (const p of allProjects) {
|
|
6823
6759
|
if (String(p.name || '').toLowerCase() === String(projName || '').toLowerCase()) continue;
|
|
6824
|
-
const otherItems =
|
|
6760
|
+
const otherItems = readWorkItems(p);
|
|
6825
6761
|
const otherWi = otherItems.find(w => w.id === item.id && w.sourcePlan === file);
|
|
6826
6762
|
if (otherWi) {
|
|
6827
6763
|
if (DONE_STATUSES.has(otherWi.status) && shouldReopen) {
|
|
@@ -6867,7 +6803,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
6867
6803
|
|
|
6868
6804
|
if (created > 0) {
|
|
6869
6805
|
// Reconciliation: exact prdItems match only, scoped to newly created items
|
|
6870
|
-
const allPrsForReconcile = allProjects.flatMap(p =>
|
|
6806
|
+
const allPrsForReconcile = allProjects.flatMap(p => readPullRequests(p));
|
|
6871
6807
|
const reconciled = reconcileItemsWithPrs(existingItems, allPrsForReconcile, { onlyIds: newlyCreatedIds });
|
|
6872
6808
|
if (reconciled > 0) log('info', `Plan reconciliation: marked ${reconciled} item(s) as done → ${projName}`);
|
|
6873
6809
|
|
|
@@ -6901,8 +6837,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
6901
6837
|
for (const { itemId, projectName: rProjName, item: rItem } of deferredReopens) {
|
|
6902
6838
|
const rProject = shared.resolveProjectSource(rProjName, allProjects, { allowCentral: false }).project;
|
|
6903
6839
|
if (!rProject) continue;
|
|
6904
|
-
|
|
6905
|
-
mutateWorkItems(rPath, items => {
|
|
6840
|
+
mutateWorkItems(rProject, items => {
|
|
6906
6841
|
const target = items.find(w => w.id === itemId);
|
|
6907
6842
|
if (target && DONE_STATUSES.has(target.status)) {
|
|
6908
6843
|
shared.reopenWorkItem(target);
|
|
@@ -6982,8 +6917,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
6982
6917
|
function clearPendingHumanFeedbackFlag(projectMeta, prId) {
|
|
6983
6918
|
if (!prId) return;
|
|
6984
6919
|
try {
|
|
6985
|
-
|
|
6986
|
-
mutatePullRequests(prsPath, prs => {
|
|
6920
|
+
mutatePullRequests(projectMeta, prs => {
|
|
6987
6921
|
const target = shared.findPrRecord(prs, prId, projectMeta);
|
|
6988
6922
|
if (!target?.humanFeedback?.pendingFix) return;
|
|
6989
6923
|
target.humanFeedback.pendingFix = false;
|
|
@@ -7058,7 +6992,7 @@ async function isWorktreeHeldPauseSuppressed(pr, project) {
|
|
|
7058
6992
|
}
|
|
7059
6993
|
if (!stillHeld) {
|
|
7060
6994
|
try {
|
|
7061
|
-
mutatePullRequests(
|
|
6995
|
+
mutatePullRequests(project, (prs) => {
|
|
7062
6996
|
if (!Array.isArray(prs)) return prs;
|
|
7063
6997
|
const target = shared.findPrRecord(prs, pr, project);
|
|
7064
6998
|
if (!target) return prs;
|
|
@@ -7110,7 +7044,7 @@ function resolvePrBranch(pr) {
|
|
|
7110
7044
|
function updatePrBranchResolutionState(project, pr, { branch = '', reason = '' } = {}) {
|
|
7111
7045
|
let changed = false;
|
|
7112
7046
|
try {
|
|
7113
|
-
mutatePullRequests(
|
|
7047
|
+
mutatePullRequests(project, prs => {
|
|
7114
7048
|
const target = shared.findPrRecord(prs, pr, project);
|
|
7115
7049
|
if (!target) return;
|
|
7116
7050
|
if (branch) {
|
|
@@ -7363,19 +7297,24 @@ function _resetAutoFixPausedLogState() {
|
|
|
7363
7297
|
// persist — the warning is for the operator at engine startup/run time).
|
|
7364
7298
|
const _warnedSilentDiscovery = new Set();
|
|
7365
7299
|
|
|
7366
|
-
function _warnSilentDiscoveryOnce(kind, project,
|
|
7300
|
+
function _warnSilentDiscoveryOnce(kind, project, config) {
|
|
7367
7301
|
const key = `${kind}:${project?.name || project?.localPath || 'default'}`;
|
|
7368
7302
|
if (_warnedSilentDiscovery.has(key)) return;
|
|
7369
7303
|
let count = 0;
|
|
7370
|
-
try {
|
|
7371
|
-
|
|
7304
|
+
try {
|
|
7305
|
+
const records = kind === 'pullRequests'
|
|
7306
|
+
? shared.readPullRequests(project)
|
|
7307
|
+
: shared.readWorkItems(project);
|
|
7308
|
+
if (Array.isArray(records)) count = records.length;
|
|
7309
|
+
} catch {}
|
|
7310
|
+
if (count <= 0) return;
|
|
7372
7311
|
_warnedSilentDiscovery.add(key);
|
|
7373
7312
|
const projName = project?.name || '(unnamed)';
|
|
7374
7313
|
const hasBlock = !!(project?.workSources?.[kind] || config?.workSources?.[kind]);
|
|
7375
7314
|
const hint = hasBlock
|
|
7376
7315
|
? `Toggle in Dashboard → Settings → Project, or set \`workSources.${kind}.enabled: true\` in config.json.`
|
|
7377
7316
|
: `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)
|
|
7317
|
+
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
7318
|
}
|
|
7380
7319
|
|
|
7381
7320
|
/**
|
|
@@ -7396,12 +7335,12 @@ function _logPrDispatchSkipOnce(pr, reason) {
|
|
|
7396
7335
|
}
|
|
7397
7336
|
|
|
7398
7337
|
/**
|
|
7399
|
-
* Scan
|
|
7338
|
+
* Scan tracked PR state for records needing review or fixes.
|
|
7400
7339
|
*/
|
|
7401
7340
|
async function discoverFromPrs(config, project) {
|
|
7402
7341
|
const src = project?.workSources?.pullRequests || config.workSources?.pullRequests;
|
|
7403
7342
|
if (!src?.enabled) {
|
|
7404
|
-
_warnSilentDiscoveryOnce('pullRequests', project,
|
|
7343
|
+
_warnSilentDiscoveryOnce('pullRequests', project, config);
|
|
7405
7344
|
return [];
|
|
7406
7345
|
}
|
|
7407
7346
|
|
|
@@ -7549,7 +7488,7 @@ async function discoverFromPrs(config, project) {
|
|
|
7549
7488
|
if (pr.reviewStatus !== REVIEW_STATUS.APPROVED) pr.reviewStatus = liveStatus;
|
|
7550
7489
|
// Persist so next tick doesn't re-check
|
|
7551
7490
|
try {
|
|
7552
|
-
|
|
7491
|
+
mutatePullRequests(project, data => {
|
|
7553
7492
|
if (!Array.isArray(data)) return data;
|
|
7554
7493
|
const target = shared.findPrRecord(data, pr, project);
|
|
7555
7494
|
if (target && target.reviewStatus !== REVIEW_STATUS.APPROVED) target.reviewStatus = liveStatus;
|
|
@@ -7765,7 +7704,7 @@ async function discoverFromPrs(config, project) {
|
|
|
7765
7704
|
log('info', `Pre-dispatch vote check: ${pr.id} is ${liveStatus} (cached was waiting) — skipping re-review`);
|
|
7766
7705
|
if (pr.reviewStatus !== REVIEW_STATUS.APPROVED) pr.reviewStatus = liveStatus;
|
|
7767
7706
|
try {
|
|
7768
|
-
|
|
7707
|
+
mutatePullRequests(project, data => {
|
|
7769
7708
|
if (!Array.isArray(data)) return data;
|
|
7770
7709
|
const target = shared.findPrRecord(data, pr, project);
|
|
7771
7710
|
if (target && target.reviewStatus !== REVIEW_STATUS.APPROVED) target.reviewStatus = liveStatus;
|
|
@@ -7940,7 +7879,7 @@ async function discoverFromPrs(config, project) {
|
|
|
7940
7879
|
const live = await checkBcFn(pr, project);
|
|
7941
7880
|
if (live?.buildStatusStale) {
|
|
7942
7881
|
try {
|
|
7943
|
-
mutatePullRequests(
|
|
7882
|
+
mutatePullRequests(project, prs => {
|
|
7944
7883
|
const target = shared.findPrRecord(prs, pr, project);
|
|
7945
7884
|
if (!target) return;
|
|
7946
7885
|
target._buildStatusStale = true;
|
|
@@ -7952,7 +7891,7 @@ async function discoverFromPrs(config, project) {
|
|
|
7952
7891
|
log('info', `Pre-dispatch build check: ${pr.id} build is ${live.buildStatus} (cached was failing) — skipping build-fix`);
|
|
7953
7892
|
// Persist the fresh status so subsequent ticks don't re-check on every pass
|
|
7954
7893
|
try {
|
|
7955
|
-
mutatePullRequests(
|
|
7894
|
+
mutatePullRequests(project, prs => {
|
|
7956
7895
|
const target = shared.findPrRecord(prs, pr, project);
|
|
7957
7896
|
if (!target) return;
|
|
7958
7897
|
target.buildStatus = live.buildStatus;
|
|
@@ -8016,8 +7955,7 @@ async function discoverFromPrs(config, project) {
|
|
|
8016
7955
|
if (pr.agent && !pr._buildFailNotified) {
|
|
8017
7956
|
// Mark noted to prevent repeated processing; the fix dispatch and PR state carry the failure details.
|
|
8018
7957
|
try {
|
|
8019
|
-
|
|
8020
|
-
mutatePullRequests(prPath, prs => {
|
|
7958
|
+
mutatePullRequests(project, prs => {
|
|
8021
7959
|
const target = shared.findPrRecord(prs, pr, project);
|
|
8022
7960
|
if (target) {
|
|
8023
7961
|
target._buildFailNotified = true;
|
|
@@ -8082,7 +8020,7 @@ async function discoverFromPrs(config, project) {
|
|
|
8082
8020
|
if (live && live.mergeConflict === false) {
|
|
8083
8021
|
log('info', `Pre-dispatch conflict check: ${pr.id} reports clean merge (cached was conflict) — skipping conflict-fix`);
|
|
8084
8022
|
try {
|
|
8085
|
-
mutatePullRequests(
|
|
8023
|
+
mutatePullRequests(project, prs => {
|
|
8086
8024
|
const target = shared.findPrRecord(prs, pr, project);
|
|
8087
8025
|
if (!target) return;
|
|
8088
8026
|
delete target._mergeConflict;
|
|
@@ -8111,7 +8049,7 @@ async function discoverFromPrs(config, project) {
|
|
|
8111
8049
|
newWork.push(item);
|
|
8112
8050
|
// Record dispatch timestamp so re-dispatch is suppressed during ADO lag window
|
|
8113
8051
|
try {
|
|
8114
|
-
mutatePullRequests(
|
|
8052
|
+
mutatePullRequests(project, prs => {
|
|
8115
8053
|
const target = shared.findPrRecord(prs, pr, project);
|
|
8116
8054
|
if (target) target._conflictFixedAt = new Date().toISOString();
|
|
8117
8055
|
});
|
|
@@ -8211,7 +8149,7 @@ function _buildRunnerBriefVars(item, project) {
|
|
|
8211
8149
|
}
|
|
8212
8150
|
|
|
8213
8151
|
/**
|
|
8214
|
-
* Scan work-
|
|
8152
|
+
* Scan a project work-item scope for manually queued tasks.
|
|
8215
8153
|
*/
|
|
8216
8154
|
function renderProjectWorkItemPromptForAgent(item, workType, agentId, config, project, root, branchName, options = {}) {
|
|
8217
8155
|
const worktreePath = options.worktreePath || path.resolve(root, config.engine?.worktreeRoot || '../worktrees', `${branchName}`);
|
|
@@ -8425,7 +8363,7 @@ function resolveWorkItemPrRecord(item, project) {
|
|
|
8425
8363
|
if (!project) return null;
|
|
8426
8364
|
const prRef = getWorkItemPrRef(item);
|
|
8427
8365
|
if (!prRef) return null;
|
|
8428
|
-
const prs =
|
|
8366
|
+
const prs = readPullRequests(project);
|
|
8429
8367
|
shared.normalizePrRecords(prs, project);
|
|
8430
8368
|
return shared.findPrRecord(prs, prRef, project);
|
|
8431
8369
|
}
|
|
@@ -8492,7 +8430,7 @@ function refreshDeferredReviewPrompt(item, config) {
|
|
|
8492
8430
|
// head SHA before rebuilding the prompt.
|
|
8493
8431
|
let livePr = pr;
|
|
8494
8432
|
try {
|
|
8495
|
-
const prs =
|
|
8433
|
+
const prs = readPullRequests(project);
|
|
8496
8434
|
const found = shared.findPrRecord(prs, pr, project);
|
|
8497
8435
|
if (found) livePr = found;
|
|
8498
8436
|
} catch (e) { log('warn', `refreshDeferredReviewPrompt: live PR lookup for ${pr.id} failed: ${e.message}`); }
|
|
@@ -8524,12 +8462,12 @@ function refreshDeferredReviewPrompt(item, config) {
|
|
|
8524
8462
|
function discoverFromWorkItems(config, project) {
|
|
8525
8463
|
const src = project?.workSources?.workItems || config.workSources?.workItems;
|
|
8526
8464
|
if (!src?.enabled) {
|
|
8527
|
-
_warnSilentDiscoveryOnce('workItems', project,
|
|
8465
|
+
_warnSilentDiscoveryOnce('workItems', project, config);
|
|
8528
8466
|
return [];
|
|
8529
8467
|
}
|
|
8530
8468
|
|
|
8531
8469
|
const root = project?.localPath ? path.resolve(project.localPath) : path.resolve(MINIONS_DIR, '..');
|
|
8532
|
-
const items =
|
|
8470
|
+
const items = readWorkItems(project);
|
|
8533
8471
|
// W-mq9b7lor (H3): snapshot each item's serializable state BEFORE the
|
|
8534
8472
|
// discover loop runs so we can compute per-item field deltas at the end
|
|
8535
8473
|
// and apply them inside the mutateWorkItems lock — the prior
|
|
@@ -8639,7 +8577,7 @@ function discoverFromWorkItems(config, project) {
|
|
|
8639
8577
|
}
|
|
8640
8578
|
|
|
8641
8579
|
const key = `work-${project?.name || 'default'}-${item.id}`;
|
|
8642
|
-
// Self-heal: collect keys for batched dispatch
|
|
8580
|
+
// Self-heal: collect keys for batched dispatch cleanup.
|
|
8643
8581
|
selfHealKeys.add(key);
|
|
8644
8582
|
dispatchCooldowns.delete(key);
|
|
8645
8583
|
// Cooldown bypass for resumed items
|
|
@@ -8945,7 +8883,7 @@ function discoverFromWorkItems(config, project) {
|
|
|
8945
8883
|
}
|
|
8946
8884
|
}
|
|
8947
8885
|
if (patches.size > 0) {
|
|
8948
|
-
mutateWorkItems(
|
|
8886
|
+
mutateWorkItems(project, (current) => {
|
|
8949
8887
|
for (const it of current) {
|
|
8950
8888
|
const patch = patches.get(it.id);
|
|
8951
8889
|
if (!patch) continue;
|
|
@@ -9211,10 +9149,9 @@ function materializeSpecsAsWorkItems(config, project) {
|
|
|
9211
9149
|
|
|
9212
9150
|
if (recentSpecs.length === 0) return;
|
|
9213
9151
|
|
|
9214
|
-
const wiPath = projectWorkItemsPath(project);
|
|
9215
9152
|
let created = 0;
|
|
9216
9153
|
|
|
9217
|
-
mutateWorkItems(
|
|
9154
|
+
mutateWorkItems(project, existingItems => {
|
|
9218
9155
|
for (const pr of mergedPrs) {
|
|
9219
9156
|
const prBranch = (pr.branch || '').toLowerCase();
|
|
9220
9157
|
const matchedSpecs = recentSpecs.filter(doc => {
|
|
@@ -9301,12 +9238,11 @@ function extractSpecInfo(filePath, projectRoot_) {
|
|
|
9301
9238
|
}
|
|
9302
9239
|
|
|
9303
9240
|
/**
|
|
9304
|
-
* Scan central
|
|
9241
|
+
* Scan central SQL state for project-agnostic tasks.
|
|
9305
9242
|
* Uses the shared work-item.md playbook with multi-project context injected.
|
|
9306
9243
|
*/
|
|
9307
9244
|
function discoverCentralWorkItems(config) {
|
|
9308
|
-
const
|
|
9309
|
-
const items = safeJsonArr(centralPath);
|
|
9245
|
+
const items = readWorkItems('central');
|
|
9310
9246
|
const projects = getProjects(config);
|
|
9311
9247
|
const dispatchProjects = getCentralDispatchProjects(projects);
|
|
9312
9248
|
const newWork = [];
|
|
@@ -9606,7 +9542,6 @@ function discoverCentralWorkItems(config) {
|
|
|
9606
9542
|
// Inject plan-to-prd variables — read the plan file content for the playbook
|
|
9607
9543
|
if (workType === WORK_TYPE.PLAN_TO_PRD && item.planFile) {
|
|
9608
9544
|
if (!fs.existsSync(PLANS_DIR)) fs.mkdirSync(PLANS_DIR, { recursive: true });
|
|
9609
|
-
if (!fs.existsSync(PRD_DIR)) fs.mkdirSync(PRD_DIR, { recursive: true });
|
|
9610
9545
|
if (planFileContent !== null) {
|
|
9611
9546
|
vars.plan_content = planFileContent;
|
|
9612
9547
|
} else {
|
|
@@ -9648,7 +9583,7 @@ function discoverCentralWorkItems(config) {
|
|
|
9648
9583
|
const prdRows = prdStore.listPrdRows();
|
|
9649
9584
|
const prdFiles = prdRows.filter(row => !row.archived).map(row => row.filename);
|
|
9650
9585
|
for (const pf of prdFiles) {
|
|
9651
|
-
const prd = prdStore.readPrd(
|
|
9586
|
+
const prd = prdStore.readPrd(pf);
|
|
9652
9587
|
if (planKey && prd && !shared.isPrdArchived(prd) && shared.prdMatchesSourcePlan(prd.source_plan, planKey)) {
|
|
9653
9588
|
prdFilename = pf;
|
|
9654
9589
|
vars.existing_prd_json = JSON.stringify(prd, null, 2);
|
|
@@ -9747,53 +9682,20 @@ function discoverCentralWorkItems(config) {
|
|
|
9747
9682
|
}
|
|
9748
9683
|
|
|
9749
9684
|
if (mutations.size > 0) {
|
|
9750
|
-
//
|
|
9751
|
-
|
|
9685
|
+
// Apply field patches to a fresh transactional snapshot.
|
|
9686
|
+
mutateWorkItems('central', (freshItems) => {
|
|
9752
9687
|
if (!Array.isArray(freshItems)) freshItems = [];
|
|
9753
9688
|
for (const fi of freshItems) {
|
|
9754
9689
|
const m = mutations.get(fi.id);
|
|
9755
9690
|
if (m) Object.assign(fi, m);
|
|
9756
9691
|
}
|
|
9757
9692
|
return freshItems;
|
|
9758
|
-
}
|
|
9693
|
+
});
|
|
9759
9694
|
}
|
|
9760
9695
|
return newWork;
|
|
9761
9696
|
}
|
|
9762
9697
|
|
|
9763
9698
|
|
|
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
9699
|
/**
|
|
9798
9700
|
* Run all work discovery sources and queue new items
|
|
9799
9701
|
* Priority: fix (0) > ask (1) > review (1) > implement (2) > work-items (3) > central (4)
|
|
@@ -9803,7 +9705,7 @@ async function discoverWork(config) {
|
|
|
9803
9705
|
const projects = getProjects(config);
|
|
9804
9706
|
let persistedFixes = 0, persistedReviews = 0, persistedOther = 0;
|
|
9805
9707
|
|
|
9806
|
-
// Side-effect passes: materialize plans and design docs into work-
|
|
9708
|
+
// Side-effect passes: materialize plans and design docs into SQL work-item state.
|
|
9807
9709
|
// These write to project work queues — picked up by discoverFromWorkItems below.
|
|
9808
9710
|
// Gated by config.engine?.planMaterializationEnabled !== false (P-d6f0a2b5) —
|
|
9809
9711
|
// operators can suppress the PRD reconcile + plan materialization pair
|
|
@@ -9827,10 +9729,10 @@ async function discoverWork(config) {
|
|
|
9827
9729
|
// projects)` loop below leaves this whole async function permanently
|
|
9828
9730
|
// unresolved, so anything only collected into a local array and persisted
|
|
9829
9731
|
// at the very end (the pre-fix allFixes/allReviews/allWorkItems) never
|
|
9830
|
-
// reaches dispatch
|
|
9732
|
+
// reaches dispatch state — silently discarding valid work already
|
|
9831
9733
|
// discovered for unrelated, healthy projects processed earlier in the same
|
|
9832
9734
|
// loop. Persisting per batch means a healthy project's discoveries land in
|
|
9833
|
-
// dispatch
|
|
9735
|
+
// dispatch state immediately, before the loop reaches a slow project.
|
|
9834
9736
|
async function persistBatch(items) {
|
|
9835
9737
|
if (!items || items.length === 0) return;
|
|
9836
9738
|
// Gate reviews and fixes: only when at max concurrency — idle agents should pick up reviews
|
|
@@ -9883,7 +9785,7 @@ async function discoverWork(config) {
|
|
|
9883
9785
|
} catch (e) {
|
|
9884
9786
|
// Fail-open contract preserved: any validator throw here means the
|
|
9885
9787
|
// item is dropped from this discovery batch rather than wedging the
|
|
9886
|
-
// whole chunk. The item stays
|
|
9788
|
+
// whole chunk. The item stays pending in SQL and the
|
|
9887
9789
|
// next tick re-discovers it.
|
|
9888
9790
|
log('warn', `pre-dispatch-eval: addToDispatchWithValidation threw for ${item?.meta?.item?.id || item?.id || 'unknown'}: ${e?.message || e}`);
|
|
9889
9791
|
}
|
|
@@ -9959,7 +9861,6 @@ async function discoverWork(config) {
|
|
|
9959
9861
|
const scheduledWork = discoverScheduledWork(config);
|
|
9960
9862
|
if (scheduledWork.length > 0) {
|
|
9961
9863
|
const { createMeeting, getMeetings } = require('./engine/meeting');
|
|
9962
|
-
const centralPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
9963
9864
|
// Separate meetings (no work-items write) from task items
|
|
9964
9865
|
const taskItems = [];
|
|
9965
9866
|
for (const item of scheduledWork) {
|
|
@@ -9973,8 +9874,7 @@ async function discoverWork(config) {
|
|
|
9973
9874
|
}
|
|
9974
9875
|
}
|
|
9975
9876
|
if (taskItems.length > 0) {
|
|
9976
|
-
|
|
9977
|
-
mutateJsonFileLocked(centralPath, (items) => {
|
|
9877
|
+
mutateWorkItems('central', (items) => {
|
|
9978
9878
|
if (!Array.isArray(items)) items = [];
|
|
9979
9879
|
let added = 0;
|
|
9980
9880
|
// Snapshot active dedup keys BEFORE the loop so multiple items in the
|
|
@@ -10010,7 +9910,7 @@ async function discoverWork(config) {
|
|
|
10010
9910
|
log('info', `Scheduled task fired: ${item._scheduleId} → ${item.title}`);
|
|
10011
9911
|
}
|
|
10012
9912
|
return items;
|
|
10013
|
-
}
|
|
9913
|
+
});
|
|
10014
9914
|
}
|
|
10015
9915
|
}
|
|
10016
9916
|
} catch (e) { log('warn', 'discover scheduled work: ' + e.message); }
|
|
@@ -10036,51 +9936,19 @@ async function discoverWork(config) {
|
|
|
10036
9936
|
if (tickCount % (ENGINE_DEFAULTS.planCompletionScanEvery || 60) === 0) {
|
|
10037
9937
|
try {
|
|
10038
9938
|
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);
|
|
9939
|
+
for (const f of prdStore.listPrdRows().filter(row => !row.archived).map(row => row.filename)) {
|
|
9940
|
+
if (completedPlanCache.has(f)) continue;
|
|
9941
|
+
const plan = prdStore.readPrd(f);
|
|
9942
|
+
if (!plan?.missing_features) continue;
|
|
9943
|
+
if (shared.isDefunctPrd(plan, PLANS_DIR)) { completedPlanCache.add(f); continue; }
|
|
9944
|
+
if (plan.status === PLAN_STATUS.COMPLETED) {
|
|
9945
|
+
const done = lifecycle.checkPlanCompletion({ item: { sourcePlan: f } }, config);
|
|
9946
|
+
if (done) completedPlanCache.add(f);
|
|
9947
|
+
continue;
|
|
10083
9948
|
}
|
|
9949
|
+
if (plan.status !== PLAN_STATUS.APPROVED && plan.status !== PLAN_STATUS.ACTIVE) continue;
|
|
9950
|
+
const completed = lifecycle.checkPlanCompletion({ item: { sourcePlan: f } }, config);
|
|
9951
|
+
if (completed) completedPlanCache.add(f);
|
|
10084
9952
|
}
|
|
10085
9953
|
} catch (e) { log('warn', 'plan completion sweep: ' + e.message); }
|
|
10086
9954
|
}
|
|
@@ -10204,26 +10072,6 @@ const completedPlanCache = new Set();
|
|
|
10204
10072
|
// Dedupe the "Skipping defunct PRD" materializer log so a lingering defunct PRD
|
|
10205
10073
|
// doesn't spam one line per discovery tick; cleared when the file stops being defunct.
|
|
10206
10074
|
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
10075
|
let lastWatchCheckAt = 0;
|
|
10228
10076
|
let lastPrStatusPollAt = 0;
|
|
10229
10077
|
let lastPrCommentsPollAt = 0;
|
|
@@ -10451,7 +10299,7 @@ async function tickInner() {
|
|
|
10451
10299
|
}
|
|
10452
10300
|
|
|
10453
10301
|
// 2.53. managed-spawn TTL/dead-PID sweep + log rotation (P-8a4d6f29). Walks
|
|
10454
|
-
//
|
|
10302
|
+
// SQL managed-process state, kills TTL-expired specs, drops dead-PID
|
|
10455
10303
|
// rows, rotates managed-logs/<name>.log past ENGINE_DEFAULTS.managedSpawn
|
|
10456
10304
|
// .logRotateBytes. Mirrors the keep-processes sweep cadence (sweepEvery=180)
|
|
10457
10305
|
// so the engine never iterates per-spec on every tick. Healthcheck loops
|
|
@@ -10498,16 +10346,12 @@ async function tickInner() {
|
|
|
10498
10346
|
safe('checkWatches', () => {
|
|
10499
10347
|
const { checkWatches } = require('./engine/watches');
|
|
10500
10348
|
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
|
-
});
|
|
10349
|
+
const pullRequestStore = require('./engine/pull-requests-store');
|
|
10350
|
+
const workItemStore = require('./engine/work-items-store');
|
|
10351
|
+
const pullRequests = projects.flatMap(p => pullRequestStore.readPullRequestsForScope(p.name));
|
|
10352
|
+
const workItems = projects.flatMap(p => workItemStore.readWorkItemsForScope(p.name));
|
|
10509
10353
|
// Also include central work items
|
|
10510
|
-
const centralWi =
|
|
10354
|
+
const centralWi = workItemStore.readWorkItemsForScope('central');
|
|
10511
10355
|
|
|
10512
10356
|
// Gather state for the new generalized target types. Each block is
|
|
10513
10357
|
// best-effort — if a module/file is missing the watch evaluator will
|
|
@@ -10515,23 +10359,11 @@ async function tickInner() {
|
|
|
10515
10359
|
let meetings = [];
|
|
10516
10360
|
try { meetings = require('./engine/meeting').getMeetings(); } catch { /* optional */ }
|
|
10517
10361
|
|
|
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 */ }
|
|
10362
|
+
const plans = require('./engine/prd-store').listPrdRows()
|
|
10363
|
+
.map(({ filename, plan }) => ({ ...plan, _source: filename, _sourcePlan: plan.source_plan || '' }));
|
|
10532
10364
|
|
|
10533
10365
|
let scheduleRuns = {};
|
|
10534
|
-
try { scheduleRuns =
|
|
10366
|
+
try { scheduleRuns = require('./engine/small-state-store').readScheduleRuns(); } catch { /* optional */ }
|
|
10535
10367
|
|
|
10536
10368
|
let pipelineRuns = {};
|
|
10537
10369
|
try { pipelineRuns = require('./engine/pipeline').getPipelineRuns(); } catch { /* optional */ }
|
|
@@ -10631,29 +10463,7 @@ async function tickInner() {
|
|
|
10631
10463
|
const prdFiles = prdStore.listPrdRows().filter(row => !row.archived).map(row => row.filename);
|
|
10632
10464
|
for (const file of prdFiles) {
|
|
10633
10465
|
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));
|
|
10466
|
+
const plan = prdStore.readPrd(file);
|
|
10657
10467
|
// Resurrection-re-execution guard: a defunct PRD (archived, or completed
|
|
10658
10468
|
// with its source plan gone from plans/) must NOT have its verify gate
|
|
10659
10469
|
// re-created. This scan deliberately re-runs completion for completed
|
|
@@ -10767,13 +10577,12 @@ async function tickInner() {
|
|
|
10767
10577
|
// Check for failed items blocking pending items
|
|
10768
10578
|
for (const project of projects) {
|
|
10769
10579
|
try {
|
|
10770
|
-
const wiPath = projectWorkItemsPath(project);
|
|
10771
10580
|
// Collect keys to clear AFTER work-items lock is released (avoid nested locks)
|
|
10772
10581
|
const dispatchKeysToClear = [];
|
|
10773
10582
|
const cooldownKeysToClear = [];
|
|
10774
10583
|
|
|
10775
10584
|
if (_isTickStale(myGeneration)) return;
|
|
10776
|
-
mutateWorkItems(
|
|
10585
|
+
mutateWorkItems(project, items => {
|
|
10777
10586
|
let changed = false;
|
|
10778
10587
|
const failedIds = new Set(items.filter(w => w.status === WI_STATUS.FAILED).map(w => w.id));
|
|
10779
10588
|
const pendingWithBlockedDeps = items.filter(w =>
|
|
@@ -11036,7 +10845,7 @@ async function tickInner() {
|
|
|
11036
10845
|
assignPendingDispatchAgent(item, altAgent, config);
|
|
11037
10846
|
pendingAgents.add(altAgent);
|
|
11038
10847
|
log('info', `Reassigning ${item.id} from unspawned temp ${prevAgent} to ${altAgent} — temp agent never spawned`);
|
|
11039
|
-
// Persist reassignment
|
|
10848
|
+
// Persist reassignment so it survives restarts/ticks.
|
|
11040
10849
|
persistPendingDispatchAgent(item);
|
|
11041
10850
|
}
|
|
11042
10851
|
}
|
|
@@ -11272,12 +11081,10 @@ async function tickInner() {
|
|
|
11272
11081
|
// Defensive: ensure the work item is re-queued if completeDispatch didn't fire
|
|
11273
11082
|
if (item.meta?.item?.id) {
|
|
11274
11083
|
try {
|
|
11275
|
-
const
|
|
11276
|
-
|
|
11277
|
-
: item.meta.project?.name ? projectWorkItemsPath({ name: item.meta.project.name, localPath: item.meta.project.localPath }) : null;
|
|
11278
|
-
if (wiPath) {
|
|
11084
|
+
const wiScope = resolveWorkItemScope(item.meta);
|
|
11085
|
+
if (wiScope) {
|
|
11279
11086
|
if (_isTickStale(myGeneration)) return;
|
|
11280
|
-
mutateWorkItems(
|
|
11087
|
+
mutateWorkItems(wiScope, items => {
|
|
11281
11088
|
const wi = items.find(i => i.id === item.meta.item.id);
|
|
11282
11089
|
if (wi && wi.status === WI_STATUS.DISPATCHED) {
|
|
11283
11090
|
// completeDispatch didn't update the work item — re-queue manually
|
|
@@ -11400,8 +11207,8 @@ async function tickInner() {
|
|
|
11400
11207
|
// clobbered. Previously the mutator callback assigned dp.pending from the
|
|
11401
11208
|
// pre-lock postDispatch snapshot, overwriting any freshly loaded queue
|
|
11402
11209
|
// entries. Sentinel-undefined for _pendingReason / _agentBusySince
|
|
11403
|
-
// mirrors the in-loop `delete` semantics —
|
|
11404
|
-
// undefined-valued keys
|
|
11210
|
+
// mirrors the in-loop `delete` semantics — the SQL store serializes
|
|
11211
|
+
// records without undefined-valued keys.
|
|
11405
11212
|
const patches = new Map();
|
|
11406
11213
|
for (const item of (postDispatch.pending || [])) {
|
|
11407
11214
|
patches.set(item.id, {
|
|
@@ -11517,7 +11324,7 @@ function emitMemoryBaseline(tickN) {
|
|
|
11517
11324
|
module.exports = {
|
|
11518
11325
|
// Paths
|
|
11519
11326
|
MINIONS_DIR, ENGINE_DIR, AGENTS_DIR, PLAYBOOKS_DIR, PLANS_DIR, PRD_DIR,
|
|
11520
|
-
CONTROL_PATH,
|
|
11327
|
+
CONTROL_PATH, INBOX_DIR, KNOWLEDGE_DIR, ARCHIVE_DIR,
|
|
11521
11328
|
IDENTITY_DIR, CONFIG_PATH, ROUTING_PATH, NOTES_PATH, SKILLS_DIR,
|
|
11522
11329
|
|
|
11523
11330
|
// Utilities
|
|
@@ -11542,10 +11349,7 @@ module.exports = {
|
|
|
11542
11349
|
discoverWork, discoverFromPrs, discoverFromWorkItems, discoverCentralWorkItems,
|
|
11543
11350
|
materializePlansAsWorkItems,
|
|
11544
11351
|
materializeSpecsAsWorkItems, // exported for testing (P-f7-git-log)
|
|
11545
|
-
reservePrdFilename, // exported for testing (P-9b7e5d3c)
|
|
11546
11352
|
restoreHijackedOperatorCheckouts, // exported for testing — operator-checkout self-heal
|
|
11547
|
-
sweepStaleArchivedPrdBackups, // exported for testing
|
|
11548
|
-
_isGhostPrdRestore, // exported for testing (RC4 — ghost-purge identity check)
|
|
11549
11353
|
|
|
11550
11354
|
// Shared helpers (used by lifecycle.js and tests)
|
|
11551
11355
|
reconcileItemsWithPrs, detectDependencyCycles,
|