@yemi33/minions 0.1.2287 → 0.1.2289
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/dashboard/js/render-plans.js +7 -2
- package/dashboard/js/settings.js +3 -0
- package/dashboard/slim/body.html +4 -11
- package/dashboard/slim/js/modals-tiles.js +66 -1
- package/dashboard/slim/js/status.js +82 -98
- package/dashboard/slim/styles.css +13 -87
- package/dashboard.js +81 -50
- package/docs/deprecated.json +175 -37
- package/docs/live-checkout-mode.md +26 -4
- package/engine/ado.js +11 -2
- package/engine/github.js +7 -0
- package/engine/lifecycle.js +9 -3
- package/engine/live-checkout.js +139 -8
- package/engine/playbook.js +5 -0
- package/engine/prd-store.js +1 -1
- package/engine/queries.js +10 -3
- package/engine/shared.js +104 -5
- package/engine.js +143 -9
- package/package.json +1 -1
- package/playbooks/plan-to-prd.md +2 -1
- package/playbooks/shared-rules.md +1 -0
package/engine/lifecycle.js
CHANGED
|
@@ -754,6 +754,11 @@ const _STALE_PRD_STATUSES = new Set([WI_STATUS.DISPATCHED, WI_STATUS.FAILED, WI_
|
|
|
754
754
|
// awaiting-approval PRDs. Previously only COMPLETED was skipped.
|
|
755
755
|
const _PRD_FROZEN_STATUSES = new Set([
|
|
756
756
|
PLAN_STATUS.COMPLETED, PLAN_STATUS.PAUSED, PLAN_STATUS.REJECTED, PLAN_STATUS.AWAITING_APPROVAL,
|
|
757
|
+
// 'archived' — Phase 10 step 4: archive becomes an in-place flag/status (the
|
|
758
|
+
// PRD stays in prd/ with status='archived' rather than moving to prd/archive/).
|
|
759
|
+
// A frozen PRD's item statuses must never be mutated; an archived-in-place PRD
|
|
760
|
+
// previously froze only by being moved out of the dir these scanners read.
|
|
761
|
+
'archived',
|
|
757
762
|
]);
|
|
758
763
|
function syncPrdItemStatus(itemId, status, sourcePlan) {
|
|
759
764
|
if (!itemId) return;
|
|
@@ -1316,7 +1321,7 @@ async function enrollPrFromCanonicalId(canonicalPrId, project, opts = {}) {
|
|
|
1316
1321
|
if (adoParts.length === 3) {
|
|
1317
1322
|
const ado = require('./ado');
|
|
1318
1323
|
if (typeof ado.fetchAdoPrMetadata === 'function') {
|
|
1319
|
-
const meta = await ado.fetchAdoPrMetadata(prNumber, adoParts[0], adoParts[1], adoParts[2]);
|
|
1324
|
+
const meta = await ado.fetchAdoPrMetadata(prNumber, adoParts[0], adoParts[1], adoParts[2], project);
|
|
1320
1325
|
if (meta) {
|
|
1321
1326
|
title = String(meta.title || '').slice(0, 200);
|
|
1322
1327
|
branch = meta.branch || '';
|
|
@@ -3296,6 +3301,7 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
3296
3301
|
const planPath = path.join(prdDir, pf);
|
|
3297
3302
|
mutateJsonFileLocked(planPath, (plan) => {
|
|
3298
3303
|
if (!plan?.missing_features) return plan;
|
|
3304
|
+
if (shared.isPrdArchived(plan)) return plan; // step 4: never mutate an archived (in-place) PRD's items
|
|
3299
3305
|
for (const feature of plan.missing_features) {
|
|
3300
3306
|
if (mergedItemSet.has(feature.id) && feature.status !== WI_STATUS.DONE) {
|
|
3301
3307
|
feature.status = WI_STATUS.DONE;
|
|
@@ -5216,7 +5222,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5216
5222
|
if (meta.item.planFile) {
|
|
5217
5223
|
try {
|
|
5218
5224
|
const prd = safeJson(path.join(PRD_DIR, expectedFile));
|
|
5219
|
-
if (prd && prd.source_plan
|
|
5225
|
+
if (prd && shared.prdMatchesSourcePlan(prd.source_plan, meta.item.planFile)) {
|
|
5220
5226
|
prdFound = true;
|
|
5221
5227
|
resolvedPrdFilename = expectedFile;
|
|
5222
5228
|
} else {
|
|
@@ -5243,7 +5249,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5243
5249
|
if (!f.endsWith('.json')) continue;
|
|
5244
5250
|
try {
|
|
5245
5251
|
const prd = safeJson(path.join(PRD_DIR, f));
|
|
5246
|
-
if (prd && prd.source_plan
|
|
5252
|
+
if (prd && !shared.isPrdArchived(prd) && shared.prdMatchesSourcePlan(prd.source_plan, meta.item.planFile)) {
|
|
5247
5253
|
prdFound = true;
|
|
5248
5254
|
resolvedPrdFilename = f;
|
|
5249
5255
|
break;
|
package/engine/live-checkout.js
CHANGED
|
@@ -62,10 +62,15 @@
|
|
|
62
62
|
* Worktree (isolated) mode is unaffected — that path still branches from
|
|
63
63
|
* origin/<mainRef> for a clean baseline elsewhere in spawnAgent.
|
|
64
64
|
*
|
|
65
|
-
* PURE — does NOT call completeDispatch
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
65
|
+
* MOSTLY PURE — does NOT call completeDispatch. It does NOT mutate the tree or
|
|
66
|
+
* write inbox alerts on any default path. The ONE opt-in exception is auto-reset
|
|
67
|
+
* (W-mqvejug6000eeb20): when `liveCheckoutAutoReset` is enabled AND the tree is
|
|
68
|
+
* dirty, it runs `git fetch origin` + `git reset --hard origin/<branch>` and
|
|
69
|
+
* writes a single `live-checkout-autoreset-<wiId>` inbox note. Both the reset
|
|
70
|
+
* git ops and the note writer are injectable (`_git`, `_writeInboxNote`) and the
|
|
71
|
+
* decision is injectable (`autoReset` / `_resolveAutoReset`), so unit tests stay
|
|
72
|
+
* filesystem-free. Default (auto-reset off) keeps the helper free of side
|
|
73
|
+
* effects — the dirty/blocked translation belongs in spawnAgent (engine.js).
|
|
69
74
|
*
|
|
70
75
|
* All git invocations route through `shared.shellSafeGit` (argv-form,
|
|
71
76
|
* shell:false, execFile). Branch and main-ref are validated via
|
|
@@ -86,6 +91,17 @@ const fs = require('fs');
|
|
|
86
91
|
const path = require('path');
|
|
87
92
|
const shared = require('./shared');
|
|
88
93
|
|
|
94
|
+
// W-mqvejug6000eeb20 — generous stdout ceiling for the read-only git probes in
|
|
95
|
+
// this module (`git status --porcelain`, `rev-parse`, …). Node's execFile
|
|
96
|
+
// default maxBuffer is 1 MB; a live-checkout tree with a very large number of
|
|
97
|
+
// dirty/untracked paths can blow past that on `git status --porcelain`, and the
|
|
98
|
+
// resulting ERR_CHILD_PROCESS_STDOUT_MAXBUFFER rejection was being mis-surfaced
|
|
99
|
+
// as a transient/retryable LIVE_CHECKOUT_FAILED that retry-storms to the cap.
|
|
100
|
+
// 50 MB is far above any realistic porcelain output (≈ thousands of files) yet
|
|
101
|
+
// still bounded. Threaded through `baseOpts` so every git call inherits it; the
|
|
102
|
+
// injected `_git` mock in unit tests ignores opts so the seam is unaffected.
|
|
103
|
+
const LIVE_CHECKOUT_GIT_MAX_BUFFER = 50 * 1024 * 1024;
|
|
104
|
+
|
|
89
105
|
// PL-live-checkout-reliability-hardening — partial-clone / GVFS blob-fetch
|
|
90
106
|
// signature match. On a Scalar/GVFS-managed ADO repo (blobless partial clone),
|
|
91
107
|
// switching the working tree onto a branch whose tree differs from HEAD must
|
|
@@ -117,6 +133,37 @@ function _isPartialCloneBlobError(message = '') {
|
|
|
117
133
|
);
|
|
118
134
|
}
|
|
119
135
|
|
|
136
|
+
// W-mqvejug6000eeb20 — production default for resolving the live-checkout
|
|
137
|
+
// auto-reset decision when the caller (engine.js spawnAgent) does not pass an
|
|
138
|
+
// explicit `autoReset` boolean. engine.js is intentionally not threading the
|
|
139
|
+
// project/engine config into prepareLiveCheckout, so we self-resolve: load
|
|
140
|
+
// config.json, match the project by `localPath`, and defer to
|
|
141
|
+
// `shared.resolveLiveCheckoutAutoReset` (per-project boolean wins, else
|
|
142
|
+
// engine-level fallback, else false). Runs only on the dirty path (cold, not
|
|
143
|
+
// hot), and fails CLOSED — any error reading config returns false so a config
|
|
144
|
+
// glitch can never silently trigger a destructive reset.
|
|
145
|
+
function _defaultResolveAutoReset(localPath) {
|
|
146
|
+
try {
|
|
147
|
+
const config = shared.safeJson(path.join(shared.MINIONS_DIR, 'config.json')) || {};
|
|
148
|
+
const projects = shared.getProjects(config);
|
|
149
|
+
const norm = (p) => path.resolve(String(p || '')).replace(/\\/g, '/').toLowerCase();
|
|
150
|
+
const target = norm(localPath);
|
|
151
|
+
const project = projects.find((p) => p && p.localPath && norm(p.localPath) === target) || null;
|
|
152
|
+
return shared.resolveLiveCheckoutAutoReset(project, config.engine);
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// W-mqvejug6000eeb20 — production default inbox-note writer for the auto-reset
|
|
159
|
+
// audit trail. Lazily required to avoid a load-order cycle (dispatch.js does not
|
|
160
|
+
// require live-checkout.js, but the lazy require keeps this module importable in
|
|
161
|
+
// isolation / in tests that never trigger the write). Best-effort.
|
|
162
|
+
function _defaultWriteInboxNote(slug, content) {
|
|
163
|
+
// eslint-disable-next-line global-require
|
|
164
|
+
return require('./dispatch').writeInboxAlert(slug, content);
|
|
165
|
+
}
|
|
166
|
+
|
|
120
167
|
async function prepareLiveCheckout(opts = {}) {
|
|
121
168
|
const {
|
|
122
169
|
localPath,
|
|
@@ -124,10 +171,15 @@ async function prepareLiveCheckout(opts = {}) {
|
|
|
124
171
|
mainRef,
|
|
125
172
|
gitOpts,
|
|
126
173
|
dispatchId, // accepted for caller bookkeeping; not used by the helper
|
|
127
|
-
wiId, // accepted for caller bookkeeping;
|
|
174
|
+
wiId, // accepted for caller bookkeeping; used in the auto-reset note slug
|
|
128
175
|
log,
|
|
176
|
+
autoReset, // W-mqvejug6000eeb20: tri-state. `true`/`false` short-circuits the
|
|
177
|
+
// config-based resolution; `undefined` (the engine.js call shape)
|
|
178
|
+
// defers to `_resolveAutoReset`.
|
|
129
179
|
_git, // private injection for testing — defaults to shared.shellSafeGit
|
|
130
180
|
_exists, // private injection for testing — defaults to fs.existsSync
|
|
181
|
+
_resolveAutoReset, // private injection for testing — defaults to config-based resolver
|
|
182
|
+
_writeInboxNote, // private injection for testing — defaults to dispatch.writeInboxAlert
|
|
131
183
|
} = opts;
|
|
132
184
|
|
|
133
185
|
// ── Required-arg guards. Throw rather than return {ok:false} so a
|
|
@@ -154,7 +206,10 @@ async function prepareLiveCheckout(opts = {}) {
|
|
|
154
206
|
|
|
155
207
|
const git = (typeof _git === 'function') ? _git : shared.shellSafeGit;
|
|
156
208
|
const exists = (typeof _exists === 'function') ? _exists : fs.existsSync;
|
|
157
|
-
|
|
209
|
+
// W-mqvejug6000eeb20 — default maxBuffer (50 MB) so a large `git status
|
|
210
|
+
// --porcelain` never overflows execFile's 1 MB ceiling. A caller-supplied
|
|
211
|
+
// gitOpts.maxBuffer still wins (spread after the default).
|
|
212
|
+
const baseOpts = { cwd: localPath, maxBuffer: LIVE_CHECKOUT_GIT_MAX_BUFFER, ...(gitOpts || {}) };
|
|
158
213
|
|
|
159
214
|
// ── Step 1: git status --porcelain=v1 -b. Bail early on dirty tree. ─────
|
|
160
215
|
// Porcelain v1 -b adds a `## <branch>` header line as the first output line
|
|
@@ -174,7 +229,83 @@ async function prepareLiveCheckout(opts = {}) {
|
|
|
174
229
|
const branchInfo = statusLines.find((line) => line.startsWith('## ')) || '';
|
|
175
230
|
const dirtyFiles = statusLines.filter((line) => !line.startsWith('## '));
|
|
176
231
|
if (dirtyFiles.length > 0) {
|
|
177
|
-
|
|
232
|
+
// W-mqvejug6000eeb20 — opt-in auto-reset. Default behavior is to bail with
|
|
233
|
+
// reason:'dirty' (spawnAgent translates to non-retryable LIVE_CHECKOUT_DIRTY
|
|
234
|
+
// and alerts the operator). When auto-reset is enabled — either the caller
|
|
235
|
+
// passed an explicit `autoReset` boolean, or the config-based resolver says
|
|
236
|
+
// so — we DISCARD the dirty state via `git fetch origin` + `git reset --hard
|
|
237
|
+
// origin/<branch>` and continue. This is destructive (the operator's
|
|
238
|
+
// uncommitted work is gone), which is why it is strictly opt-in.
|
|
239
|
+
let wantAutoReset = false;
|
|
240
|
+
if (typeof autoReset === 'boolean') {
|
|
241
|
+
wantAutoReset = autoReset;
|
|
242
|
+
} else {
|
|
243
|
+
const resolver = (typeof _resolveAutoReset === 'function') ? _resolveAutoReset : _defaultResolveAutoReset;
|
|
244
|
+
try { wantAutoReset = !!resolver(localPath); } catch { wantAutoReset = false; }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!wantAutoReset) {
|
|
248
|
+
return { ok: false, reason: 'dirty', dirtyFiles, branchInfo };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (typeof log === 'function') {
|
|
252
|
+
log(`live-checkout auto-reset: discarding ${dirtyFiles.length} dirty path(s) on '${branchName}' via fetch + reset --hard origin/${branchName}`);
|
|
253
|
+
}
|
|
254
|
+
let resetOk = true;
|
|
255
|
+
try {
|
|
256
|
+
await git(['fetch', 'origin'], baseOpts);
|
|
257
|
+
await git(['reset', '--hard', `origin/${branchName}`], baseOpts);
|
|
258
|
+
} catch (e) {
|
|
259
|
+
resetOk = false;
|
|
260
|
+
if (typeof log === 'function') {
|
|
261
|
+
log(`live-checkout auto-reset FAILED (fetch/reset): ${e && e.message ? e.message : e}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Re-run the porcelain preflight once. If the tree is now clean we proceed;
|
|
266
|
+
// otherwise (reset failed, or something is still dirty) we fall back to the
|
|
267
|
+
// safe dirty refusal so we never dispatch onto an unexpected tree.
|
|
268
|
+
let stillDirty = dirtyFiles;
|
|
269
|
+
if (resetOk) {
|
|
270
|
+
try {
|
|
271
|
+
const recheckRaw = await git(['status', '--porcelain=v1', '-b'], baseOpts);
|
|
272
|
+
const recheckStr = typeof recheckRaw === 'string' ? recheckRaw : '';
|
|
273
|
+
stillDirty = recheckStr
|
|
274
|
+
.split(/\r?\n/)
|
|
275
|
+
.map((line) => line.replace(/\s+$/, ''))
|
|
276
|
+
.filter((line) => line.length > 0 && !line.startsWith('## '));
|
|
277
|
+
} catch (e) {
|
|
278
|
+
resetOk = false;
|
|
279
|
+
if (typeof log === 'function') {
|
|
280
|
+
log(`live-checkout auto-reset re-check FAILED: ${e && e.message ? e.message : e}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!resetOk || stillDirty.length > 0) {
|
|
286
|
+
return { ok: false, reason: 'dirty', dirtyFiles: stillDirty, branchInfo };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Audit trail: record the discarded paths so the operator can recover from
|
|
290
|
+
// reflog / understand why their tree changed. Best-effort, never throws.
|
|
291
|
+
try {
|
|
292
|
+
const writeNote = (typeof _writeInboxNote === 'function') ? _writeInboxNote : _defaultWriteInboxNote;
|
|
293
|
+
const slug = `live-checkout-autoreset-${wiId || dispatchId || 'unknown'}`;
|
|
294
|
+
const body = [
|
|
295
|
+
`# Live-checkout auto-reset on '${branchName}'`,
|
|
296
|
+
'',
|
|
297
|
+
`⚠️ The live-checkout tree at \`${localPath}\` was dirty at dispatch time and`,
|
|
298
|
+
'`liveCheckoutAutoReset` is enabled, so it was force-reset to',
|
|
299
|
+
`\`origin/${branchName}\`. **The following uncommitted changes were DISCARDED**`,
|
|
300
|
+
'(recover from `git reflog` / `git fsck --lost-found` if needed):',
|
|
301
|
+
'',
|
|
302
|
+
'```',
|
|
303
|
+
...dirtyFiles,
|
|
304
|
+
'```',
|
|
305
|
+
].join('\n');
|
|
306
|
+
writeNote(slug, body);
|
|
307
|
+
} catch { /* best-effort audit note */ }
|
|
308
|
+
// Fall through — tree is now clean, continue with normal preparation.
|
|
178
309
|
}
|
|
179
310
|
|
|
180
311
|
// ── Step 2: mid-operation / detached-HEAD preflight (P-b2e8d4a6). ──────
|
|
@@ -423,7 +554,7 @@ async function restoreLiveCheckoutAtDispatchEnd(opts = {}) {
|
|
|
423
554
|
const alert = (typeof writeInboxAlert === 'function') ? writeInboxAlert : () => {};
|
|
424
555
|
const proj = projectName || 'project';
|
|
425
556
|
const did = dispatchId || '(unknown)';
|
|
426
|
-
const baseOpts = { cwd: localPath, ...(gitOpts || {}) };
|
|
557
|
+
const baseOpts = { cwd: localPath, maxBuffer: LIVE_CHECKOUT_GIT_MAX_BUFFER, ...(gitOpts || {}) };
|
|
427
558
|
|
|
428
559
|
const result = { restored: false, reason: null, failureAlerted: false, fallbackAlerted: false };
|
|
429
560
|
|
package/engine/playbook.js
CHANGED
|
@@ -432,6 +432,9 @@ const PLAYBOOK_REQUIRED_VARS = {
|
|
|
432
432
|
'meeting-investigate': ['meeting_title', 'agenda'],
|
|
433
433
|
'meeting-debate': ['meeting_title', 'agenda'],
|
|
434
434
|
'meeting-conclude': ['meeting_title', 'agenda'],
|
|
435
|
+
// M005 — SHERLOC two-phase build-fix. Same required vars as fix (PR-context
|
|
436
|
+
// dispatch), but routed to build-fix-complex.md for multi-file failures.
|
|
437
|
+
'build-fix-complex': ['pr_id', 'pr_branch'],
|
|
435
438
|
};
|
|
436
439
|
|
|
437
440
|
/**
|
|
@@ -878,6 +881,7 @@ function renderPlaybook(type, vars) {
|
|
|
878
881
|
if (block) inertAppendices.push(block);
|
|
879
882
|
} catch (e) { log('warn', `handoff-context inject failed: ${e.message}`); }
|
|
880
883
|
}
|
|
884
|
+
|
|
881
885
|
// Inject KB guardrail
|
|
882
886
|
content += `\n\n---\n\n## Knowledge Base Rules\n\n`;
|
|
883
887
|
content += `**Never delete, move, or overwrite files in \`knowledge/\`.** The sweep (consolidation engine) is the only process that writes to \`knowledge/\`. If you think a KB file is wrong, note it in your learnings file — do not touch \`knowledge/\` directly.\n`;
|
|
@@ -1306,6 +1310,7 @@ function selectPlaybook(workType, item) {
|
|
|
1306
1310
|
if (workType === WORK_TYPE.FIX && hasPrContext) {
|
|
1307
1311
|
return 'fix';
|
|
1308
1312
|
}
|
|
1313
|
+
// M005 — SHERLOC: complex build-fix variant, always PR-context (mirrors FIX routing).
|
|
1309
1314
|
if (workType === WORK_TYPE.BUILD_FIX_COMPLEX && hasPrContext) {
|
|
1310
1315
|
return 'build-fix-complex';
|
|
1311
1316
|
}
|
package/engine/prd-store.js
CHANGED
|
@@ -205,7 +205,7 @@ function reconcilePrdsFromDisk() {
|
|
|
205
205
|
let mtime;
|
|
206
206
|
try { mtime = Math.round(fs.statSync(full).mtimeMs); } catch { continue; }
|
|
207
207
|
if (_reconcileMtimes.get(key) === mtime) continue; // unchanged since last seen
|
|
208
|
-
const prd =
|
|
208
|
+
const prd = require('./shared').safeJsonNoRestore(full);
|
|
209
209
|
if (!prd) continue;
|
|
210
210
|
try {
|
|
211
211
|
const { withTransaction } = require('./db');
|
package/engine/queries.js
CHANGED
|
@@ -2158,11 +2158,18 @@ function getPrdInfo(config) {
|
|
|
2158
2158
|
// two paths is proven by test/unit/db-phase10-read-flip.test.js.
|
|
2159
2159
|
const processPrd = (pf, archived, plan, mtimeMs) => {
|
|
2160
2160
|
if (!plan || !plan.missing_features) return;
|
|
2161
|
+
// Phase 10 step 4.2: archived-ness is the FLAG, not the directory. `archived`
|
|
2162
|
+
// is the physical-location signal (prd/ vs prd/archive/); a PRD archived
|
|
2163
|
+
// IN PLACE (stays in prd/ with archived:true / status:'archived') must still
|
|
2164
|
+
// render as archived. Derive the logical value here — the single funnel both
|
|
2165
|
+
// the disk-scan and SQL-mirror sources pass through. Equivalent for current
|
|
2166
|
+
// data (nothing is archived-in-place yet → isPrdArchived is false → ===archived).
|
|
2167
|
+
const isArch = archived || shared.isPrdArchived(plan);
|
|
2161
2168
|
// Staleness: gate on actual source-plan content change rather than mtime
|
|
2162
2169
|
// alone (W-mqfevwr60018bd09). Pure path / pure mtime drifts return
|
|
2163
2170
|
// stale=false so a repointed source_plan doesn't masquerade as a revision.
|
|
2164
2171
|
let planStale = false;
|
|
2165
|
-
if (!
|
|
2172
|
+
if (!isArch && plan.source_plan) {
|
|
2166
2173
|
try { planStale = shared.isSourcePlanContentStale(PLANS_DIR, plan).stale; } catch { /* optional */ }
|
|
2167
2174
|
}
|
|
2168
2175
|
existingPrds.push({
|
|
@@ -2170,7 +2177,7 @@ function getPrdInfo(config) {
|
|
|
2170
2177
|
status: plan.status || 'active',
|
|
2171
2178
|
planStale: planStale || plan.planStale || false,
|
|
2172
2179
|
completedAt: plan.completedAt || '',
|
|
2173
|
-
_archived:
|
|
2180
|
+
_archived: isArch,
|
|
2174
2181
|
});
|
|
2175
2182
|
if (Array.isArray(plan.verifyPrs) && plan.verifyPrs.length > 0) {
|
|
2176
2183
|
verifyPrsByPlan[pf] = plan.verifyPrs.map(r => ({
|
|
@@ -2183,7 +2190,7 @@ function getPrdInfo(config) {
|
|
|
2183
2190
|
allPrdItems.push({
|
|
2184
2191
|
...f, _source: pf, _planStatus: plan.status || 'active',
|
|
2185
2192
|
_planSummary: plan.plan_summary || pf, _planProject: plan.project || '',
|
|
2186
|
-
_archived:
|
|
2193
|
+
_archived: isArch, _sourcePlan: plan.source_plan || '',
|
|
2187
2194
|
_branchStrategy: plan.branch_strategy || 'parallel',
|
|
2188
2195
|
_planStale: planStale || plan.planStale || false, _lastSyncedFromPlan: plan.lastSyncedFromPlan || null,
|
|
2189
2196
|
_prdUpdatedAt: new Date(mtimeMs).toISOString(),
|
package/engine/shared.js
CHANGED
|
@@ -744,17 +744,46 @@ function safeReadDir(dir) {
|
|
|
744
744
|
// also fall back to plans/archive/<basename> so a silently-relocated
|
|
745
745
|
// source plan no longer spams ENOENT warnings on every tick.
|
|
746
746
|
|
|
747
|
+
/**
|
|
748
|
+
* Canonical association key for a plan/PRD source reference: the bare basename,
|
|
749
|
+
* stripped of any directory AND the legacy `plans/` (or `plans/archive/`) prefix
|
|
750
|
+
* some PRDs carry in their `source_plan` field. This is THE single key for
|
|
751
|
+
* matching a plan markdown file to the PRD(s) that point at it.
|
|
752
|
+
*
|
|
753
|
+
* It exists because `source_plan` is written inconsistently — sometimes a bare
|
|
754
|
+
* `x.md`, sometimes a prefixed `plans/x.md` (pre-#415). Comparing the raw
|
|
755
|
+
* strings (`prd.source_plan === planFile`) silently fails across the two forms,
|
|
756
|
+
* which let an archived plan keep a LIVE PRD — the plan↔PRD status desync behind
|
|
757
|
+
* the "old PRDs came back" resurrection. Always compare/derive via this helper.
|
|
758
|
+
*/
|
|
759
|
+
function sourcePlanKey(ref) {
|
|
760
|
+
return path.basename(String(ref || ''));
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* True when a PRD's `source_plan` references the given plan file, tolerant of
|
|
765
|
+
* the `plans/` prefix + directory differences. Use instead of raw `===`.
|
|
766
|
+
*/
|
|
767
|
+
function prdMatchesSourcePlan(sourcePlanRef, planFile) {
|
|
768
|
+
const a = sourcePlanKey(sourcePlanRef);
|
|
769
|
+
const b = sourcePlanKey(planFile);
|
|
770
|
+
return !!a && a === b;
|
|
771
|
+
}
|
|
772
|
+
|
|
747
773
|
/**
|
|
748
774
|
* Resolve a PRD's `source_plan` field to an absolute file path under
|
|
749
775
|
* `plansDir`. Tries the direct join first; falls back to
|
|
750
|
-
* plansDir/archive/<basename
|
|
751
|
-
*
|
|
776
|
+
* plansDir/archive/<basename> when the direct path is missing. The direct join
|
|
777
|
+
* uses the canonical key so a `plans/`-prefixed source_plan resolves correctly
|
|
778
|
+
* (a raw join would yield plansDir/plans/x.md). Returns null when neither
|
|
779
|
+
* location resolves to a regular file.
|
|
752
780
|
*/
|
|
753
781
|
function resolveSourcePlanPath(plansDir, sourcePlan) {
|
|
754
782
|
if (!plansDir || !sourcePlan || typeof sourcePlan !== 'string') return null;
|
|
755
|
-
const
|
|
783
|
+
const key = sourcePlanKey(sourcePlan);
|
|
784
|
+
const direct = path.join(plansDir, key);
|
|
756
785
|
try { if (fs.statSync(direct).isFile()) return direct; } catch { /* miss */ }
|
|
757
|
-
const archived = path.join(plansDir, 'archive',
|
|
786
|
+
const archived = path.join(plansDir, 'archive', key);
|
|
758
787
|
try { if (fs.statSync(archived).isFile()) return archived; } catch { /* miss */ }
|
|
759
788
|
return null;
|
|
760
789
|
}
|
|
@@ -2677,6 +2706,26 @@ function isLiveCheckoutProject(project) {
|
|
|
2677
2706
|
return resolveCheckoutMode(project) === CHECKOUT_MODES.LIVE;
|
|
2678
2707
|
}
|
|
2679
2708
|
|
|
2709
|
+
// W-mqvejug6000eeb20 — resolve the effective live-checkout auto-reset decision.
|
|
2710
|
+
// Precedence: an explicit per-project boolean (`project.liveCheckoutAutoReset`)
|
|
2711
|
+
// wins; otherwise the fleet-wide `engine.liveCheckoutAutoReset` applies;
|
|
2712
|
+
// otherwise `false`. Only an explicit boolean counts as "set" at either level —
|
|
2713
|
+
// anything else (undefined / null / string) is treated as unset so the next
|
|
2714
|
+
// tier (or the `false` default) decides. When ON and a live-checkout tree is
|
|
2715
|
+
// dirty, `engine/live-checkout.js#prepareLiveCheckout` runs
|
|
2716
|
+
// `git fetch origin` + `git reset --hard origin/<branch>` instead of refusing
|
|
2717
|
+
// with LIVE_CHECKOUT_DIRTY (the reset DISCARDS the operator's uncommitted work,
|
|
2718
|
+
// so it is opt-in). Sibling of `resolveCheckoutMode` — pure, no I/O.
|
|
2719
|
+
function resolveLiveCheckoutAutoReset(project, engine) {
|
|
2720
|
+
if (project && typeof project === 'object' && typeof project.liveCheckoutAutoReset === 'boolean') {
|
|
2721
|
+
return project.liveCheckoutAutoReset;
|
|
2722
|
+
}
|
|
2723
|
+
if (engine && typeof engine === 'object' && typeof engine.liveCheckoutAutoReset === 'boolean') {
|
|
2724
|
+
return engine.liveCheckoutAutoReset;
|
|
2725
|
+
}
|
|
2726
|
+
return false;
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2680
2729
|
function validateCheckoutMode(value) {
|
|
2681
2730
|
if (value === undefined || value === null || value === '') return undefined;
|
|
2682
2731
|
if (typeof value !== 'string') {
|
|
@@ -3143,6 +3192,15 @@ const ENGINE_DEFAULTS = {
|
|
|
3143
3192
|
// itself ALWAYS runs on every orphan-sweep escalation (the flag only gates
|
|
3144
3193
|
// the kill); holder details are appended to the inbox note either way.
|
|
3145
3194
|
autoReapOrphanWorktreeHolders: false,
|
|
3195
|
+
// W-mqvejug6000eeb20 — fleet-wide fallback for live-checkout auto-reset.
|
|
3196
|
+
// When ON, a dirty/broken live-checkout tree is `git fetch origin` +
|
|
3197
|
+
// `git reset --hard origin/<branch>`'d before dispatch instead of failing
|
|
3198
|
+
// LIVE_CHECKOUT_DIRTY. Per-project `project.liveCheckoutAutoReset` overrides
|
|
3199
|
+
// this. Default OFF — auto-reset is destructive (it discards the operator's
|
|
3200
|
+
// uncommitted changes), so it is strictly opt-in. Resolution precedence lives
|
|
3201
|
+
// in `resolveLiveCheckoutAutoReset`. Fires ONLY on confirmed dirty-tree
|
|
3202
|
+
// detection, never on mid-operation / blob-fetch / tooling failures.
|
|
3203
|
+
liveCheckoutAutoReset: false,
|
|
3146
3204
|
orphanHolderScanTimeoutMs: 5000, // 5s ceiling for the cross-platform holder scan (PowerShell / /proc walk / lsof)
|
|
3147
3205
|
ccMaxTurns: 50, // max tool-use turns per CC/doc-chat call before CLI stops (per response, not per session)
|
|
3148
3206
|
ccTurnTimeoutMs: 300000, // W-mpmwxni2000c25c7-b/-d: 5min per-turn no-progress watchdog. The window resets on every liveness signal — token chunk, tool-call notification, tool-update — so an actively-streaming CC/doc-chat turn (long shell command, deep search, sub-agent loop) survives indefinitely up to the outer CC_CALL_TIMEOUT_MS (~1h) ceiling. Only true silence past this window with no progress fires the cancel: the in-flight LLM call is aborted and the handler surfaces `{code:'cc-turn-timeout', retryable:true}` via the typed error envelope so the UI can stop the spinner and offer Retry. Clamped to [10000, 3600000] in the settings POST handler. Independent of CC_CALL_TIMEOUT_MS. Non-streaming doc-chat is the lone wall-clock exception (no progress hooks); see _raceCcDocChatTimeout in dashboard.js for the dual factory/promise shape.
|
|
@@ -3804,6 +3862,44 @@ const DONE_STATUSES = new Set([WI_STATUS.DONE, 'in-pr', 'implemented', 'complete
|
|
|
3804
3862
|
// Terminal statuses for plan completion — item won't progress further (done, failed, cancelled).
|
|
3805
3863
|
// Used by checkPlanCompletion to unblock the gate when items are in an unrecoverable state.
|
|
3806
3864
|
const PLAN_TERMINAL_STATUSES = new Set([...DONE_STATUSES, WI_STATUS.FAILED, WI_STATUS.CANCELLED]);
|
|
3865
|
+
|
|
3866
|
+
// True when a PRD is archived — by an explicit flag or the 'archived' top-level
|
|
3867
|
+
// status. (Distinct from the directory: a PRD JSON can carry this regardless of
|
|
3868
|
+
// whether it physically sits in prd/ or prd/archive/.)
|
|
3869
|
+
function isPrdArchived(plan) {
|
|
3870
|
+
return !!plan && typeof plan === 'object' && (plan.archived === true || plan.status === 'archived');
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3873
|
+
// True when a PRD must NOT be (re)materialized or re-verified because it is
|
|
3874
|
+
// DEFUNCT — a dead leftover whose plan is no longer live. This is the
|
|
3875
|
+
// resurrection-re-execution guard (the "old PRDs came back" class): the
|
|
3876
|
+
// autonomous fleet keeps committing prd/*.json onto feature/cc-pr/backport
|
|
3877
|
+
// branches (vector 2) and checking those branches out IN the operator's live
|
|
3878
|
+
// checkout, which restores dead PRD files into prd/. The materializer then
|
|
3879
|
+
// re-creates and re-runs already-finished work — regardless of the resurrected
|
|
3880
|
+
// PRD's own status (awaiting-approval / undefined / completed), so a status-only
|
|
3881
|
+
// check is not enough.
|
|
3882
|
+
//
|
|
3883
|
+
// Definition (plan↔PRD status is meant to stay synced — see prdMatchesSourcePlan):
|
|
3884
|
+
// • archived flag/status → defunct; else
|
|
3885
|
+
// • plan markdown is LIVE in plansDir → NEVER defunct (active PRD / legit
|
|
3886
|
+
// re-open keeps its plan live — this is the catastrophe guard: we never flag
|
|
3887
|
+
// a PRD whose plan is present); else
|
|
3888
|
+
// • plan is gone from plansDir AND (it's positively in plansDir/archive/, OR
|
|
3889
|
+
// the PRD is completed) → defunct.
|
|
3890
|
+
// On any fs uncertainty we DON'T flag (favor not blocking real work).
|
|
3891
|
+
function isDefunctPrd(plan, plansDir) {
|
|
3892
|
+
if (isPrdArchived(plan)) return true;
|
|
3893
|
+
if (!plan || typeof plan !== 'object' || !plansDir) return false;
|
|
3894
|
+
const sp = sourcePlanKey(plan.source_plan || plan.sourcePlan);
|
|
3895
|
+
if (!sp) return false;
|
|
3896
|
+
let liveExists;
|
|
3897
|
+
try { liveExists = fs.existsSync(path.join(plansDir, sp)); } catch { return false; }
|
|
3898
|
+
if (liveExists) return false; // plan is live → active PRD, never defunct
|
|
3899
|
+
let archivedExists = false;
|
|
3900
|
+
try { archivedExists = fs.existsSync(path.join(plansDir, 'archive', sp)); } catch { /* treat as absent */ }
|
|
3901
|
+
return archivedExists || plan.status === 'completed';
|
|
3902
|
+
}
|
|
3807
3903
|
const WORK_TYPE = {
|
|
3808
3904
|
IMPLEMENT: 'implement', IMPLEMENT_LARGE: 'implement:large', FIX: 'fix', REVIEW: 'review',
|
|
3809
3905
|
VERIFY: 'verify', PLAN: 'plan', PLAN_TO_PRD: 'plan-to-prd', DECOMPOSE: 'decompose',
|
|
@@ -8850,7 +8946,7 @@ module.exports = {
|
|
|
8850
8946
|
runtimeConfigWarnings,
|
|
8851
8947
|
projectWorkSourceWarnings,
|
|
8852
8948
|
backfillProjectWorkSourceDefaults,
|
|
8853
|
-
WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, WORK_TYPE, WORKTREE_REQUIRING_TYPES, VALID_WORK_TYPES, resolveWorkItemTypeFromPrdItem, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, PR_POLLABLE_STATUSES, PR_PENDING_REASON, BUILD_STATUS, REVIEW_STATUS, FETCH_TIMEOUT_MS, RETRY_DELAY_MS, ADO_TOKEN_REFRESH_MAX_RETRIES, DISPATCH_RESULT, mutateMetrics, mutateWatches, mutateScheduleRuns, mutatePipelineRuns, mutateManagedProcesses, mutateWorktreePool, mutateQaRuns, mutateQaSessions, trackReviewMetric, queuePlanToPrd, extractPlanDeclaredProject, extractPlanTargetProjects,
|
|
8949
|
+
WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, isPrdArchived, isDefunctPrd, WORK_TYPE, WORKTREE_REQUIRING_TYPES, VALID_WORK_TYPES, resolveWorkItemTypeFromPrdItem, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, PR_POLLABLE_STATUSES, PR_PENDING_REASON, BUILD_STATUS, REVIEW_STATUS, FETCH_TIMEOUT_MS, RETRY_DELAY_MS, ADO_TOKEN_REFRESH_MAX_RETRIES, DISPATCH_RESULT, mutateMetrics, mutateWatches, mutateScheduleRuns, mutatePipelineRuns, mutateManagedProcesses, mutateWorktreePool, mutateQaRuns, mutateQaSessions, trackReviewMetric, queuePlanToPrd, extractPlanDeclaredProject, extractPlanTargetProjects,
|
|
8854
8950
|
WATCH_STATUS, WATCH_TARGET_TYPE, WATCH_CONDITION, WATCH_ABSOLUTE_CONDITIONS, WATCH_ACTION_TYPE,
|
|
8855
8951
|
WATCH_STALLED_DEFAULT_TICKS, WATCH_STUCK_STAGE_DEFAULT_TICKS,
|
|
8856
8952
|
PIPELINE_STATUS, STAGE_TYPE, MEETING_STATUS, AGENT_STATUS,
|
|
@@ -8927,6 +9023,8 @@ module.exports = {
|
|
|
8927
9023
|
getAdoOrgBase,
|
|
8928
9024
|
sanitizePath,
|
|
8929
9025
|
resolveSourcePlanPath, // W-mqfevwr60018bd09 — PRD staleness: resolve source_plan to absolute path (archive-aware)
|
|
9026
|
+
sourcePlanKey, prdMatchesSourcePlan, // canonical plan↔PRD association key (prefix/dir-tolerant) — keeps plan+PRD status synced
|
|
9027
|
+
|
|
8930
9028
|
computeSourcePlanContentHash, // W-mqfevwr60018bd09 — sha256 of source plan markdown body
|
|
8931
9029
|
isSourcePlanContentStale, // W-mqfevwr60018bd09 — gate destructive PRD resync on actual content change
|
|
8932
9030
|
sanitizeBranch,
|
|
@@ -8970,6 +9068,7 @@ module.exports = {
|
|
|
8970
9068
|
validateCheckoutMode,
|
|
8971
9069
|
resolveCheckoutMode,
|
|
8972
9070
|
isLiveCheckoutProject,
|
|
9071
|
+
resolveLiveCheckoutAutoReset,
|
|
8973
9072
|
validatePid,
|
|
8974
9073
|
PR_FIX_CAUSE,
|
|
8975
9074
|
getPrFixAutomationCause,
|