@yemi33/minions 0.1.2370 → 0.1.2372
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 +38 -11
- package/dashboard/js/render-other.js +2 -30
- package/dashboard/js/render-work-items.js +0 -34
- package/dashboard/js/settings.js +12 -27
- package/dashboard/pages/tools.html +1 -1
- package/dashboard.js +80 -257
- package/docs/README.md +2 -3
- package/docs/architecture.excalidraw +2 -2
- package/docs/auto-discovery.md +3 -3
- package/docs/completion-reports.md +0 -121
- package/docs/copilot-cli-schema.md +9 -17
- package/docs/deprecated.json +0 -51
- package/docs/design-state-storage.md +4 -4
- package/docs/harness-propagation.md +33 -263
- package/docs/human-vs-automated.md +1 -1
- package/docs/named-agents.md +0 -2
- package/docs/plan-lifecycle.md +5 -6
- package/docs/qa-runbook-lifecycle.md +10 -25
- package/docs/shared-lifecycle-module-map.md +2 -10
- package/engine/ado-comment.js +5 -11
- package/engine/ado.js +10 -5
- package/engine/cleanup.js +16 -36
- package/engine/cli.js +13 -175
- package/engine/comment-format.js +8 -182
- package/engine/db/index.js +60 -10
- package/engine/db/migrations/018-sql-only-cutover.js +56 -0
- package/engine/dispatch-store.js +0 -114
- package/engine/dispatch.js +1 -6
- package/engine/gh-comment.js +2 -10
- package/engine/github.js +8 -6
- package/engine/lifecycle.js +58 -173
- package/engine/llm.js +9 -11
- package/engine/managed-spawn.js +1 -6
- package/engine/memory-store.js +8 -6
- package/engine/metrics-store.js +4 -128
- package/engine/pipeline.js +4 -7
- package/engine/playbook.js +8 -196
- package/engine/prd-store.js +115 -141
- package/engine/preflight.js +8 -55
- package/engine/projects.js +34 -41
- package/engine/pull-requests-store.js +9 -234
- package/engine/qa-runs.js +4 -15
- package/engine/qa-sessions.js +5 -17
- package/engine/queries.js +23 -103
- package/engine/routing.js +1 -1
- package/engine/runtimes/claude.js +1 -2
- package/engine/runtimes/codex.js +0 -2
- package/engine/runtimes/copilot.js +1 -4
- package/engine/shared-branch-pr-reconcile.js +2 -5
- package/engine/shared.js +174 -533
- package/engine/small-state-store.js +12 -647
- package/engine/spawn-agent.js +5 -96
- package/engine/state-operations.js +166 -0
- package/engine/supervisor.js +0 -1
- package/engine/watch-actions.js +2 -3
- package/engine/watches-store.js +2 -128
- package/engine/work-items-store.js +3 -254
- package/engine.js +102 -334
- package/package.json +2 -2
- package/playbooks/build-fix-complex.md +0 -4
- package/playbooks/fix.md +0 -4
- package/playbooks/implement-shared.md +0 -4
- package/playbooks/implement.md +0 -4
- package/playbooks/plan-to-prd.md +16 -11
- package/playbooks/plan.md +0 -4
- package/playbooks/review.md +0 -6
- package/playbooks/shared-rules.md +0 -65
- package/docs/harness-transparency.md +0 -199
- package/docs/project-skills.md +0 -193
- package/engine/discover-project-skills.js +0 -673
- package/engine/playbook-intents.js +0 -76
package/engine.js
CHANGED
|
@@ -35,10 +35,11 @@ const { exec, execAsync, execSilent, runFile, ts, ENGINE_DEFAULTS,
|
|
|
35
35
|
WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, REVIEW_STATUS, DISPATCH_RESULT, AGENT_STATUS,
|
|
36
36
|
FAILURE_CLASS, resolvePollFlag } = shared;
|
|
37
37
|
const { resolveRuntime } = require('./engine/runtimes');
|
|
38
|
-
const { assertStaleHeadOk, preApproveWorkspaceMcps
|
|
38
|
+
const { assertStaleHeadOk, preApproveWorkspaceMcps } = require('./engine/spawn-agent');
|
|
39
39
|
const adoGitAuth = require('./engine/ado-git-auth');
|
|
40
40
|
const queries = require('./engine/queries');
|
|
41
41
|
const dispatchEvents = require('./engine/dispatch-events');
|
|
42
|
+
const prdStore = require('./engine/prd-store');
|
|
42
43
|
|
|
43
44
|
// ─── Paths ──────────────────────────────────────────────────────────────────
|
|
44
45
|
|
|
@@ -441,7 +442,6 @@ function _buildAgentSpawnFlags(runtime, opts = {}) {
|
|
|
441
442
|
if (caps.fallbackModel && opts.fallbackModel) flags.push('--fallback-model', String(opts.fallbackModel));
|
|
442
443
|
if (opts.stream != null && opts.stream !== '') flags.push('--stream', String(opts.stream));
|
|
443
444
|
if (opts.disableBuiltinMcps === true) flags.push('--disable-builtin-mcps');
|
|
444
|
-
if (opts.suppressAgentsMd === true) flags.push('--no-custom-instructions');
|
|
445
445
|
if (opts.reasoningSummaries === true) flags.push('--enable-reasoning-summaries');
|
|
446
446
|
|
|
447
447
|
return flags;
|
|
@@ -1033,36 +1033,50 @@ async function runWorktreeAdd(rootDir, worktreePath, addArgs, gitOpts, worktreeC
|
|
|
1033
1033
|
// probe failure against a branch that genuinely exists on origin (e.g. a
|
|
1034
1034
|
// PR-author's real branch) silently took the fresh-branch fallback and
|
|
1035
1035
|
// forked a divergent local branch under the same name instead of retrying.
|
|
1036
|
-
// We retry
|
|
1037
|
-
//
|
|
1038
|
-
//
|
|
1036
|
+
// We retry with EXPONENTIAL backoff (mirroring `_fetchWithTransientRetry`'s
|
|
1037
|
+
// transient-recovery intent): 3 total attempts with ~1.5s then ~4s backoff.
|
|
1038
|
+
// A single flat 1.5s retry (the pre-W-mrdwcpzn shape) landed inside the same
|
|
1039
|
+
// brief ADO ls-remote flaky window often enough to hard-fail worktree creation
|
|
1040
|
+
// twice within an hour against office/iss/fluid-client-framework — live repro
|
|
1041
|
+
// confirmed the command succeeds reliably when retried a few seconds later, so
|
|
1042
|
+
// the deeper/backed-off retry closes that gap. If NONE of the attempts cleanly
|
|
1043
|
+
// resolve to "not found", we throw so the caller does NOT treat this as a
|
|
1044
|
+
// confirmed absence.
|
|
1045
|
+
const _PROBE_BRANCH_BACKOFFS_MS = [1500, 4000]; // delays BEFORE attempts 2 and 3 (3 total attempts)
|
|
1039
1046
|
async function probeBranchOnRemote(rootDir, branchName, gitOpts) {
|
|
1040
1047
|
if (!branchName || !rootDir) return false;
|
|
1041
1048
|
const _attemptProbe = () => shared.shellSafeGit(
|
|
1042
1049
|
['ls-remote', '--exit-code', '--heads', 'origin', branchName],
|
|
1043
1050
|
{ ...gitOpts, cwd: rootDir, timeout: 10000 },
|
|
1044
1051
|
);
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
} catch (e) {
|
|
1049
|
-
if (e && e.code === 2) return false; // clean, authoritative "not found"
|
|
1050
|
-
const firstErrMsg = adoGitAuth.redactBearer(String(e?.message || e)).split('\n')[0].slice(0, 200);
|
|
1051
|
-
log('warn', `probeBranchOnRemote: ls-remote --heads origin ${branchName} failed (${firstErrMsg}) — retrying once after 1.5s before giving up`);
|
|
1052
|
-
await new Promise(r => setTimeout(r, 1500));
|
|
1052
|
+
const totalAttempts = _PROBE_BRANCH_BACKOFFS_MS.length + 1;
|
|
1053
|
+
let lastErr = null;
|
|
1054
|
+
for (let attempt = 1; attempt <= totalAttempts; attempt++) {
|
|
1053
1055
|
try {
|
|
1054
1056
|
await _attemptProbe();
|
|
1057
|
+
if (attempt > 1) log('info', `probeBranchOnRemote: ls-remote --heads origin ${branchName} succeeded on attempt ${attempt}/${totalAttempts}`);
|
|
1055
1058
|
return true;
|
|
1056
|
-
} catch (
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1059
|
+
} catch (e) {
|
|
1060
|
+
// exit code 2 = clean, authoritative "not found". This short-circuits
|
|
1061
|
+
// immediately on ANY attempt with no further retry/backoff — a confirmed
|
|
1062
|
+
// absence is authoritative and must stay fast (no #767 regression).
|
|
1063
|
+
if (e && e.code === 2) return false;
|
|
1064
|
+
lastErr = e;
|
|
1065
|
+
const errMsg = adoGitAuth.redactBearer(String(e?.message || e)).split('\n')[0].slice(0, 200);
|
|
1066
|
+
const backoffMs = _PROBE_BRANCH_BACKOFFS_MS[attempt - 1];
|
|
1067
|
+
if (backoffMs != null) {
|
|
1068
|
+
log('warn', `probeBranchOnRemote: ls-remote --heads origin ${branchName} failed on attempt ${attempt}/${totalAttempts} (${errMsg}) — retrying after ${backoffMs}ms`);
|
|
1069
|
+
await new Promise(r => setTimeout(r, backoffMs));
|
|
1070
|
+
} else {
|
|
1071
|
+
log('warn', `probeBranchOnRemote: ls-remote --heads origin ${branchName} failed on final attempt ${attempt}/${totalAttempts} (${errMsg}) — cannot confirm branch absence, surfacing error`);
|
|
1072
|
+
}
|
|
1064
1073
|
}
|
|
1065
1074
|
}
|
|
1075
|
+
const finalErrMsg = adoGitAuth.redactBearer(String(lastErr?.message || lastErr)).split('\n')[0].slice(0, 200);
|
|
1076
|
+
const probeErr = new Error(`probeBranchOnRemote: could not determine whether origin has ${branchName} after ${totalAttempts} attempts: ${finalErrMsg}`);
|
|
1077
|
+
probeErr._probeFailed = true;
|
|
1078
|
+
probeErr._cause = lastErr;
|
|
1079
|
+
throw probeErr;
|
|
1066
1080
|
}
|
|
1067
1081
|
|
|
1068
1082
|
// Detect and remove worktree registrations whose backing directory is missing
|
|
@@ -2080,38 +2094,6 @@ async function recoverPartialWorktree(rootDir, worktreePath, branchName, gitOpts
|
|
|
2080
2094
|
}
|
|
2081
2095
|
}
|
|
2082
2096
|
|
|
2083
|
-
// P-f52d81ba — persist the dispatch's detected monorepo sub-project (+ any per-surface
|
|
2084
|
-
// skill-discovery truncations the render surfaced) onto the dispatch record so
|
|
2085
|
-
// later grounding/observability can see which sub-project scoped discovery + harness
|
|
2086
|
-
// propagation. Best-effort: mutation failures must never block the spawn. Only
|
|
2087
|
-
// writes fields that carry a value so unset sub-projects leave the record untouched.
|
|
2088
|
-
function _recordDispatchDiscoveryDiagnostics(id, dispatchItem, subproject, diagnostics) {
|
|
2089
|
-
try {
|
|
2090
|
-
const dominantSubproject = (typeof subproject === 'string' && subproject) ? subproject : undefined;
|
|
2091
|
-
const truncations = diagnostics && Array.isArray(diagnostics.truncations) && diagnostics.truncations.length
|
|
2092
|
-
? diagnostics.truncations
|
|
2093
|
-
: undefined;
|
|
2094
|
-
if (dominantSubproject === undefined && truncations === undefined) return;
|
|
2095
|
-
mutateDispatch((dispatch) => {
|
|
2096
|
-
for (const queue of ['pending', 'active', 'completed']) {
|
|
2097
|
-
const arr = Array.isArray(dispatch?.[queue]) ? dispatch[queue] : null;
|
|
2098
|
-
if (!arr) continue;
|
|
2099
|
-
const found = arr.find(d => d && d.id === id);
|
|
2100
|
-
if (!found) continue;
|
|
2101
|
-
if (dominantSubproject !== undefined) found._dominantSubproject = dominantSubproject;
|
|
2102
|
-
if (truncations !== undefined) found._skillDiscoveryTruncations = truncations;
|
|
2103
|
-
}
|
|
2104
|
-
return dispatch;
|
|
2105
|
-
});
|
|
2106
|
-
if (dispatchItem) {
|
|
2107
|
-
if (dominantSubproject !== undefined) dispatchItem._dominantSubproject = dominantSubproject;
|
|
2108
|
-
if (truncations !== undefined) dispatchItem._skillDiscoveryTruncations = truncations;
|
|
2109
|
-
}
|
|
2110
|
-
} catch (e) {
|
|
2111
|
-
log('warn', `record discovery diagnostics failed for ${id} (non-fatal): ${e.message}`);
|
|
2112
|
-
}
|
|
2113
|
-
}
|
|
2114
|
-
|
|
2115
2097
|
// Seed a spawned agent's COPILOT_HOME mcp-config as a copy of the operator's
|
|
2116
2098
|
// `~/.copilot/mcp-config.json` MINUS the servers in
|
|
2117
2099
|
// `engine.copilotAgentDisabledMcpServers`, then point COPILOT_HOME at it.
|
|
@@ -2119,11 +2101,8 @@ function _recordDispatchDiscoveryDiagnostics(id, dispatchItem, subproject, diagn
|
|
|
2119
2101
|
// Why config-filtering and not `--disable-mcp-server`: copilot SPAWNS every
|
|
2120
2102
|
// server in its config on startup and authenticates it; `--disable-mcp-server
|
|
2121
2103
|
// <name>` only hides that server's TOOLS from the model, it does NOT stop the
|
|
2122
|
-
// server process
|
|
2123
|
-
//
|
|
2124
|
-
// an unbounded popup storm at minions' spawn cadence. A server simply ABSENT
|
|
2125
|
-
// from the config can't be spawned, so filtering the config is the only real
|
|
2126
|
-
// disable lever.
|
|
2104
|
+
// server process (PR #321 — verified live: agents spawned azmcp.exe/workiq.exe
|
|
2105
|
+
// despite those servers being in `copilotAgentDisabledMcpServers`).
|
|
2127
2106
|
//
|
|
2128
2107
|
// Default disabled list `[]` => agents inherit ALL of the operator's MCP servers
|
|
2129
2108
|
// (non-breaking; matches interactive copilot). Add a name to disable it for
|
|
@@ -3300,7 +3279,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3300
3279
|
// the dispatch-end auto-restore (P-d9e6b2c4) can return the tree to where the
|
|
3301
3280
|
// operator started — even after an engine restart + re-attach, where the
|
|
3302
3281
|
// spawnAgent closure is gone and only the persisted dispatch record survives
|
|
3303
|
-
//
|
|
3282
|
+
// even after an engine restart + re-attach.
|
|
3304
3283
|
if (_liveResult.originalRef) {
|
|
3305
3284
|
dispatchItem.originalRef = _liveResult.originalRef;
|
|
3306
3285
|
dispatchItem.originalRefType = _liveResult.originalRefType || 'branch';
|
|
@@ -4365,45 +4344,6 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
4365
4344
|
|
|
4366
4345
|
updateAgentStatus(id, AGENT_STATUS.READY, 'Worktree ready, preparing to spawn process');
|
|
4367
4346
|
|
|
4368
|
-
// ── P-f52d81ba — derive the effective monorepo sub-project for this dispatch ──────
|
|
4369
|
-
// Feeds BOTH project-skill discovery (scopeSubproject → sub-project's skills lead the
|
|
4370
|
-
// rendered project_skills_block) and project-local harness propagation
|
|
4371
|
-
// (workdir clip → only the sub-project's --project-harness-dir roots are surfaced).
|
|
4372
|
-
// Precedence:
|
|
4373
|
-
// 1. Explicit meta.workdir wins — effective sub-project = its first segment. No
|
|
4374
|
-
// diff work (the operator already declared the target sub-package).
|
|
4375
|
-
// 2. Else best-effort from the CURRENT dispatch's diff: `git diff
|
|
4376
|
-
// --name-only <main>...HEAD` in this worktree → detectDominantSubproject.
|
|
4377
|
-
// 3. Else '' (flat discovery / unclipped harness — today's behavior).
|
|
4378
|
-
// Contract: never blocks dispatch, never changes agent cwd (only an explicit
|
|
4379
|
-
// meta.workdir moves cwd, applied separately below), and degrades to '' on
|
|
4380
|
-
// any failure. Derived from THIS worktree's HEAD so a follow-up/parent's
|
|
4381
|
-
// stale diff can't leak in (AC: current-dispatch diff only).
|
|
4382
|
-
let _effectiveSubproject = '';
|
|
4383
|
-
try {
|
|
4384
|
-
if (validatedWorkdir) {
|
|
4385
|
-
_effectiveSubproject = String(validatedWorkdir).replace(/\\/g, '/').replace(/^\/+/, '').split('/')[0] || '';
|
|
4386
|
-
} else if (worktreePath && fs.existsSync(worktreePath) && project?.localPath) {
|
|
4387
|
-
let _subprojectChangedPaths = [];
|
|
4388
|
-
try {
|
|
4389
|
-
const _subprojectBase = sanitizeBranch(shared.resolveMainBranch(rootDir, project.mainBranch));
|
|
4390
|
-
const _subprojectDiff = await execAsync(`git diff --name-only ${_subprojectBase}...HEAD`, { ..._gitOpts, cwd: worktreePath, timeout: 10000 });
|
|
4391
|
-
_subprojectChangedPaths = (_subprojectDiff.stdout || '').split('\n').map(s => s.trim()).filter(Boolean);
|
|
4392
|
-
} catch (e) {
|
|
4393
|
-
log('debug', `subproject-detect: git diff failed for ${id} (flat discovery): ${e.message}`);
|
|
4394
|
-
}
|
|
4395
|
-
if (_subprojectChangedPaths.length) {
|
|
4396
|
-
const { detectDominantSubproject } = require('./engine/discover-project-skills')._internal;
|
|
4397
|
-
const _det = detectDominantSubproject({ projectPath: project.localPath, changedPaths: _subprojectChangedPaths });
|
|
4398
|
-
if (_det && _det.confident && _det.subproject) _effectiveSubproject = _det.subproject;
|
|
4399
|
-
}
|
|
4400
|
-
}
|
|
4401
|
-
} catch (e) {
|
|
4402
|
-
log('warn', `subproject-detect failed for ${id} (non-fatal, flat discovery): ${e.message}`);
|
|
4403
|
-
_effectiveSubproject = '';
|
|
4404
|
-
}
|
|
4405
|
-
|
|
4406
|
-
let _refreshedDiscoveryDiagnostics = null;
|
|
4407
4347
|
if (worktreePath && meta?.source === 'work-item' && meta?.item?.branchStrategy === 'shared-branch') {
|
|
4408
4348
|
const refreshed = renderProjectWorkItemPromptForAgent(
|
|
4409
4349
|
meta.item,
|
|
@@ -4413,21 +4353,15 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
4413
4353
|
project,
|
|
4414
4354
|
rootDir,
|
|
4415
4355
|
branchName,
|
|
4416
|
-
{ worktreePath
|
|
4356
|
+
{ worktreePath }
|
|
4417
4357
|
);
|
|
4418
4358
|
if (refreshed.prompt) {
|
|
4419
4359
|
taskPrompt = refreshed.prompt;
|
|
4420
4360
|
fullTaskPrompt = buildFullTaskPrompt(taskPrompt);
|
|
4421
4361
|
safeWrite(promptPath, fullTaskPrompt);
|
|
4422
|
-
log('info', `Refreshed shared-branch prompt for ${id} with worktree ${worktreePath}
|
|
4362
|
+
log('info', `Refreshed shared-branch prompt for ${id} with worktree ${worktreePath}`);
|
|
4423
4363
|
}
|
|
4424
|
-
_refreshedDiscoveryDiagnostics = refreshed.discoveryDiagnostics || null;
|
|
4425
4364
|
}
|
|
4426
|
-
// Record the detected sub-project (+ any per-surface skill-discovery truncations the
|
|
4427
|
-
// shared-branch re-render surfaced) onto the dispatch record for later
|
|
4428
|
-
// grounding/observability. Sub-project-only for non-re-render dispatches (their
|
|
4429
|
-
// prompt was rendered at dispatch-build time; truncations are logged there).
|
|
4430
|
-
_recordDispatchDiscoveryDiagnostics(id, dispatchItem, _effectiveSubproject, _refreshedDiscoveryDiagnostics);
|
|
4431
4365
|
|
|
4432
4366
|
// Inject dirty file list when worktree has uncommitted changes (e.g., max_turns retry)
|
|
4433
4367
|
// This signals to the respawned agent that prior work exists in the worktree (#960)
|
|
@@ -4524,15 +4458,6 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
4524
4458
|
const resolvedModel = runtime.resolveModel(shared.resolveAgentModel(agentConfig, engineConfig));
|
|
4525
4459
|
const resolvedMaxBudget = shared.resolveAgentMaxBudget(agentConfig, engineConfig);
|
|
4526
4460
|
const resolvedBare = shared.resolveAgentBareMode(agentConfig, engineConfig);
|
|
4527
|
-
// P-49e1c8b7 — hermetic harness opt-out (per-agent override allowed).
|
|
4528
|
-
// When true, this dispatch:
|
|
4529
|
-
// - skips Claude workspace .mcp.json pre-approval (preApproveWorkspaceMcps),
|
|
4530
|
-
// - skips project-local-on-main `--project-harness-dir` propagation,
|
|
4531
|
-
// - forwards `--hermetic-harness` to spawn-agent.js so computeAddDirs
|
|
4532
|
-
// returns [minionsDir] only (user-asset dirs stripped from --add-dir).
|
|
4533
|
-
// Independent of copilotDisableBuiltinMcps and copilotSuppressAgentsMd,
|
|
4534
|
-
// which retain their existing semantics.
|
|
4535
|
-
const resolvedHermetic = shared.resolveAgentHermeticHarness(agentConfig, engineConfig);
|
|
4536
4461
|
|
|
4537
4462
|
// W-mpg6isvy000xca4d — On retry after FAILURE_CLASS.MODEL_UNAVAILABLE, swap
|
|
4538
4463
|
// to the runtime-appropriate fallback model. Two paths gated on
|
|
@@ -4585,7 +4510,6 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
4585
4510
|
fallbackModel: engineConfig.claudeFallbackModel,
|
|
4586
4511
|
stream: engineConfig.copilotStreamMode,
|
|
4587
4512
|
disableBuiltinMcps: engineConfig.copilotDisableBuiltinMcps,
|
|
4588
|
-
suppressAgentsMd: engineConfig.copilotSuppressAgentsMd,
|
|
4589
4513
|
reasoningSummaries: engineConfig.copilotReasoningSummaries,
|
|
4590
4514
|
});
|
|
4591
4515
|
|
|
@@ -4600,28 +4524,21 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
4600
4524
|
// and when engine.claudePreApproveWorkspaceMcps is false (no runtime.name
|
|
4601
4525
|
// check at this call site, per CLAUDE.md rule). Best-effort: NEVER block dispatch.
|
|
4602
4526
|
//
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
mutateJsonFileLocked: shared.mutateJsonFileLocked,
|
|
4616
|
-
});
|
|
4617
|
-
if (result.wrote) {
|
|
4618
|
-
log('info', `Pre-approved ${result.servers.length} workspace MCP server(s) in ~/.claude.json for ${worktreePath}: ${result.servers.join(', ')}`);
|
|
4619
|
-
} else if (result.reason !== 'no-workspace-mcp' && result.reason !== 'not-claude' && result.reason !== 'no-worktree') {
|
|
4620
|
-
log('debug', `Skipped workspace MCP pre-approval for ${id} (reason=${result.reason})`);
|
|
4621
|
-
}
|
|
4622
|
-
} catch (err) {
|
|
4623
|
-
log('warn', `Workspace MCP pre-approval failed for ${id} (non-fatal): ${err.message}`);
|
|
4527
|
+
try {
|
|
4528
|
+
const result = preApproveWorkspaceMcps({
|
|
4529
|
+
runtimeName,
|
|
4530
|
+
worktreePath,
|
|
4531
|
+
homeDir: os.homedir(),
|
|
4532
|
+
engineConfig,
|
|
4533
|
+
mutateJsonFileLocked: shared.mutateJsonFileLocked,
|
|
4534
|
+
});
|
|
4535
|
+
if (result.wrote) {
|
|
4536
|
+
log('info', `Pre-approved ${result.servers.length} workspace MCP server(s) in ~/.claude.json for ${worktreePath}: ${result.servers.join(', ')}`);
|
|
4537
|
+
} else if (result.reason !== 'no-workspace-mcp' && result.reason !== 'not-claude' && result.reason !== 'no-worktree') {
|
|
4538
|
+
log('debug', `Skipped workspace MCP pre-approval for ${id} (reason=${result.reason})`);
|
|
4624
4539
|
}
|
|
4540
|
+
} catch (err) {
|
|
4541
|
+
log('warn', `Workspace MCP pre-approval failed for ${id} (non-fatal): ${err.message}`);
|
|
4625
4542
|
}
|
|
4626
4543
|
|
|
4627
4544
|
_phaseT.afterRuntime = Date.now();
|
|
@@ -4696,123 +4613,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
4696
4613
|
// spawn-agent.js handles CLAUDECODE env cleanup and claude binary resolution
|
|
4697
4614
|
const spawnScript = path.join(ENGINE_DIR, 'spawn-agent.js');
|
|
4698
4615
|
|
|
4699
|
-
|
|
4700
|
-
// The worktree-uncommitted footgun (docs/harness-propagation.md): when a
|
|
4701
|
-
// mutating dispatch runs in a fresh `git worktree add` (worktreePath !==
|
|
4702
|
-
// project.localPath), uncommitted `<repo>/.claude/skills/foo/SKILL.md` etc.
|
|
4703
|
-
// on the operator's main checkout are invisible to the runtime CLI because
|
|
4704
|
-
// its native discovery is rooted at cwd. Union the project-scope harness
|
|
4705
|
-
// dirs (skills + commands) that exist under `project.localPath` but NOT
|
|
4706
|
-
// under `worktreePath`, and forward them to spawn-agent as
|
|
4707
|
-
// `--project-harness-dir <dir>` so `computeAddDirs` surfaces them via
|
|
4708
|
-
// `--add-dir`. Gated by engine.harnessPropagateProjectLocal (default true).
|
|
4709
|
-
// No-op for live-checkout mode (worktreePath === null) and read-only types
|
|
4710
|
-
// (also worktreePath === null per resolveSpawnPaths).
|
|
4711
|
-
//
|
|
4712
|
-
// P-49e1c8b7 — Also skipped entirely when resolvedHermetic is true: hermetic
|
|
4713
|
-
// dispatches want a known-empty harness surface so neither user-asset dirs
|
|
4714
|
-
// (filtered inside computeAddDirs) nor project-local-on-main dirs (filtered
|
|
4715
|
-
// here) reach the agent. We skip the queries.getProjectHarnesses(...) call
|
|
4716
|
-
// too — saves the fs scan when the result will be discarded.
|
|
4717
|
-
//
|
|
4718
|
-
// P-714ef144 — when meta.workdir is set, the filter helper clips both the
|
|
4719
|
-
// project-side and worktree-side anchors to <base>/<workdir>, so a
|
|
4720
|
-
// `packages/foo` dispatch never sees harness dirs from sibling packages.
|
|
4721
|
-
// The clipping is purely additive on top of the existing project-vs-worktree
|
|
4722
|
-
// disjoint filter; when workdir is null the helper produces the same result
|
|
4723
|
-
// as the legacy inline loop.
|
|
4724
|
-
const projectHarnessArgs = [];
|
|
4725
|
-
const propagatedProjectHarnessDirs = [];
|
|
4726
|
-
if (!resolvedHermetic
|
|
4727
|
-
&& engineConfig.harnessPropagateProjectLocal !== false
|
|
4728
|
-
&& worktreePath
|
|
4729
|
-
&& project?.localPath
|
|
4730
|
-
&& path.resolve(worktreePath) !== path.resolve(project.localPath)) {
|
|
4731
|
-
try {
|
|
4732
|
-
const harnesses = queries.getProjectHarnesses(project);
|
|
4733
|
-
const candidateDirs = [
|
|
4734
|
-
...(harnesses.skills || []),
|
|
4735
|
-
...(harnesses.commands || []),
|
|
4736
|
-
]
|
|
4737
|
-
.map((entry) => entry?.dir)
|
|
4738
|
-
.filter((d) => typeof d === 'string' && d);
|
|
4739
|
-
const filteredDirs = shared.filterProjectHarnessDirsForWorkdir(candidateDirs, {
|
|
4740
|
-
projectLocalPath: project.localPath,
|
|
4741
|
-
worktreePath,
|
|
4742
|
-
// P-f52d81ba — clip to the effective sub-project. Explicit meta.workdir wins
|
|
4743
|
-
// (validatedWorkdir, possibly multi-segment); otherwise use the
|
|
4744
|
-
// auto-derived dominant sub-project (single first-level segment) so an
|
|
4745
|
-
// ocm/-dominant diff surfaces only ocm/'s --project-harness-dir roots.
|
|
4746
|
-
// Empty ⇒ no clip (today's whole-project harness surface).
|
|
4747
|
-
workdir: validatedWorkdir || _effectiveSubproject || null,
|
|
4748
|
-
});
|
|
4749
|
-
for (const abs of filteredDirs) {
|
|
4750
|
-
if (!fs.existsSync(abs)) continue;
|
|
4751
|
-
projectHarnessArgs.push('--project-harness-dir', abs);
|
|
4752
|
-
propagatedProjectHarnessDirs.push(abs);
|
|
4753
|
-
}
|
|
4754
|
-
if (projectHarnessArgs.length) {
|
|
4755
|
-
log('debug', `Project-local harness propagation: ${projectHarnessArgs.length / 2} dir(s) for ${id} (${worktreePath}${validatedWorkdir ? ', workdir=' + validatedWorkdir : (_effectiveSubproject ? ', subproject=' + _effectiveSubproject : '')})`);
|
|
4756
|
-
}
|
|
4757
|
-
} catch (err) {
|
|
4758
|
-
log('warn', `Project-local harness propagation failed for ${id} (non-fatal): ${err.message}`);
|
|
4759
|
-
}
|
|
4760
|
-
}
|
|
4761
|
-
|
|
4762
|
-
// ── P-a8f3c2d1 — harness grounding manifest (pure capture) ──────────────
|
|
4763
|
-
// Snapshot the skills / MCP servers / commands the engine exposed to this
|
|
4764
|
-
// agent — BOTH user-scope (queries.getUserHarnesses) and project-scope
|
|
4765
|
-
// (queries.getProjectHarnesses) — plus the resolved `--add-dir` set
|
|
4766
|
-
// (engine/spawn-agent.js#computeAddDirs, called with the exact same inputs
|
|
4767
|
-
// spawn-agent uses). Persisted onto the dispatch record as `_harnessPropagated`
|
|
4768
|
-
// so the completion-report grounding cross-check (P-c6f5e8b3) can mark each
|
|
4769
|
-
// self-reported entry grounded:true|false. CRITICAL: include user-scope
|
|
4770
|
-
// harnesses or legitimate ~/.agents / ~/.claude skill usage is wrongly
|
|
4771
|
-
// flagged grounded:false downstream. This is observability only — it does NOT
|
|
4772
|
-
// change which dirs are propagated at spawn time. Best-effort: any failure
|
|
4773
|
-
// here must never block the spawn.
|
|
4774
|
-
try {
|
|
4775
|
-
const homeDir = os.homedir();
|
|
4776
|
-
const userHarnesses = queries.getUserHarnesses(homeDir);
|
|
4777
|
-
const projectHarnesses = project?.localPath
|
|
4778
|
-
? queries.getProjectHarnesses(project)
|
|
4779
|
-
: { skills: [], commands: [], mcps: [] };
|
|
4780
|
-
let addDirs = [];
|
|
4781
|
-
try {
|
|
4782
|
-
addDirs = computeAddDirs({
|
|
4783
|
-
runtime,
|
|
4784
|
-
minionsDir: MINIONS_DIR,
|
|
4785
|
-
homeDir,
|
|
4786
|
-
projectHarnessDirs: propagatedProjectHarnessDirs,
|
|
4787
|
-
hermetic: resolvedHermetic,
|
|
4788
|
-
});
|
|
4789
|
-
} catch { addDirs = []; }
|
|
4790
|
-
const harnessManifest = queries.buildHarnessPropagatedManifest({
|
|
4791
|
-
userHarnesses,
|
|
4792
|
-
projectHarnesses,
|
|
4793
|
-
addDirs,
|
|
4794
|
-
});
|
|
4795
|
-
mutateDispatch((dispatch) => {
|
|
4796
|
-
for (const queue of ['pending', 'active', 'completed']) {
|
|
4797
|
-
const arr = Array.isArray(dispatch?.[queue]) ? dispatch[queue] : null;
|
|
4798
|
-
if (!arr) continue;
|
|
4799
|
-
const found = arr.find(d => d && d.id === id);
|
|
4800
|
-
if (found) found._harnessPropagated = harnessManifest;
|
|
4801
|
-
}
|
|
4802
|
-
return dispatch;
|
|
4803
|
-
});
|
|
4804
|
-
dispatchItem._harnessPropagated = harnessManifest;
|
|
4805
|
-
} catch (err) {
|
|
4806
|
-
log('warn', `Harness grounding manifest capture failed for ${id} (non-fatal): ${err.message}`);
|
|
4807
|
-
}
|
|
4808
|
-
|
|
4809
|
-
// P-49e1c8b7 — hermetic harness opt-out forwarded to spawn-agent.js so
|
|
4810
|
-
// computeAddDirs returns [minionsDir] only (user-asset dirs stripped).
|
|
4811
|
-
// Append BEFORE projectHarnessArgs (which will be empty when hermetic, but
|
|
4812
|
-
// ordering keeps the intent explicit in process listings).
|
|
4813
|
-
const hermeticArgs = resolvedHermetic ? ['--hermetic-harness'] : [];
|
|
4814
|
-
|
|
4815
|
-
const spawnArgs = [spawnScript, promptPath, sysPromptPath, ...args, ...hermeticArgs, ...projectHarnessArgs];
|
|
4616
|
+
const spawnArgs = [spawnScript, promptPath, sysPromptPath, ...args];
|
|
4816
4617
|
|
|
4817
4618
|
// Live output file — stamped BEFORE child process is spawned (#W-mo248lkjwgsu).
|
|
4818
4619
|
// Writing the stub pre-spawn lets the orphan detector distinguish three failure modes
|
|
@@ -5158,7 +4959,6 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
5158
4959
|
fallbackModel: engineConfig?.claudeFallbackModel,
|
|
5159
4960
|
stream: engineConfig?.copilotStreamMode,
|
|
5160
4961
|
disableBuiltinMcps: engineConfig?.copilotDisableBuiltinMcps,
|
|
5161
|
-
suppressAgentsMd: engineConfig?.copilotSuppressAgentsMd,
|
|
5162
4962
|
reasoningSummaries: engineConfig?.copilotReasoningSummaries,
|
|
5163
4963
|
});
|
|
5164
4964
|
if (!resumeArgs.includes('--resume')) {
|
|
@@ -5223,12 +5023,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
5223
5023
|
let resumeProc;
|
|
5224
5024
|
try {
|
|
5225
5025
|
// detached so the resumed steering session also survives engine death (matches initial spawn)
|
|
5226
|
-
|
|
5227
|
-
// can still reach uncommitted project-local harness dirs (the runtime
|
|
5228
|
-
// CLI re-indexes asset dirs on every spawn, including this resume).
|
|
5229
|
-
// P-49e1c8b7 — same for --hermetic-harness so the resumed dispatch
|
|
5230
|
-
// keeps the known-empty harness contract.
|
|
5231
|
-
resumeProc = runFile(process.execPath, [spawnScript, steerPromptPath, sysPromptPath, ...resumeArgs, ...hermeticArgs, ...projectHarnessArgs], {
|
|
5026
|
+
resumeProc = runFile(process.execPath, [spawnScript, steerPromptPath, sysPromptPath, ...resumeArgs], {
|
|
5232
5027
|
cwd,
|
|
5233
5028
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
5234
5029
|
env: childEnv,
|
|
@@ -6708,8 +6503,9 @@ function materializePlansAsWorkItems(config) {
|
|
|
6708
6503
|
}
|
|
6709
6504
|
}
|
|
6710
6505
|
|
|
6711
|
-
|
|
6712
|
-
|
|
6506
|
+
const planFiles = prdStore.listPrdRows()
|
|
6507
|
+
.filter(row => !row.archived)
|
|
6508
|
+
.map(row => row.filename);
|
|
6713
6509
|
|
|
6714
6510
|
// Regex for detecting sequential PRD item IDs (P-001, M001, M-001, WI-001, …) — hoisted outside loop.
|
|
6715
6511
|
// Covers all patterns where an alphabetic prefix is followed by an optional dash and pure digits,
|
|
@@ -6937,11 +6733,11 @@ function materializePlansAsWorkItems(config) {
|
|
|
6937
6733
|
|
|
6938
6734
|
let totalCreated = 0;
|
|
6939
6735
|
for (const [projName, { project, items: projItems }] of itemsByProject) {
|
|
6940
|
-
// Re-read PRD status
|
|
6736
|
+
// Re-read authoritative PRD status to guard against mid-tick status changes (P-f1a3b5c7).
|
|
6941
6737
|
// A pause/reject/awaiting-approval signal may arrive between the outer-loop initial
|
|
6942
6738
|
// read and this point (e.g. operator pauses via dashboard while the tick is running).
|
|
6943
6739
|
// Re-reading here prevents phantom WI creation on a paused, rejected, or unapproved plan.
|
|
6944
|
-
const freshPrd =
|
|
6740
|
+
const freshPrd = prdStore.readPrd(path.join(PRD_DIR, file));
|
|
6945
6741
|
const freshStatus = (freshPrd || {}).status || ((freshPrd || {}).requires_approval ? 'awaiting-approval' : null);
|
|
6946
6742
|
if (freshStatus === PLAN_STATUS.PAUSED || freshStatus === PLAN_STATUS.REJECTED || freshStatus === 'awaiting-approval') {
|
|
6947
6743
|
log('warn', `PRD ${file}: status changed to '${freshStatus}' mid-tick — skipping WI batch for ${projName} (P-f1a3b5c7)`);
|
|
@@ -8364,30 +8160,6 @@ function _buildRunnerBriefVars(item, project) {
|
|
|
8364
8160
|
}
|
|
8365
8161
|
}
|
|
8366
8162
|
|
|
8367
|
-
// P-f52d81ba — sync effective-sub-project derivation for the build-time prompt render
|
|
8368
|
-
// (renderProjectWorkItemPromptForAgent runs synchronously, so no git diff).
|
|
8369
|
-
// Explicit meta.workdir wins (first segment). Otherwise best-effort from the
|
|
8370
|
-
// work item's references[*].path via detectDominantSubproject. Returns '' (flat
|
|
8371
|
-
// discovery) on anything unresolved. spawnAgent's async re-render overrides
|
|
8372
|
-
// this with a git-diff-derived sub-project via options.dominantSubproject.
|
|
8373
|
-
function _deriveDominantSubprojectSync(item, project) {
|
|
8374
|
-
try {
|
|
8375
|
-
const wd = item && item.meta && item.meta.workdir;
|
|
8376
|
-
if (typeof wd === 'string' && wd.trim()) {
|
|
8377
|
-
return wd.replace(/\\/g, '/').replace(/^\/+/, '').split('/')[0] || '';
|
|
8378
|
-
}
|
|
8379
|
-
const projectPath = project && project.localPath;
|
|
8380
|
-
if (!projectPath) return '';
|
|
8381
|
-
const paths = (item && Array.isArray(item.references))
|
|
8382
|
-
? item.references.map(r => (r && typeof r.path === 'string') ? r.path : '').filter(Boolean)
|
|
8383
|
-
: [];
|
|
8384
|
-
if (paths.length === 0) return '';
|
|
8385
|
-
const { detectDominantSubproject } = require('./engine/discover-project-skills')._internal;
|
|
8386
|
-
const det = detectDominantSubproject({ projectPath, changedPaths: paths });
|
|
8387
|
-
return (det && det.confident && det.subproject) ? det.subproject : '';
|
|
8388
|
-
} catch { return ''; }
|
|
8389
|
-
}
|
|
8390
|
-
|
|
8391
8163
|
/**
|
|
8392
8164
|
* Scan work-items.json for manually queued tasks
|
|
8393
8165
|
*/
|
|
@@ -8469,12 +8241,6 @@ function renderProjectWorkItemPromptForAgent(item, workType, agentId, config, pr
|
|
|
8469
8241
|
qa_artifacts_dir: item.meta && item.meta.qaRunId
|
|
8470
8242
|
? path.posix.join('engine', 'qa-artifacts', String(item.meta.qaRunId))
|
|
8471
8243
|
: '',
|
|
8472
|
-
// W-mq16xtdx001a347e — escape hatch for review dispatches that should NOT
|
|
8473
|
-
// be steered toward project-local review skills (e.g. when the diff
|
|
8474
|
-
// under review IS that skill, so a meta-review needs first-principles).
|
|
8475
|
-
// Default OFF — the new "Project review skills" block is the whole point
|
|
8476
|
-
// of W-mq16xtdx and should surface on every review by default.
|
|
8477
|
-
skip_project_review_skills: !!(item.meta && item.meta.skipProjectReviewSkills),
|
|
8478
8244
|
// P-e6b3c2d8 — QA Session template vars. The qa-sessions chain helpers
|
|
8479
8245
|
// (engine/qa-sessions.js#_baseWorkItem) stamp meta.sessionId,
|
|
8480
8246
|
// meta.sessionPhase, and meta.qaSession.{target,flowsRaw,mode,capture,runner}
|
|
@@ -8550,26 +8316,6 @@ function renderProjectWorkItemPromptForAgent(item, workType, agentId, config, pr
|
|
|
8550
8316
|
// failure via the qa-session-draft-failed / qa-session-execute-failed
|
|
8551
8317
|
// path. (See playbooks/qa-session-draft.md → "Failure path" section.)
|
|
8552
8318
|
..._buildRunnerBriefVars(item, project),
|
|
8553
|
-
// W-mq16xtdx001a347e + W-mq1cczi90006b21f — escape hatches for dispatches
|
|
8554
|
-
// that should NOT be steered toward project-local skills (e.g. when the
|
|
8555
|
-
// diff under review IS that skill, so a meta-review needs first-principles).
|
|
8556
|
-
// Default OFF — the project skills block is the whole point of these WIs
|
|
8557
|
-
// and should surface on every applicable dispatch by default.
|
|
8558
|
-
//
|
|
8559
|
-
// Both meta names are honored:
|
|
8560
|
-
// - meta.skipProjectReviewSkills (PR-82 alias, review-only suppression)
|
|
8561
|
-
// - meta.skipProjectSkills (W-mq1cczi90006b21f, generic suppression)
|
|
8562
|
-
// Either flag suppresses both blocks on the dispatch.
|
|
8563
|
-
skip_project_review_skills: !!(item.meta && item.meta.skipProjectReviewSkills),
|
|
8564
|
-
skip_project_skills: !!(item.meta && (item.meta.skipProjectSkills || item.meta.skipProjectReviewSkills)),
|
|
8565
|
-
// P-f52d81ba — effective monorepo sub-project for scoped skill discovery. When the
|
|
8566
|
-
// async caller (spawnAgent) supplies a git-diff-derived sub-project it wins (even
|
|
8567
|
-
// ''); otherwise derive best-effort from explicit meta.workdir /
|
|
8568
|
-
// references[*].path. Empty ⇒ flat discovery. playbook.js reads this as the
|
|
8569
|
-
// discovery scopeSubproject.
|
|
8570
|
-
dominant_subproject: (typeof options.dominantSubproject === 'string')
|
|
8571
|
-
? options.dominantSubproject
|
|
8572
|
-
: _deriveDominantSubprojectSync(item, project),
|
|
8573
8319
|
// M006 — typed handoff envelope. Set on follow-up dispatch metas by
|
|
8574
8320
|
// lifecycle.js when queuing an item after a completing agent. The
|
|
8575
8321
|
// renderPlaybook path injects a "Handoff Context" section when truthy
|
|
@@ -8594,11 +8340,6 @@ function renderProjectWorkItemPromptForAgent(item, workType, agentId, config, pr
|
|
|
8594
8340
|
needsReview: false,
|
|
8595
8341
|
checkpointCount: cpResult.checkpointCount,
|
|
8596
8342
|
prompt,
|
|
8597
|
-
// P-f52d81ba — surface the discovery diagnostics renderPlaybook stashed on
|
|
8598
|
-
// the vars object (detected sub-project + per-surface truncations) so spawnAgent
|
|
8599
|
-
// can record them onto the dispatch record. null when no playbook render
|
|
8600
|
-
// ran (item.prompt short-circuit) or discovery was skipped.
|
|
8601
|
-
discoveryDiagnostics: vars._discoveryDiagnostics || null,
|
|
8602
8343
|
};
|
|
8603
8344
|
}
|
|
8604
8345
|
|
|
@@ -8690,19 +8431,34 @@ function refreshDeferredReviewPrompt(item, config) {
|
|
|
8690
8431
|
if (!pr) return;
|
|
8691
8432
|
const project = projectFromDispatchMeta(item.meta?.project, config);
|
|
8692
8433
|
if (!project) return;
|
|
8693
|
-
|
|
8694
|
-
|
|
8434
|
+
// W-mrewhflh000k1acd: meta.reviewHeadSha (and meta.pr) were snapshotted at
|
|
8435
|
+
// DEFER time — when no non-author reviewer was available. If the PR head
|
|
8436
|
+
// advances while the review sits pending reassignment, reusing that stale
|
|
8437
|
+
// snapshot means lifecycle.js#updatePrAfterReview stamps reviewedHeadSha
|
|
8438
|
+
// against the OLD head once this review actually completes, so
|
|
8439
|
+
// isPrReviewCurrent() wrongly judges freshness against the true current
|
|
8440
|
+
// head — producing spurious re-review dispatch loops (or false
|
|
8441
|
+
// "already reviewed" skips). Re-fetch the live PR record and recompute the
|
|
8442
|
+
// head SHA before rebuilding the prompt.
|
|
8443
|
+
let livePr = pr;
|
|
8444
|
+
try {
|
|
8445
|
+
const prs = safeJsonArr(projectPrPath(project));
|
|
8446
|
+
const found = shared.findPrRecord(prs, pr, project);
|
|
8447
|
+
if (found) livePr = found;
|
|
8448
|
+
} catch (e) { log('warn', `refreshDeferredReviewPrompt: live PR lookup for ${pr.id} failed: ${e.message}`); }
|
|
8449
|
+
const prBranch = item.meta.branch || livePr.branch || '';
|
|
8450
|
+
const prNumber = shared.getPrNumber(livePr);
|
|
8695
8451
|
const extraVars = {
|
|
8696
|
-
pr_id:
|
|
8452
|
+
pr_id: livePr.id,
|
|
8697
8453
|
pr_number: prNumber,
|
|
8698
|
-
pr_title:
|
|
8454
|
+
pr_title: livePr.title || '',
|
|
8699
8455
|
pr_branch: prBranch,
|
|
8700
|
-
pr_author:
|
|
8701
|
-
pr_url:
|
|
8456
|
+
pr_author: livePr.agent || '',
|
|
8457
|
+
pr_url: livePr.url || '',
|
|
8702
8458
|
};
|
|
8703
8459
|
const rebuilt = buildPrDispatch(
|
|
8704
|
-
item.agent, config, project,
|
|
8705
|
-
extraVars, `Review ${
|
|
8460
|
+
item.agent, config, project, livePr, WORK_TYPE.REVIEW,
|
|
8461
|
+
extraVars, `Review ${livePr.id}: ${livePr.title || ''}`, item.meta
|
|
8706
8462
|
);
|
|
8707
8463
|
if (rebuilt && rebuilt.prompt) {
|
|
8708
8464
|
item.prompt = rebuilt.prompt;
|
|
@@ -8710,6 +8466,8 @@ function refreshDeferredReviewPrompt(item, config) {
|
|
|
8710
8466
|
if (rebuilt.agentName) item.agentName = rebuilt.agentName;
|
|
8711
8467
|
if (rebuilt.agentRole) item.agentRole = rebuilt.agentRole;
|
|
8712
8468
|
}
|
|
8469
|
+
item.meta.pr = livePr;
|
|
8470
|
+
item.meta.reviewHeadSha = getPrCauseHead(livePr);
|
|
8713
8471
|
item.meta.deferReviewerResolution = false;
|
|
8714
8472
|
}
|
|
8715
8473
|
|
|
@@ -9837,12 +9595,13 @@ function discoverCentralWorkItems(config) {
|
|
|
9837
9595
|
// PRD's source_plan can't fork a duplicate PRD (#415 class).
|
|
9838
9596
|
let prdFilename = null;
|
|
9839
9597
|
const planKey = path.basename(String(item.planFile || ''));
|
|
9840
|
-
const
|
|
9598
|
+
const prdRows = prdStore.listPrdRows();
|
|
9599
|
+
const prdFiles = prdRows.filter(row => !row.archived).map(row => row.filename);
|
|
9841
9600
|
for (const pf of prdFiles) {
|
|
9842
|
-
const prd =
|
|
9601
|
+
const prd = prdStore.readPrd(path.join(PRD_DIR, pf));
|
|
9843
9602
|
if (planKey && prd && !shared.isPrdArchived(prd) && shared.prdMatchesSourcePlan(prd.source_plan, planKey)) {
|
|
9844
9603
|
prdFilename = pf;
|
|
9845
|
-
|
|
9604
|
+
vars.existing_prd_json = JSON.stringify(prd, null, 2);
|
|
9846
9605
|
log('info', `plan-to-prd: reusing existing PRD "${pf}" for plan "${item.planFile}" (#884)`);
|
|
9847
9606
|
break;
|
|
9848
9607
|
}
|
|
@@ -9882,7 +9641,7 @@ function discoverCentralWorkItems(config) {
|
|
|
9882
9641
|
shared.sanitizePath(prdFilename, PRD_DIR);
|
|
9883
9642
|
const prdExisting = new Set([
|
|
9884
9643
|
...prdFiles,
|
|
9885
|
-
...
|
|
9644
|
+
...prdRows.filter(row => row.archived).map(row => row.filename),
|
|
9886
9645
|
]);
|
|
9887
9646
|
for (const otherItem of items) {
|
|
9888
9647
|
if (!otherItem || otherItem.id === item.id) continue;
|
|
@@ -10245,7 +10004,7 @@ async function discoverWork(config) {
|
|
|
10245
10004
|
// basename. The plan-completion scan does the same purge safely (identity-
|
|
10246
10005
|
// checked + neutralize); here the isDefunctPrd guard below already skips
|
|
10247
10006
|
// archived/defunct PRDs, so this loop never re-runs completion on a ghost.)
|
|
10248
|
-
for (const f of
|
|
10007
|
+
for (const f of prdStore.listPrdRows().filter(row => !row.archived).map(row => row.filename)) {
|
|
10249
10008
|
if (completedPlanCache.has(f)) continue;
|
|
10250
10009
|
// safeJsonNoRestore — defense in depth: if the file vanished between
|
|
10251
10010
|
// readdir and read (e.g. concurrent archive), do not resurrect it
|
|
@@ -10819,7 +10578,7 @@ async function tickInner() {
|
|
|
10819
10578
|
try { persistVerifyPrsToPrd(config); } catch (err) { log('warn', `PRD verify-PR persist error: ${err?.message || err}`); }
|
|
10820
10579
|
// Check if any plans can be marked completed (all features done/in-pr)
|
|
10821
10580
|
try {
|
|
10822
|
-
const prdFiles =
|
|
10581
|
+
const prdFiles = prdStore.listPrdRows().filter(row => !row.archived).map(row => row.filename);
|
|
10823
10582
|
for (const file of prdFiles) {
|
|
10824
10583
|
if (completedPlanCache.has(file)) continue;
|
|
10825
10584
|
if (fs.existsSync(path.join(PRD_DIR, 'archive', file))) {
|
|
@@ -10978,6 +10737,14 @@ async function tickInner() {
|
|
|
10978
10737
|
// Only retry if something depends on this item
|
|
10979
10738
|
const isBlocking = items.some(w => w.status === WI_STATUS.PENDING && (w.depends_on || []).includes(item.id));
|
|
10980
10739
|
if (!isBlocking) continue;
|
|
10740
|
+
// W-mrdykpv60005d50d: don't resurrect items whose failure class is explicitly
|
|
10741
|
+
// non-retryable (AUTH, INJECTION_FLAGGED, PRE_DISPATCH_EVAL_STUCK, etc.) — mirrors
|
|
10742
|
+
// the same isRetryableFailureReason() gate used by the dispatch-completion retry path
|
|
10743
|
+
// (engine/dispatch.js:764) and the quarantine auto-recovery block above (engine.js:8804-8811).
|
|
10744
|
+
if (!isRetryableFailureReason(item.failReason, item._failureClass)) {
|
|
10745
|
+
log('info', `Stall recovery: skipping ${item.id} — failure class '${item._failureClass || 'unknown'}' is non-retryable`);
|
|
10746
|
+
continue;
|
|
10747
|
+
}
|
|
10981
10748
|
|
|
10982
10749
|
log('info', `Stall recovery: auto-retrying ${item.id} (blocking ${pendingWithBlockedDeps.filter(w => (w.depends_on || []).includes(item.id)).length} items)`);
|
|
10983
10750
|
item.status = WI_STATUS.PENDING;
|
|
@@ -11753,6 +11520,7 @@ module.exports = {
|
|
|
11753
11520
|
normalizePrBranch, resolvePrBranch, prCausePart, getPrCauseHead, getPrCauseBase, getPrAutomationCauseKey, getPrAutomationDispatchKey, // exported for testing
|
|
11754
11521
|
isPrReviewCurrent, isPrFixedAfterReview, // exported for testing (W-mrej8p740006b0bf — SHA-based review freshness)
|
|
11755
11522
|
ensurePrBranchForDispatch, isWithinLinkGraceWindow, PR_LINK_GRACE_WINDOW_MS, // exported for testing (W-mphm0kt0000cebc3)
|
|
11523
|
+
refreshDeferredReviewPrompt, // exported for testing (W-mrewhflh000k1acd — stale reviewHeadSha on reassignment)
|
|
11756
11524
|
|
|
11757
11525
|
// Playbooks
|
|
11758
11526
|
renderPlaybook, validatePlaybookVars, PLAYBOOK_REQUIRED_VARS, buildWorkItemDispatchVars,
|