@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/dashboard.js
CHANGED
|
@@ -1842,11 +1842,16 @@ function _archivePrdPostProcess({ planFile, archivePath, planPath, plansDir, _mu
|
|
|
1842
1842
|
let plan = {};
|
|
1843
1843
|
let archivedSource = null;
|
|
1844
1844
|
|
|
1845
|
-
// (a) Flip status + archivedAt.
|
|
1845
|
+
// (a) Flip the archived FLAG + status + archivedAt. Phase 10 step 4.2b:
|
|
1846
|
+
// archived-ness is the flag now (not the directory), so set `archived:true`
|
|
1847
|
+
// explicitly. The mutate dual-writes the change into SQL via the chokepoint.
|
|
1848
|
+
// When archivePath === planPath (in-place archive) this flags the PRD where it
|
|
1849
|
+
// sits; when they differ (.md-cascade / legacy move) it flags the moved copy.
|
|
1846
1850
|
try {
|
|
1847
1851
|
plan = mutate(archivePath, (data) => {
|
|
1848
1852
|
if (!data || Array.isArray(data) || typeof data !== 'object') data = {};
|
|
1849
1853
|
data.status = 'archived';
|
|
1854
|
+
data.archived = true;
|
|
1850
1855
|
data.archivedAt = new Date().toISOString();
|
|
1851
1856
|
return data;
|
|
1852
1857
|
}, { defaultValue: {} }) || {};
|
|
@@ -1866,30 +1871,38 @@ function _archivePrdPostProcess({ planFile, archivePath, planPath, plansDir, _mu
|
|
|
1866
1871
|
} catch { /* readObj fallback is best-effort */ }
|
|
1867
1872
|
}
|
|
1868
1873
|
|
|
1869
|
-
// (b) Neutralize the .backup sidecar
|
|
1870
|
-
//
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1874
|
+
// (b) Neutralize the .backup sidecar at the OLD live path ONLY when the file
|
|
1875
|
+
// actually moved (archivePath !== planPath) — a stale prd/<f>.json.backup left
|
|
1876
|
+
// behind by a move is resurrection fuel. For an in-place archive the file (and
|
|
1877
|
+
// its current .backup) stay put and are valid, so there is nothing to neutralize.
|
|
1878
|
+
if (planPath !== archivePath) {
|
|
1879
|
+
try {
|
|
1880
|
+
const backupCleanup = shared.neutralizeJsonBackupSidecar(planPath);
|
|
1881
|
+
if (!backupCleanup.ok) {
|
|
1882
|
+
const warning = `Archive backup cleanup failed for ${planFile}: unlink failed (${backupCleanup.unlinkError}); fallback neutralize failed (${backupCleanup.writeError})`;
|
|
1883
|
+
archiveWarnings.push(warning);
|
|
1884
|
+
console.warn(warning);
|
|
1885
|
+
}
|
|
1886
|
+
} catch (e) {
|
|
1887
|
+
const warning = `Archive backup cleanup failed for ${planFile}: ${e.message}`;
|
|
1875
1888
|
archiveWarnings.push(warning);
|
|
1876
1889
|
console.warn(warning);
|
|
1877
1890
|
}
|
|
1878
|
-
} catch (e) {
|
|
1879
|
-
const warning = `Archive backup cleanup failed for ${planFile}: ${e.message}`;
|
|
1880
|
-
archiveWarnings.push(warning);
|
|
1881
|
-
console.warn(warning);
|
|
1882
1891
|
}
|
|
1883
1892
|
|
|
1884
1893
|
// (c) Move the source plan markdown into plans/archive/.
|
|
1885
1894
|
if (plan.source_plan) {
|
|
1886
1895
|
try {
|
|
1887
|
-
|
|
1896
|
+
// Canonical key: a `plans/`-prefixed source_plan would otherwise join to
|
|
1897
|
+
// plansDir/plans/x.md and silently miss, leaving the plan un-archived
|
|
1898
|
+
// while the PRD archived — the plan↔PRD desync. (W-? archive sync.)
|
|
1899
|
+
const planKey = shared.sourcePlanKey(plan.source_plan);
|
|
1900
|
+
const mdPath = path.join(plansDir, planKey);
|
|
1888
1901
|
if (fs.existsSync(mdPath)) {
|
|
1889
1902
|
// DATA-LOSS GUARD: moveFileNoClobber dedupes + retries so archiving a
|
|
1890
1903
|
// source plan can't overwrite a previously-archived plan of the same
|
|
1891
1904
|
// basename (and survives transient Windows file locks).
|
|
1892
|
-
const mdDest = shared.moveFileNoClobber(mdPath, path.join(plansDir, 'archive'),
|
|
1905
|
+
const mdDest = shared.moveFileNoClobber(mdPath, path.join(plansDir, 'archive'), planKey);
|
|
1893
1906
|
archivedSource = path.basename(mdDest);
|
|
1894
1907
|
}
|
|
1895
1908
|
} catch (e) {
|
|
@@ -8393,13 +8406,20 @@ const server = http.createServer(async (req, res) => {
|
|
|
8393
8406
|
const planPath = resolvePlanPath(body.file);
|
|
8394
8407
|
if (!fs.existsSync(planPath)) return jsonReply(res, 404, { error: 'plan not found' });
|
|
8395
8408
|
|
|
8396
|
-
|
|
8397
|
-
//
|
|
8398
|
-
// basename
|
|
8399
|
-
//
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8409
|
+
// Phase 10 step 4.2b: a PRD is archived IN PLACE (flag flip via
|
|
8410
|
+
// _archivePrdPostProcess — the json stays in prd/, no move, so the
|
|
8411
|
+
// live↔archive basename-collision class / footgun #7 is gone). Only the
|
|
8412
|
+
// .md plan still physically moves to plans/archive/.
|
|
8413
|
+
let archivePath;
|
|
8414
|
+
if (isPrd) {
|
|
8415
|
+
archivePath = planPath; // in-place — do not move the PRD json
|
|
8416
|
+
} else {
|
|
8417
|
+
const archiveDir = path.join(PLANS_DIR, 'archive');
|
|
8418
|
+
// DATA-LOSS GUARD: moveFileNoClobber dedupes the destination so a same-
|
|
8419
|
+
// basename collision can't silently destroy a previously-archived plan +
|
|
8420
|
+
// its completed work-item history. Bumps to -2/-3 instead of overwriting.
|
|
8421
|
+
archivePath = shared.moveFileNoClobber(planPath, archiveDir, body.file);
|
|
8422
|
+
}
|
|
8403
8423
|
|
|
8404
8424
|
let archivedSource = null;
|
|
8405
8425
|
let plan = {};
|
|
@@ -8451,33 +8471,19 @@ const server = http.createServer(async (req, res) => {
|
|
|
8451
8471
|
console.warn(warning);
|
|
8452
8472
|
continue;
|
|
8453
8473
|
}
|
|
8454
|
-
|
|
8455
|
-
|
|
8456
|
-
|
|
8457
|
-
|
|
8458
|
-
|
|
8459
|
-
|
|
8460
|
-
|
|
8461
|
-
|
|
8462
|
-
|
|
8463
|
-
|
|
8464
|
-
}
|
|
8465
|
-
let prdArchivePath;
|
|
8466
|
-
try {
|
|
8467
|
-
// DATA-LOSS GUARD: dedupe + retry so the cascade can't clobber a
|
|
8468
|
-
// previously-archived PRD of the same basename.
|
|
8469
|
-
prdArchivePath = shared.moveFileNoClobber(prdLivePath, prdArchiveDir, prdFile);
|
|
8470
|
-
} catch (e) {
|
|
8471
|
-
const warning = `Archive could not move PRD ${prdFile}: ${e.message}`;
|
|
8472
|
-
archiveWarnings.push(warning);
|
|
8473
|
-
console.warn(warning);
|
|
8474
|
-
continue;
|
|
8475
|
-
}
|
|
8476
|
-
// Delegate status-flip + sidecar cleanup + (no-op) source-plan move
|
|
8477
|
-
// to the shared helper so both branches stay in lockstep.
|
|
8474
|
+
// Canonical match (prefix/dir-tolerant): a raw `prd.source_plan !==
|
|
8475
|
+
// body.file` skipped PRDs whose source_plan carried the legacy
|
|
8476
|
+
// `plans/` prefix, archiving the plan but NOT its PRD — the plan↔PRD
|
|
8477
|
+
// status desync behind the "old PRDs came back" resurrection.
|
|
8478
|
+
if (!prd || !shared.prdMatchesSourcePlan(prd.source_plan, body.file)) continue;
|
|
8479
|
+
|
|
8480
|
+
// Phase 10 step 4.2b: flag the PRD archived IN PLACE (no move). The
|
|
8481
|
+
// outer handler already moved the .md to plans/archive/, so the
|
|
8482
|
+
// helper's (c) source-plan move is a no-op; (a) flips the flag (dual-
|
|
8483
|
+
// writing SQL) and (b) skips backup-neutralize since the file stayed.
|
|
8478
8484
|
const cascadeResult = _archivePrdPostProcess({
|
|
8479
8485
|
planFile: prdFile,
|
|
8480
|
-
archivePath:
|
|
8486
|
+
archivePath: prdLivePath,
|
|
8481
8487
|
planPath: prdLivePath,
|
|
8482
8488
|
plansDir: PLANS_DIR,
|
|
8483
8489
|
});
|
|
@@ -8530,11 +8536,36 @@ const server = http.createServer(async (req, res) => {
|
|
|
8530
8536
|
const isJson = body.file.endsWith('.json');
|
|
8531
8537
|
const targetDir = isJson ? PRD_DIR : PLANS_DIR;
|
|
8532
8538
|
const archivePath = path.join(targetDir, 'archive', body.file);
|
|
8533
|
-
|
|
8534
|
-
//
|
|
8535
|
-
//
|
|
8536
|
-
//
|
|
8537
|
-
|
|
8539
|
+
const livePath = path.join(targetDir, body.file);
|
|
8540
|
+
// Phase 10 step 4.2b: a PRD can be archived two ways now — legacy
|
|
8541
|
+
// (physically in <dir>/archive/) or in-place (in <dir>/ with the archived
|
|
8542
|
+
// flag set). Restore from whichever it is.
|
|
8543
|
+
let liveDest;
|
|
8544
|
+
if (fs.existsSync(archivePath)) {
|
|
8545
|
+
// DATA-LOSS GUARD: restoring must not clobber a LIVE plan/PRD of the same
|
|
8546
|
+
// basename (renameSync overwrites). moveFileNoClobber bumps the restored
|
|
8547
|
+
// name instead so the existing live file survives.
|
|
8548
|
+
liveDest = shared.moveFileNoClobber(archivePath, targetDir, body.file);
|
|
8549
|
+
} else if (isJson && fs.existsSync(livePath) && shared.isPrdArchived(safeJsonNoRestore(livePath))) {
|
|
8550
|
+
liveDest = livePath; // in-place flag-archived — already in prd/, just clear the flag below
|
|
8551
|
+
} else {
|
|
8552
|
+
return jsonReply(res, 404, { error: 'File not found in archive' });
|
|
8553
|
+
}
|
|
8554
|
+
// Clear the archived flag/status so the unarchived PRD renders LIVE again.
|
|
8555
|
+
// Archived-ness is the FLAG now (4.2a), so moving the file back is no longer
|
|
8556
|
+
// enough — a legacy PRD carries status='archived' and would still render
|
|
8557
|
+
// archived without this. Dual-writes the cleared state into SQL.
|
|
8558
|
+
if (isJson) {
|
|
8559
|
+
try {
|
|
8560
|
+
mutateJsonFileLocked(liveDest, (d) => {
|
|
8561
|
+
if (!d || typeof d !== 'object' || Array.isArray(d)) return d;
|
|
8562
|
+
if (d.status === 'archived') d.status = 'active';
|
|
8563
|
+
delete d.archived;
|
|
8564
|
+
delete d.archivedAt;
|
|
8565
|
+
return d;
|
|
8566
|
+
}, { skipWriteIfUnchanged: true });
|
|
8567
|
+
} catch (e) { console.warn(`Unarchive flag-clear failed for ${body.file}: ${e.message}`); }
|
|
8568
|
+
}
|
|
8538
8569
|
|
|
8539
8570
|
// Also unarchive linked source plan
|
|
8540
8571
|
let unarchivedSource = null;
|
package/docs/deprecated.json
CHANGED
|
@@ -3,10 +3,26 @@
|
|
|
3
3
|
"id": "agent-config-skills-field",
|
|
4
4
|
"description": "Legacy per-agent descriptive-metadata array `agents.<id>.skills` in config.json, renamed to `agents.<id>.expertise` to remove the name collision with executable runtime/harness skills (SKILL.md). The field is metadata only (capability tags like `architecture`, `bug-fixes`); nothing in the dispatch path reads it for behavior. A read-compat shim honors the old key so operator configs still carrying `skills` (and no `expertise`) keep working.",
|
|
5
5
|
"code": [
|
|
6
|
-
{
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
{
|
|
7
|
+
"file": "engine/playbook.js",
|
|
8
|
+
"lines": "950",
|
|
9
|
+
"note": "buildSystemPrompt reads `agent.expertise ?? agent.skills ?? []` for the `Expertise:` identity line"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"file": "engine/lifecycle.js",
|
|
13
|
+
"lines": "4620-4621",
|
|
14
|
+
"note": "pickReReviewAgentHints reads `agent.expertise` with an `agent.skills` array fallback"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"file": "engine/queries.js",
|
|
18
|
+
"lines": "731",
|
|
19
|
+
"note": "getAgents normalizes `expertise: a.expertise ?? a.skills ?? []` so the dashboard/settings UI always receives `expertise`"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"file": "dashboard.js",
|
|
23
|
+
"lines": "10708-10716",
|
|
24
|
+
"note": "settings POST accepts a legacy `updates.skills` key, persists as `config.agents[id].expertise`, and deletes the old `skills` key"
|
|
25
|
+
}
|
|
10
26
|
],
|
|
11
27
|
"removalGate": "Telemetry / a config sweep across all known engines must show no persisted `config.agents.<id>.skills` key (only `expertise`) for >=30 consecutive days, confirming every operator config has been re-saved through the dashboard (which drops the legacy key) or hand-migrated.",
|
|
12
28
|
"targetRemovalDate": "2026-09-17",
|
|
@@ -31,7 +47,10 @@
|
|
|
31
47
|
{
|
|
32
48
|
"id": "legacy-done-aliases",
|
|
33
49
|
"location": "engine/cleanup.js:1165-1166",
|
|
34
|
-
"constants": [
|
|
50
|
+
"constants": [
|
|
51
|
+
"LEGACY_DONE_ALIASES",
|
|
52
|
+
"LEGACY_NEEDS_REVIEW_STATUS"
|
|
53
|
+
],
|
|
35
54
|
"reason": "Read-side tolerance: cleanup sweep auto-migrates four obsolete work-item / PRD status strings ('in-pr', 'implemented', 'complete', 'needs-human-review') to the canonical 'done' / 'failed' values. The aliases are no longer written anywhere in the engine; the constants exist only to repair stale on-disk values from old engine versions.",
|
|
36
55
|
"targetRemovalDate": null,
|
|
37
56
|
"notes": "Keep indefinitely until telemetry / a sweep log shows zero migrations performed for 30 consecutive days across all known projects (work-items.json + prd/*.json). At that point the constants and both _migrateLegacyItem branches in engine/cleanup.js (definitions at :1165-1166; usage at :1168-1183 for work items and :1269-1272 for PRD missing_features) can be deleted. Total cost on disk today: 4 strings."
|
|
@@ -40,9 +59,21 @@
|
|
|
40
59
|
"id": "config-claude-binary-override",
|
|
41
60
|
"description": "Legacy `config.claude.binary` runtime-resolution override. Older `minions init` versions persisted a `config.claude.binary` field that pointed the Claude runtime at a specific binary path. The runtime adapter still honors this override on every Claude spawn; the engine emits a `deprecated-config-claude` warning at config-load time but does NOT delete the override, so the override branch in claude.js is load-bearing for any install that still carries a non-default value.",
|
|
42
61
|
"code": [
|
|
43
|
-
{
|
|
44
|
-
|
|
45
|
-
|
|
62
|
+
{
|
|
63
|
+
"file": "engine/runtimes/claude.js",
|
|
64
|
+
"lines": "82-86",
|
|
65
|
+
"note": "resolveBinary() respects config.claude.binary on every Claude spawn (probes npm package dir or direct binary path)"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"file": "engine/shared.js",
|
|
69
|
+
"lines": "2482-2496",
|
|
70
|
+
"note": "warnings.push({ id: 'deprecated-config-claude' }) — surface-only; never deletes the override"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"file": "engine/shared.js",
|
|
74
|
+
"lines": "3120-3124",
|
|
75
|
+
"note": "DEFAULT_CLAUDE.binary baseline that the warning + prune logic compares against"
|
|
76
|
+
}
|
|
46
77
|
],
|
|
47
78
|
"removalGate": "Telemetry: the `deprecated-config-claude` warning emitted at engine/shared.js:2492-2495 must report zero hits across all known engines for >=30 consecutive days, AND a sweep of every persisted config.json must show no `config.claude.binary` value that diverges from DEFAULT_CLAUDE.binary. Only then is the override branch in resolveBinary() (engine/runtimes/claude.js:82-86) removable, along with the `_deprecatedConfigClaudeFields` membership for `binary` and the warning emitter at engine/shared.js:2482-2496.",
|
|
48
79
|
"targetRemovalDate": null,
|
|
@@ -52,13 +83,41 @@
|
|
|
52
83
|
"id": "legacy-cc-model-migration",
|
|
53
84
|
"description": "applyLegacyCcModelMigration: in-memory shim that promotes legacy `engine.ccModel` to `engine.defaultModel` when defaultModel is unset, so single-model installs keep working after the runtime fleet refactor (P-3b8e5f1d). No on-disk rewrite — the persisted config.json continues to carry the legacy `ccModel` field. Called unconditionally on every engine boot from cli.start().",
|
|
54
85
|
"code": [
|
|
55
|
-
{
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
{
|
|
61
|
-
|
|
86
|
+
{
|
|
87
|
+
"file": "engine/shared.js",
|
|
88
|
+
"lines": "2407",
|
|
89
|
+
"note": "applyLegacyCcModelMigration definition (function signature + once-per-process flag via _resetLegacyCcModelMigrationFlag)"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"file": "engine/cli.js",
|
|
93
|
+
"lines": "477",
|
|
94
|
+
"note": "Boot call site inside start(); wrapped in try/catch so a migration failure cannot block startup"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"file": "CLAUDE.md",
|
|
98
|
+
"lines": "316",
|
|
99
|
+
"note": "Architectural documentation calling out the in-memory promotion contract"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"file": "docs/slim-ux/concepts.md",
|
|
103
|
+
"lines": "671",
|
|
104
|
+
"note": "Surface-level concepts doc cross-reference"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"file": "test/unit.test.js",
|
|
108
|
+
"lines": "19801",
|
|
109
|
+
"note": "Source-inspection test pinning the CLAUDE.md description against the function name"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"file": "test/unit/runtime-fleet-helpers.test.js",
|
|
113
|
+
"lines": "209-254",
|
|
114
|
+
"note": "Behavioural unit tests (promotion, no-op when defaultModel set, no-op when ccModel unset, empty-string handling, once-only logging, null-safety)"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"file": "test/unit/runtime-fleet-helpers.test.js",
|
|
118
|
+
"lines": "500-505",
|
|
119
|
+
"note": "Source-inspection test pinning the cli.js boot call site"
|
|
120
|
+
}
|
|
62
121
|
],
|
|
63
122
|
"removalGate": "Telemetry: the once-per-boot deprecation log line emitted by applyLegacyCcModelMigration (via the injected logger at engine/shared.js:2407) must show zero promotion events across all known engines for >=30 consecutive days, AND a sweep of every persisted config.json must confirm no `engine.ccModel` field remains. Once both conditions hold, removal deletes the function + _resetLegacyCcModelMigrationFlag export at engine/shared.js:4977, the boot call at engine/cli.js:477, the CLAUDE.md:316 paragraph and docs/slim-ux/concepts.md:671 reference, and the tests at runtime-fleet-helpers.test.js:209-254 + :500-505 + unit.test.js:19801.",
|
|
64
123
|
"targetRemovalDate": null,
|
|
@@ -68,14 +127,39 @@
|
|
|
68
127
|
"id": "sql-state-json-mirrors",
|
|
69
128
|
"description": "Phase X.5 follow-up to the SQL state migration (commits 62bd6a2c..1111cf54, phases 0–7). Every engine state file that previously used mutateJsonFileLocked now routes through a SQL store, but each store still writes a JSON dual-write mirror after every mutation because a handful of direct-readers (a few unit tests + a couple of inline safeJson calls) have not been migrated to the SQL read path. Once those readers are confirmed routed through the SQL store (or rewritten to use the store's read helper), the mirror writers can be deleted and the JSON files retired.",
|
|
70
129
|
"code": [
|
|
71
|
-
{
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
{
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
130
|
+
{
|
|
131
|
+
"file": "engine/dispatch-store.js",
|
|
132
|
+
"note": "_mirrorJsonFromSql + _readDispatchJsonFallback — used when SQL is empty AND JSON has content (test seeding + first-time hydrate)"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"file": "engine/work-items-store.js",
|
|
136
|
+
"note": "_mirrorJsonFromSql + _readJsonArrayFallback (per scope) — same fallback contract as dispatch-store"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"file": "engine/pull-requests-store.js",
|
|
140
|
+
"note": "_mirrorJsonFromSql + _readJsonArrayFallback (per scope)"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"file": "engine/logs-store.js",
|
|
144
|
+
"note": "engine/log.json mirror written by shared._flushLogBuffer's byJsonPath loop — Phase 4.5 will retire"
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"file": "engine/metrics-store.js",
|
|
148
|
+
"note": "_mirrorJsonFromSql + _readJsonObjectFallback"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"file": "engine/watches-store.js",
|
|
152
|
+
"note": "_mirrorJsonFromSql + _readJsonArrayFallback"
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"file": "engine/small-state-store.js",
|
|
156
|
+
"note": "_mirrorScheduleRunsJson, _mirrorPipelineRunsJson, _mirrorManagedProcessesJson, _mirrorWorktreePoolJson + each store's _readJson fallback path"
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"file": "CLAUDE.md",
|
|
160
|
+
"lines": "47-66, 240-265",
|
|
161
|
+
"note": "State Files + Concurrency sections still describe JSON files as the source of truth; they describe a layered SQLite-then-mirror reality in places but the headline contract still reads as JSON-primary. Rewrite these sections to make SQL-as-source-of-truth the headline and the JSON mirrors a transitional compatibility detail."
|
|
162
|
+
}
|
|
79
163
|
],
|
|
80
164
|
"removalGate": "All direct-readers of the mirror JSON files must be confirmed routed through their respective SQL store's read helper. Specifically: (a) grep the codebase for `safeJson`, `safeJsonArr`, `safeJsonObj`, `readFileSync(...work-items.json|pull-requests.json|metrics.json|watches.json|schedule-runs.json|pipeline-runs.json|managed-processes.json|worktree-pool.json|log.json|dispatch.json...)` and confirm every hit is either (i) a test fixture that can move to the SQL helper, or (ii) intentionally documented as bypassing SQL. (b) Run the full test suite with each store's _mirrorJsonFromSql temporarily neutered (returning early before safeWrite) and confirm 0 failures — that proves no production code path depends on the mirror. Once both conditions hold, removal deletes each store's _mirrorJsonFromSql call site in shared.js (mutateWorkItems/mutatePullRequests/etc.), the corresponding _readJsonArrayFallback paths, and the JSON file gitignore entries. CLAUDE.md update can ship independently as soon as someone has bandwidth.",
|
|
81
165
|
"targetRemovalDate": null,
|
|
@@ -85,15 +169,51 @@
|
|
|
85
169
|
"id": "prune-default-claude-config",
|
|
86
170
|
"description": "pruneDefaultClaudeConfig: active sanitizer that strips generated `config.claude.{binary,outputFormat,allowedTools,permissionMode}` defaults from persisted config.json so the `deprecated-config-claude` warning stops tripping on stale defaults left by older `minions init` versions. Sub-cluster of `config-claude-binary-override` — the prune deliberately preserves non-default user overrides (binary/allowedTools), which is what keeps the override branch in engine/runtimes/claude.js load-bearing.",
|
|
87
171
|
"code": [
|
|
88
|
-
{
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
{
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
172
|
+
{
|
|
173
|
+
"file": "engine/shared.js",
|
|
174
|
+
"lines": "3126",
|
|
175
|
+
"note": "pruneDefaultClaudeConfig definition: preserves non-default binary/allowedTools, always strips permissionMode + outputFormat"
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"file": "engine/shared.js",
|
|
179
|
+
"lines": "5673",
|
|
180
|
+
"note": "Module export entry"
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"file": "dashboard.js",
|
|
184
|
+
"lines": "202",
|
|
185
|
+
"note": "Called when loading config for the dashboard UI"
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"file": "dashboard.js",
|
|
189
|
+
"lines": "9116",
|
|
190
|
+
"note": "Called during first config save handler"
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"file": "dashboard.js",
|
|
194
|
+
"lines": "9331",
|
|
195
|
+
"note": "Called during second config save path"
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"file": "dashboard.js",
|
|
199
|
+
"lines": "9450",
|
|
200
|
+
"note": "Called during third config save path"
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"file": "minions.js",
|
|
204
|
+
"lines": "385",
|
|
205
|
+
"note": "Called during CLI init/update flow"
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"file": "test/unit.test.js",
|
|
209
|
+
"lines": "2260-2303",
|
|
210
|
+
"note": "Behavioural unit tests (default strip, override preservation, outputFormat unconditional strip) + dashboard call-site source pin"
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"file": "test/unit/runtime-fleet-helpers.test.js",
|
|
214
|
+
"lines": "546",
|
|
215
|
+
"note": "Source-inspection test pinning the dashboard handler call site"
|
|
216
|
+
}
|
|
97
217
|
],
|
|
98
218
|
"removalGate": "Telemetry: pruneDefaultClaudeConfig must return false (no mutation) for every call across all known engines for >=30 consecutive days (add an `_engine.pruneDefaultClaudeConfigStrips` counter if needed to observe this), AND the parent `config-claude-binary-override` entry must have already cleared its own gate. The dependency is strict: removing the prune while users still rely on the override branch would surface the `deprecated-config-claude` warning on every stale generated default. Once both conditions hold, removal is the function definition (engine/shared.js:3126), the export at :5673, all 5 call sites (dashboard.js:202, :9116, :9331, :9450; minions.js:385), and the tests at unit.test.js:2260-2303 + runtime-fleet-helpers.test.js:546.",
|
|
99
219
|
"targetRemovalDate": null,
|
|
@@ -106,7 +226,10 @@
|
|
|
106
226
|
"status": "removed",
|
|
107
227
|
"removedDate": "2026-06-25",
|
|
108
228
|
"code": [
|
|
109
|
-
{
|
|
229
|
+
{
|
|
230
|
+
"file": "engine/ado.js",
|
|
231
|
+
"note": "isAdoThrottled() arg-less branch and the global-OR fold over the per-org Map. Single call site to migrate: shared.getAdoOrgBase(project) is already in scope at every consumer."
|
|
232
|
+
}
|
|
110
233
|
],
|
|
111
234
|
"removalGate": "Two conditions must hold simultaneously: (a) grep `engine/ado.js` for `isAdoThrottled\\s*\\(\\s*\\)` and confirm zero arg-less call sites remain across the engine — every caller passes a concrete `orgBase` resolved via `shared.getAdoOrgBase(project)`; (b) `GET /api/diagnostics/ado-throttle` on a live engine has been observed for >=2 consecutive weeks reporting per-org keys (proves the per-org Map is populated under load and the global-OR isn't masking a regression). Once both hold, removal deletes the arg-less branch in isAdoThrottled and the global-OR fold; callers that still pass no argument become an immediate, surfaced bug rather than a silent over-throttle.",
|
|
112
235
|
"targetRemovalDate": "2026-08-03",
|
|
@@ -114,16 +237,18 @@
|
|
|
114
237
|
},
|
|
115
238
|
{
|
|
116
239
|
"id": "pr-link-autoObserve-body-param",
|
|
117
|
-
"
|
|
240
|
+
"status": "removed",
|
|
241
|
+
"removedDate": "2026-06-25",
|
|
242
|
+
"description": "Legacy `autoObserve` body parameter on `POST /api/pull-requests/link`. Replaced by canonical `contextOnly` body param (inverse boolean: `autoObserve: false` ⇔ `contextOnly: true`). This was a READ-BRIDGE on the input side only — the handler read `body.contextOnly` first and fell back to `!body.autoObserve` for callers not yet migrated. Removed by M001 (work/M001).",
|
|
118
243
|
"code": [
|
|
119
244
|
{
|
|
120
245
|
"file": "dashboard.js",
|
|
121
|
-
"note": "linkPullRequestForTracking
|
|
246
|
+
"note": "Removed: linkPullRequestForTracking no longer accepts autoObserve in the destructure; resolvedContextOnly is now simply typeof contextOnly === 'boolean' ? contextOnly : false. Route registry params string no longer lists autoObserve?."
|
|
122
247
|
}
|
|
123
248
|
],
|
|
124
249
|
"deprecated": "2026-06-08",
|
|
125
250
|
"targetRemovalDate": "2026-06-25",
|
|
126
|
-
"notes": "
|
|
251
|
+
"notes": "Removed on 2026-06-25. On-disk sweep confirmed CLEAN on 2026-06-18. autoObserve parameter silently ignored by server after this removal — callers that still send it will see the value dropped with contextOnly defaulting to false."
|
|
127
252
|
},
|
|
128
253
|
{
|
|
129
254
|
"id": "worktreemode-field-rename",
|
|
@@ -150,10 +275,23 @@
|
|
|
150
275
|
"id": "pr-observe-observe-body-param",
|
|
151
276
|
"description": "Legacy `observe` body parameter on `POST /api/pull-requests/observe`. The W-mq5s5ttx000j7ab8 endpoint sub-WI introduces canonical `contextOnly` as the inverse (`observe: false` ⇔ `contextOnly: true`) and keeps `observe` accepted for backward compat. Registering the deprecation here so the alias has a documented removal path; the WI explicitly notes this entry is the implementer's call (it is kept for backward compat and may live longer than the underscore-prefixed record fields).",
|
|
152
277
|
"code": [
|
|
153
|
-
{
|
|
278
|
+
{
|
|
279
|
+
"file": "dashboard.js",
|
|
280
|
+
"note": "POST /api/pull-requests/observe handler reads `body.contextOnly` first, then falls back to `!body.observe` for backwards compat."
|
|
281
|
+
}
|
|
154
282
|
],
|
|
155
283
|
"deprecated": "2026-06-08",
|
|
156
284
|
"targetRemovalDate": null,
|
|
157
285
|
"notes": "targetRemovalDate intentionally null — unlike the record-field aliases (`_contextOnly`, `_autoObserve`, `_manual`) which carry a 7-day clock, the `observe` body param is documented as a longer-lived back-compat alias. Set targetRemovalDate to a concrete future date once the dashboard UI + any client scripts are confirmed to POST `contextOnly` exclusively. Removal scope when the date is set: drop the `body.observe` fallback in dashboard.js, drop `observe` from the route registry params, and update any client still POSTing `observe`."
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
"id": "discover-review-skills-shim",
|
|
289
|
+
"description": "engine/discover-review-skills.js is a 25-line re-export shim for engine/discover-project-skills.js. Zero production callers; only test files (discover-review-skills.test.js, discover-project-skills.test.js:706) reference it.",
|
|
290
|
+
"addedDate": "2026-06-24",
|
|
291
|
+
"targetRemovalDate": "2026-09-01",
|
|
292
|
+
"removalGate": "No external callers import discover-review-skills.js; test files updated to import from discover-project-skills.js directly.",
|
|
293
|
+
"removalScope": "Delete engine/discover-review-skills.js, delete test/unit/discover-review-skills.test.js, update test/unit/discover-project-skills.test.js:706 to import from discover-project-skills.js.",
|
|
294
|
+
"autoRemoveSafe": false,
|
|
295
|
+
"notes": "gate: zero test references to discover-review-skills; must update tests to point to discover-project-skills.js before file can be deleted"
|
|
158
296
|
}
|
|
159
297
|
]
|
|
@@ -43,6 +43,23 @@ The engine never calls `git reset --hard`, `git clean -fd`, `git stash`, or any
|
|
|
43
43
|
|
|
44
44
|
A thrown error from `prepareLiveCheckout` — a required-arg/ref-validation guard, or a transient `git status`/`rev-parse`/`checkout`/`symbolic-ref` failure — is **not** proof that the operator tree is dirty. `spawnAgent`'s `catch` block therefore completes the dispatch with the **separate** `FAILURE_CLASS.LIVE_CHECKOUT_FAILED` (`'live-checkout-failed'`), which is deliberately **excluded** from `engine/dispatch.js`'s `neverRetry` set. The dispatcher auto-retries with bounded backoff up to `ENGINE_DEFAULTS.maxRetries`, so racy branch-lock handoff, a just-finished sibling dispatch, or transient git state recovers on the next attempt **without a manual `/api/work-items/retry`**. Genuinely terminal underlying reasons (auth, validation) still short-circuit via the reason-string check in `isRetryableFailureReason`. No dirty-files inbox alert is written and no `_pendingReason: 'live_checkout_dirty'` stamp is applied for this path — those belong to the confirmed-dirty result above.
|
|
45
45
|
|
|
46
|
+
> **`git status --porcelain` maxBuffer (W-mqvejug6000eeb20).** All read-only git probes in `prepareLiveCheckout` / `restoreLiveCheckoutAtDispatchEnd` run with a 50 MB `maxBuffer` (`LIVE_CHECKOUT_GIT_MAX_BUFFER`, threaded through `baseOpts`). A live tree with thousands of dirty/untracked paths could otherwise overflow `execFile`'s 1 MB default and reject with `ERR_CHILD_PROCESS_STDOUT_MAXBUFFER` — a *thrown* error that mis-classified as a transient retryable `LIVE_CHECKOUT_FAILED` (per 2a) and retry-stormed to the cap instead of being surfaced as the dirty tree it actually was. A caller-supplied `gitOpts.maxBuffer` still wins (spread after the default).
|
|
47
|
+
|
|
48
|
+
#### 2b. Opt-in auto-reset on dirty (`liveCheckoutAutoReset`, W-mqvejug6000eeb20)
|
|
49
|
+
|
|
50
|
+
By default the dirty tree refusal (Guarantee 2) is terminal — the engine never touches the operator's uncommitted work. **Opt in** to automatic recovery with `liveCheckoutAutoReset`. When enabled, a dirty live tree is **force-reset to the remote** instead of refusing the dispatch:
|
|
51
|
+
|
|
52
|
+
1. `prepareLiveCheckout` detects the dirty tree (same porcelain preflight as Guarantee 2).
|
|
53
|
+
2. It resolves the effective flag via `shared.resolveLiveCheckoutAutoReset(project, engine)` — **per-project `project.liveCheckoutAutoReset` (boolean) wins**, else the fleet-wide `engine.liveCheckoutAutoReset`, else `false`. (engine.js does not thread project/engine config into the helper, so the production path self-resolves by loading `config.json` and matching the project by `localPath`; it **fails closed** to `false` on any config read error so a glitch can never silently trigger a destructive reset.)
|
|
54
|
+
3. If ON: `git fetch origin` + `git reset --hard origin/<branch>`, then **re-run the porcelain preflight once**. If the tree is now clean, dispatch proceeds normally. If the reset failed or the tree is *still* dirty, it falls back to the safe `{ ok:false, reason:'dirty' }` refusal — the engine never dispatches onto an unexpected tree.
|
|
55
|
+
4. On a successful reset it writes a single `live-checkout-autoreset-<wiId>` inbox note listing **exactly which paths were discarded**, so the operator can recover them from `git reflog` / `git fsck --lost-found`.
|
|
56
|
+
|
|
57
|
+
This is **DESTRUCTIVE** — it permanently discards the operator's uncommitted changes in the live checkout — which is why it is **OFF by default** and strictly opt-in. Use it only for unattended/CI-style live checkouts where the tree is expected to track the remote and any local drift is disposable.
|
|
58
|
+
|
|
59
|
+
- **Fleet-wide:** Dashboard → Settings → `Live-checkout auto-reset (fleet-wide)` (`engine.liveCheckoutAutoReset`).
|
|
60
|
+
- **Per-project override:** set `liveCheckoutAutoReset: true|false` on the project object in `config.json` (see the config snippet under *Enabling live mode*). A per-project boolean overrides the fleet-wide default for that project. *(Per-project Settings-UI persistence is deferred — the per-project override is config.json-only for now.)*
|
|
61
|
+
|
|
62
|
+
|
|
46
63
|
### 3. No auto-pull, no `--force`, no fast-forward
|
|
47
64
|
|
|
48
65
|
After the clean-tree check, `prepareLiveCheckout`:
|
|
@@ -109,6 +126,10 @@ The core invariant holds end-to-end through restore: **the engine only ever swit
|
|
|
109
126
|
"name": "android-aosp",
|
|
110
127
|
"localPath": "/home/yemi/aosp",
|
|
111
128
|
"checkoutMode": "live",
|
|
129
|
+
// "liveCheckoutAutoReset": true, // OPT-IN, DESTRUCTIVE — on a dirty tree,
|
|
130
|
+
// git fetch origin + git reset --hard origin/<branch> instead of refusing
|
|
131
|
+
// (discards uncommitted operator changes; logs a live-checkout-autoreset
|
|
132
|
+
// inbox note). Overrides the fleet-wide engine.liveCheckoutAutoReset.
|
|
112
133
|
// …
|
|
113
134
|
}]
|
|
114
135
|
}
|
|
@@ -118,7 +139,7 @@ Absent / `null` / `''` reads as `'worktree'` (the default) — explicit is prefe
|
|
|
118
139
|
|
|
119
140
|
### Recovering from `live_checkout_dirty` refusal
|
|
120
141
|
|
|
121
|
-
When dispatch is blocked by a dirty tree, the dashboard shows the work item as pending with `_pendingReason: 'live_checkout_dirty'` and an inbox alert lists the dirty files. From the project checkout:
|
|
142
|
+
When dispatch is blocked by a dirty tree, the dashboard shows the work item as pending with `_pendingReason: 'live_checkout_dirty'` and an inbox alert lists the dirty files. (To make the engine *auto-recover* from this instead of refusing — at the cost of discarding the dirty changes — enable [opt-in auto-reset](#2b-opt-in-auto-reset-on-dirty-livecheckoutautoreset-w-mqvejug6000eeb20).) From the project checkout:
|
|
122
143
|
|
|
123
144
|
```bash
|
|
124
145
|
# Option A — preserve work for later
|
|
@@ -211,7 +232,7 @@ To opt back out, clear `liveValidation` from the project config (or set to `null
|
|
|
211
232
|
Live-checkout mode is deliberately small. These are NOT supported and will not be added:
|
|
212
233
|
|
|
213
234
|
- **No `auto` mode.** The choice between `worktree` and `live` is per-project and operator-set. The engine will not auto-detect submodules / `repo` workspaces and silently switch modes.
|
|
214
|
-
- **No auto-stash on dirty refusal.** The engine refuses and exits; it never `git stash`es to "make room" for a dispatch. Stashes silently mutate the operator's tree and conflate engine state with operator state.
|
|
235
|
+
- **No auto-stash on dirty refusal.** The engine refuses and exits; it never `git stash`es to "make room" for a dispatch. Stashes silently mutate the operator's tree and conflate engine state with operator state. (The opt-in `liveCheckoutAutoReset` is the one sanctioned escape hatch — but it **discards** rather than stashes, is OFF by default, and is gated on explicit per-project / fleet-wide opt-in. See [§2b](#2b-opt-in-auto-reset-on-dirty-livecheckoutautoreset-w-mqvejug6000eeb20).)
|
|
215
236
|
- **No concurrent dispatches per project.** The cap is 1; raising it would require per-WI subdirectories, which live mode explicitly does not provide.
|
|
216
237
|
- **No per-WI subdirectory isolation.** Live mode is one-checkout-per-project by design. If you need isolation, use `checkoutMode: 'worktree'` (the default).
|
|
217
238
|
- **No per-WI override.** `checkoutMode` is per-project only. There is no `meta.checkoutMode` on a work item that overrides the project setting.
|
|
@@ -223,8 +244,9 @@ Live-checkout mode is deliberately small. These are NOT supported and will not b
|
|
|
223
244
|
| File | Purpose |
|
|
224
245
|
|---|---|
|
|
225
246
|
| `engine/shared.js` — `CHECKOUT_MODES`, `validateCheckoutMode`, `resolveCheckoutMode`, `isLiveCheckoutProject` | Enum + validator + back-compat resolver (P-a3f9b201; consolidated W-mqiaw974). |
|
|
247
|
+
| `engine/shared.js` — `resolveLiveCheckoutAutoReset` + `ENGINE_DEFAULTS.liveCheckoutAutoReset` | Pure precedence resolver (per-project boolean > fleet-wide engine default > false) + the fleet-wide default (OFF). Gates the opt-in dirty-tree auto-reset in `prepareLiveCheckout` (W-mqvejug6000eeb20). |
|
|
226
248
|
| `engine/shared.js` — `resolveSpawnPaths` | Returns `{ cwd: localPath, worktreeRootDir: null, liveMode: true }` for live projects (P-a3f9b202). |
|
|
227
|
-
| `engine/live-checkout.js` — `prepareLiveCheckout` | Pure helper: dirty check, mid-operation / detached-HEAD preflight (incl. `BISECT_LOG`; throw-on-git-dir-failure; exit-1-only detached), original-ref capture, **already-on-branch fast path**, `refs/heads/<branch>` existence check, branch resolution from HEAD (no fetch — issue #226), **no-half-switch + `blob-fetch` classification** for partial-clone hydration failures (P-a3f9b203; preflight + capture P-b2e8d4a6; hardening PL-live-checkout-reliability-hardening). |
|
|
249
|
+
| `engine/live-checkout.js` — `prepareLiveCheckout` | Pure helper: dirty check, mid-operation / detached-HEAD preflight (incl. `BISECT_LOG`; throw-on-git-dir-failure; exit-1-only detached), original-ref capture, **already-on-branch fast path**, `refs/heads/<branch>` existence check, branch resolution from HEAD (no fetch — issue #226), **no-half-switch + `blob-fetch` classification** for partial-clone hydration failures, **opt-in dirty auto-reset** (`git fetch origin` + `reset --hard origin/<branch>` + re-check + `live-checkout-autoreset-<wiId>` note when `liveCheckoutAutoReset` is on), **50 MB git maxBuffer** (P-a3f9b203; preflight + capture P-b2e8d4a6; hardening PL-live-checkout-reliability-hardening; auto-reset + maxBuffer W-mqvejug6000eeb20). |
|
|
228
250
|
| `engine/live-checkout.js` — `restoreLiveCheckoutAtDispatchEnd` | Dispatch-end auto-restore (plain `git checkout <originalRef>`, never `--force`/reset/clean/stash, best-effort) + **self-healing dirty recovery** (auto-commit agent WIP onto the agent branch) + `live-checkout-failed-<dispatchId>` terminal-failure alert + `live-checkout-branch-<dispatchId>` fallback notify (now also on unexpected restore errors) (P-d9e6b2c4; self-heal PL-live-checkout-reliability-hardening). |
|
|
229
251
|
| `engine/live-checkout.js` — `maybeRestoreLiveCheckoutFromRecord` | Shared wrapper that fires the dispatch-end restore from a persisted dispatch record; used by `cli.js` + both `timeout.js` reaping paths so a restart-spanning live dispatch is never stranded (PL-live-checkout-reliability-hardening). |
|
|
230
252
|
| `engine.js` — `spawnAgent` live-mode block | Calls `prepareLiveCheckout`, handles dirty / throw branches, gates `git worktree add` on `!liveMode` (P-a3f9b204). |
|
|
@@ -237,7 +259,7 @@ Live-checkout mode is deliberately small. These are NOT supported and will not b
|
|
|
237
259
|
| `engine.js` — worktree-pool / orphan-GC short-circuits | `worktreePath===null` no-ops in live mode (P-a3f9b206). |
|
|
238
260
|
| `engine/cleanup.js` — `runPeriodicWorktreeSweep` live filter | Excludes live-checkout projects from the registry-derived periodic worktree GC so the operator's primary checkout never enters the GC decision surface (PL-live-checkout-reliability-hardening). |
|
|
239
261
|
| `engine/create-pr-worktree.js` — `prepareCreatePrWorktree` step-4 restore | `reset --hard HEAD` (not the index-leaking `checkout -- .`) + retried untracked removal + `liveTreeDirty` surfaced + `shared.removeWorktree` teardown (PL-live-checkout-reliability-hardening). |
|
|
240
|
-
| `dashboard/js/settings.js` — checkoutMode dropdown + chip | Operator-facing UI (P-a3f9b207). |
|
|
262
|
+
| `dashboard/js/settings.js` — checkoutMode dropdown + chip + `set-liveCheckoutAutoReset` fleet toggle | Operator-facing UI; the fleet-wide auto-reset toggle persists to `engine.liveCheckoutAutoReset` (per-project UI deferred — config.json only) (P-a3f9b207; auto-reset toggle W-mqvejug6000eeb20). |
|
|
241
263
|
| `test/unit/{resolve-spawn-paths-live-mode,prepare-live-checkout,spawn-agent-live-mode-wiring}.test.js` | Wiring and contract tests (P-a3f9b208). |
|
|
242
264
|
| `engine/shared.js` — `FAILURE_CLASS.LIVE_CHECKOUT_DIRTY` | Non-retryable refusal class. |
|
|
243
265
|
| `engine/shared.js` — `FAILURE_CLASS.LIVE_CHECKOUT_MID_OPERATION` | Non-retryable refusal class for a mid-operation / detached-HEAD operator tree (in-progress merge/rebase/cherry-pick/revert/bisect or detached HEAD), distinct from the dirty-tree class. Emitted by `spawnAgent`'s mid-op / detached-HEAD refusal block (P-a7f3c1d9; wired P-c5a1f3b8). |
|
package/engine/ado.js
CHANGED
|
@@ -2224,6 +2224,13 @@ async function reconcilePrs(config) {
|
|
|
2224
2224
|
// are human-authored and should not be auto-tracked or auto-reviewed.
|
|
2225
2225
|
if (!confirmedItemId) continue;
|
|
2226
2226
|
|
|
2227
|
+
// Only auto-link when the work item was dispatched to a configured Minions
|
|
2228
|
+
// agent. PRs from human coworkers or external Copilot agents who happen to
|
|
2229
|
+
// use Minions-style branch naming (work/W-xxx) must not enter the review loop.
|
|
2230
|
+
const configuredAgentIds = new Set(Object.keys(config.agents || {}));
|
|
2231
|
+
const dispatchedTo = String(linkedItem?.dispatched_to || '').toLowerCase();
|
|
2232
|
+
if (!dispatchedTo || !configuredAgentIds.has(dispatchedTo)) continue;
|
|
2233
|
+
|
|
2227
2234
|
const entry = {
|
|
2228
2235
|
id: prId,
|
|
2229
2236
|
prNumber: adoPr.pullRequestId,
|
|
@@ -2512,10 +2519,12 @@ async function checkLiveBuildAndConflict(pr, project) {
|
|
|
2512
2519
|
}
|
|
2513
2520
|
}
|
|
2514
2521
|
|
|
2515
|
-
async function fetchAdoPrMetadata(prNum, adoOrg, adoProj, adoRepo) {
|
|
2522
|
+
async function fetchAdoPrMetadata(prNum, adoOrg, adoProj, adoRepo, project) {
|
|
2516
2523
|
const token = await getAdoToken();
|
|
2517
2524
|
if (!token) return null;
|
|
2518
|
-
|
|
2525
|
+
// Use full project config when provided so visualstudio.com prUrlBase
|
|
2526
|
+
// resolves correctly via getAdoOrgBase; fall back to bare { adoOrg } stub.
|
|
2527
|
+
const orgBase = getAdoOrgBase(project && project.prUrlBase ? project : { adoOrg });
|
|
2519
2528
|
const url = `${orgBase}/${encodeURIComponent(adoProj)}/_apis/git/repositories/${encodeURIComponent(adoRepo)}/pullrequests/${encodeURIComponent(String(prNum))}?api-version=7.1`;
|
|
2520
2529
|
const pr = await adoFetch(url, token);
|
|
2521
2530
|
if (!pr) return null;
|
package/engine/github.js
CHANGED
|
@@ -1498,6 +1498,13 @@ async function reconcilePrs(config) {
|
|
|
1498
1498
|
const authorLogin = String(ghPr.user?.login || '').trim().toLowerCase();
|
|
1499
1499
|
if (!confirmedItemId || !configuredAuthorLogins.has(authorLogin)) continue;
|
|
1500
1500
|
|
|
1501
|
+
// Only auto-link when the work item was dispatched to a configured Minions
|
|
1502
|
+
// agent. PRs from human coworkers or external Copilot agents who happen to
|
|
1503
|
+
// use Minions-style branch naming (work/W-xxx) must not enter the review loop.
|
|
1504
|
+
const configuredAgentIds = new Set(Object.keys(config.agents || {}));
|
|
1505
|
+
const dispatchedTo = String(linkedItem?.dispatched_to || '').toLowerCase();
|
|
1506
|
+
if (!dispatchedTo || !configuredAgentIds.has(dispatchedTo)) continue;
|
|
1507
|
+
|
|
1501
1508
|
const entry = {
|
|
1502
1509
|
id: prId,
|
|
1503
1510
|
prNumber: ghPr.number,
|