@yemi33/minions 0.1.2305 → 0.1.2307
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 +1 -0
- package/dashboard/js/refresh.js +17 -13
- package/dashboard/js/render-other.js +251 -3
- package/dashboard/js/render-plans.js +1 -1
- package/dashboard/js/render-prs.js +4 -1
- package/dashboard/js/render-utils.js +13 -0
- package/dashboard/js/render-work-items.js +11 -13
- package/dashboard/js/settings.js +30 -3
- package/dashboard/slim/body.html +6 -22
- package/dashboard/slim/js/modals-tiles.js +32 -22
- package/dashboard/slim/js/plans.js +24 -516
- package/dashboard/slim/styles.css +10 -7
- package/dashboard/styles.css +58 -0
- package/dashboard.js +66 -9
- package/docs/completion-reports.md +27 -2
- package/docs/copilot-cli-schema.md +31 -1
- package/docs/design-state-storage.md +1 -1
- package/docs/harness-transparency.md +18 -0
- package/docs/live-checkout-mode.md +57 -8
- package/engine/ado-comment.js +5 -2
- package/engine/ado.js +37 -0
- package/engine/cli.js +58 -3
- package/engine/comment-format.js +82 -2
- package/engine/consolidation.js +59 -2
- package/engine/discover-project-skills.js +4 -0
- package/engine/dispatch.js +36 -1
- package/engine/gh-comment.js +8 -3
- package/engine/lifecycle.js +241 -25
- package/engine/live-checkout.js +335 -7
- package/engine/playbook.js +2 -2
- package/engine/pre-dispatch-eval.js +54 -5
- package/engine/queries.js +11 -7
- package/engine/shared.js +96 -6
- package/engine/supervisor.js +144 -19
- package/engine/watchdog.js +10 -0
- package/engine.js +210 -5
- package/package.json +1 -1
- package/playbooks/review.md +2 -0
- package/playbooks/shared-rules.md +9 -0
|
@@ -680,12 +680,15 @@
|
|
|
680
680
|
/* Cockpit-tile detail modal: list of dispatches / PRs / watches, or a
|
|
681
681
|
key/value engine readout (reuses .agent-detail-row). */
|
|
682
682
|
#slim-tile-modal .modal { width: 720px; max-width: calc(100vw - 32px); }
|
|
683
|
-
/* W-mqrdggys000f94a4 — the Queued-work
|
|
684
|
-
screen in an iframe; widen the modal +
|
|
685
|
-
embedded
|
|
686
|
-
#slim-tile-modal.tile-modal--work .modal
|
|
687
|
-
#slim-tile-modal.tile-modal--
|
|
688
|
-
|
|
683
|
+
/* W-mqrdggys000f94a4 / W-mr28ko750010199f — the Queued-work + Plans tiles
|
|
684
|
+
embed a full classic screen (/work, /plans) in an iframe; widen the modal +
|
|
685
|
+
remove body padding so the embedded screen gets the full width/height. */
|
|
686
|
+
#slim-tile-modal.tile-modal--work .modal,
|
|
687
|
+
#slim-tile-modal.tile-modal--plans .modal { width: 1180px; }
|
|
688
|
+
#slim-tile-modal.tile-modal--work .modal-body,
|
|
689
|
+
#slim-tile-modal.tile-modal--plans .modal-body { padding: 0; }
|
|
690
|
+
.slim-work-embed,
|
|
691
|
+
.slim-plans-embed {
|
|
689
692
|
display: block; width: 100%; height: calc(100vh - 160px); min-height: 360px;
|
|
690
693
|
border: 0; background: var(--bg);
|
|
691
694
|
}
|
|
@@ -766,7 +769,7 @@
|
|
|
766
769
|
|
|
767
770
|
/* Knowledge control panel (slim-knowledge-modal): Pinned Context / Notes /
|
|
768
771
|
KB tabs in one box. Reuses .tile-* and .pinned-row primitives. */
|
|
769
|
-
.slim-knowledge-modal-inner
|
|
772
|
+
.slim-knowledge-modal-inner { width: 720px; max-width: calc(100vw - 32px); }
|
|
770
773
|
.kn-tabs { display: flex; gap: 6px; margin-left: 16px; }
|
|
771
774
|
.kn-tab {
|
|
772
775
|
border: 1px solid var(--border);
|
package/dashboard/styles.css
CHANGED
|
@@ -1288,6 +1288,64 @@
|
|
|
1288
1288
|
.project-mode-live { background: rgba(210,153,34,0.15); color: var(--yellow); border: 1px solid var(--yellow); }
|
|
1289
1289
|
.project-mode-hybrid { background: rgba(88,166,255,0.15); color: var(--blue); border: 1px solid var(--blue); }
|
|
1290
1290
|
|
|
1291
|
+
/* Clickable checkout-mode pill (W-mr1b67zi0006b788) — swaps the "help"
|
|
1292
|
+
* cursor for "pointer" and adds a hover/focus highlight so it reads as
|
|
1293
|
+
* interactive, opening the picker in _openCheckoutModeMenu(). */
|
|
1294
|
+
.project-mode-pill-clickable { cursor: pointer; }
|
|
1295
|
+
.project-mode-pill-clickable:hover,
|
|
1296
|
+
.project-mode-pill-clickable:focus-visible {
|
|
1297
|
+
filter: brightness(1.2);
|
|
1298
|
+
outline: 1px solid var(--blue);
|
|
1299
|
+
outline-offset: 1px;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
/* Checkout-mode picker popover (W-mr1b67zi0006b788). Rendered via
|
|
1303
|
+
* document.createElement in _openCheckoutModeMenu (no innerHTML), appended
|
|
1304
|
+
* to <body> and positioned under the clicked pill. */
|
|
1305
|
+
.checkout-mode-menu {
|
|
1306
|
+
z-index: 1000;
|
|
1307
|
+
min-width: 220px;
|
|
1308
|
+
background: var(--surface2);
|
|
1309
|
+
border: 1px solid var(--border);
|
|
1310
|
+
border-radius: var(--radius-md, 6px);
|
|
1311
|
+
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
|
|
1312
|
+
padding: 6px;
|
|
1313
|
+
font-size: var(--text-sm);
|
|
1314
|
+
}
|
|
1315
|
+
.checkout-mode-menu-title {
|
|
1316
|
+
font-weight: 600; color: var(--muted);
|
|
1317
|
+
padding: 4px 8px 6px; border-bottom: 1px solid var(--border);
|
|
1318
|
+
margin-bottom: 4px;
|
|
1319
|
+
}
|
|
1320
|
+
.checkout-mode-menu-item {
|
|
1321
|
+
padding: 6px 8px; border-radius: 4px; cursor: pointer;
|
|
1322
|
+
color: var(--text);
|
|
1323
|
+
}
|
|
1324
|
+
.checkout-mode-menu-item:hover,
|
|
1325
|
+
.checkout-mode-menu-item:focus {
|
|
1326
|
+
background: var(--surface); outline: none;
|
|
1327
|
+
}
|
|
1328
|
+
.checkout-mode-menu-item-active { color: var(--blue); font-weight: 600; }
|
|
1329
|
+
.checkout-mode-menu-item-desc {
|
|
1330
|
+
font-size: var(--text-xs); color: var(--muted); font-weight: 400;
|
|
1331
|
+
margin-top: 2px; line-height: 1.35;
|
|
1332
|
+
}
|
|
1333
|
+
.checkout-mode-menu-select {
|
|
1334
|
+
width: 100%; margin: 4px 0 8px; padding: 4px 6px;
|
|
1335
|
+
background: var(--surface); color: var(--text); border: 1px solid var(--border);
|
|
1336
|
+
border-radius: 4px;
|
|
1337
|
+
}
|
|
1338
|
+
.checkout-mode-menu-actions {
|
|
1339
|
+
display: flex; justify-content: flex-end; gap: 6px; padding-top: 4px;
|
|
1340
|
+
}
|
|
1341
|
+
.checkout-mode-menu-btn {
|
|
1342
|
+
padding: 4px 10px; border-radius: 4px; border: 1px solid var(--border);
|
|
1343
|
+
background: var(--surface); color: var(--text); cursor: pointer; font-size: var(--text-sm);
|
|
1344
|
+
}
|
|
1345
|
+
.checkout-mode-menu-btn-primary {
|
|
1346
|
+
background: var(--blue); color: #fff; border-color: var(--blue);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1291
1349
|
/* QA tab (W-mpeiwz6k0005bf34-d) — targets / runbooks / runs sections.
|
|
1292
1350
|
* Reuses surface/border/text tokens defined in :root so the QA page
|
|
1293
1351
|
* blends with the rest of the dashboard. */
|
package/dashboard.js
CHANGED
|
@@ -434,6 +434,15 @@ function mergeSettingsConfigUpdate(current, candidate, body, patch = {}) {
|
|
|
434
434
|
} else {
|
|
435
435
|
delete currentProject.checkoutMode;
|
|
436
436
|
}
|
|
437
|
+
// W-mqtvnnj1000357fa — mirror the per-project liveCheckoutAutoStash
|
|
438
|
+
// override the same way: present on candidate → copy; absent → clear so
|
|
439
|
+
// the engine fleet-wide `engine.liveCheckoutAutoStash` applies. Without
|
|
440
|
+
// this branch the validated update is silently dropped before reaching disk.
|
|
441
|
+
if (Object.prototype.hasOwnProperty.call(candidateProject, 'liveCheckoutAutoStash')) {
|
|
442
|
+
currentProject.liveCheckoutAutoStash = candidateProject.liveCheckoutAutoStash;
|
|
443
|
+
} else {
|
|
444
|
+
delete currentProject.liveCheckoutAutoStash;
|
|
445
|
+
}
|
|
437
446
|
// W-mqiaw974 (issue #241): the field was renamed from the legacy
|
|
438
447
|
// `worktreeMode`. Drop any stale legacy key on every settings save so a
|
|
439
448
|
// migrated project never carries both fields.
|
|
@@ -1350,12 +1359,15 @@ function linkPullRequestForTracking({ url, title, project: projectName, contextO
|
|
|
1350
1359
|
// W-mpmwxkzm0009ba0b — Per-row auto-observe toggle backing helper for
|
|
1351
1360
|
// POST /api/pull-requests/observe. Flips canonical `contextOnly` on an
|
|
1352
1361
|
// existing tracked PR record under a lock (per CLAUDE.md mutate convention).
|
|
1353
|
-
// Body shape: { host: 'github'|'ado', slug, number,
|
|
1354
|
-
//
|
|
1355
|
-
// (W-mq5s5ttx000j7ab8-c)
|
|
1362
|
+
// Body shape: { host: 'github'|'ado', slug, number, contextOnly: boolean }.
|
|
1363
|
+
// `contextOnly` is canonical and read first; the legacy `observe` body
|
|
1364
|
+
// param (W-mq5s5ttx000j7ab8-c) is accepted as a documented back-compat
|
|
1365
|
+
// fallback (`contextOnly = !observe`) for any caller that still sends it —
|
|
1366
|
+
// the dashboard client itself now sends `contextOnly` exclusively
|
|
1367
|
+
// (P-2b6e4d81; see docs/deprecated.json#pr-observe-observe-body-param).
|
|
1356
1368
|
// Returns the updated record + the PR path that was touched. Throws an
|
|
1357
1369
|
// Error with `statusCode` for the route handler to map to an HTTP status.
|
|
1358
|
-
function updatePullRequestObserveFlag({ host, slug, number, observe } = {}, config = CONFIG, minionsDir = MINIONS_DIR) {
|
|
1370
|
+
function updatePullRequestObserveFlag({ host, slug, number, observe, contextOnly } = {}, config = CONFIG, minionsDir = MINIONS_DIR) {
|
|
1359
1371
|
const hostStr = String(host || '').trim().toLowerCase();
|
|
1360
1372
|
const slugStr = String(slug || '').trim();
|
|
1361
1373
|
const numberInt = Number.parseInt(number, 10);
|
|
@@ -1374,8 +1386,13 @@ function updatePullRequestObserveFlag({ host, slug, number, observe } = {}, conf
|
|
|
1374
1386
|
err.statusCode = 400;
|
|
1375
1387
|
throw err;
|
|
1376
1388
|
}
|
|
1377
|
-
|
|
1378
|
-
|
|
1389
|
+
let contextOnlyValue;
|
|
1390
|
+
if (typeof contextOnly === 'boolean') {
|
|
1391
|
+
contextOnlyValue = contextOnly;
|
|
1392
|
+
} else if (typeof observe === 'boolean') {
|
|
1393
|
+
contextOnlyValue = !observe;
|
|
1394
|
+
} else {
|
|
1395
|
+
const err = new Error('contextOnly (or legacy observe) must be a boolean');
|
|
1379
1396
|
err.statusCode = 400;
|
|
1380
1397
|
throw err;
|
|
1381
1398
|
}
|
|
@@ -1394,7 +1411,7 @@ function updatePullRequestObserveFlag({ host, slug, number, observe } = {}, conf
|
|
|
1394
1411
|
shared.mutatePullRequests(prPath, (prs) => {
|
|
1395
1412
|
const pr = prs.find(p => p && p.id === canonicalId);
|
|
1396
1413
|
if (!pr) return prs;
|
|
1397
|
-
pr.contextOnly =
|
|
1414
|
+
pr.contextOnly = contextOnlyValue;
|
|
1398
1415
|
updated = { id: pr.id, contextOnly: pr.contextOnly };
|
|
1399
1416
|
updatedPath = prPath;
|
|
1400
1417
|
return prs;
|
|
@@ -10949,6 +10966,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10949
10966
|
// the per-project dropdown. resolveCheckoutMode honors the legacy
|
|
10950
10967
|
// worktreeMode field; 'worktree' (default) or 'live'.
|
|
10951
10968
|
checkoutMode: shared.resolveCheckoutMode(p),
|
|
10969
|
+
// W-mqtvnnj1000357fa — surface the RAW per-project auto-stash override
|
|
10970
|
+
// (true / false / undefined) so the Settings UI can pre-select the
|
|
10971
|
+
// tri-state dropdown. Undefined means "use fleet default"
|
|
10972
|
+
// (engine.liveCheckoutAutoStash); only an explicit boolean overrides.
|
|
10973
|
+
liveCheckoutAutoStash: (typeof p.liveCheckoutAutoStash === 'boolean') ? p.liveCheckoutAutoStash : undefined,
|
|
10952
10974
|
workSources: {
|
|
10953
10975
|
pullRequests: { enabled: p.workSources?.pullRequests?.enabled !== false, cooldownMinutes: p.workSources?.pullRequests?.cooldownMinutes ?? 30 },
|
|
10954
10976
|
workItems: { enabled: p.workSources?.workItems?.enabled !== false, cooldownMinutes: p.workSources?.workItems?.cooldownMinutes ?? 0 }
|
|
@@ -11404,6 +11426,18 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
11404
11426
|
else proj.liveValidation = validatedLv;
|
|
11405
11427
|
}
|
|
11406
11428
|
}
|
|
11429
|
+
// W-mqtvnnj1000357fa — per-project live-checkout auto-stash override.
|
|
11430
|
+
// null / '' / undefined clears the override (engine fleet-wide
|
|
11431
|
+
// `engine.liveCheckoutAutoStash` applies); an explicit boolean pins
|
|
11432
|
+
// the per-project decision. Anything else is coerced to boolean.
|
|
11433
|
+
if (Object.prototype.hasOwnProperty.call(update, 'liveCheckoutAutoStash')) {
|
|
11434
|
+
const rawStash = update.liveCheckoutAutoStash;
|
|
11435
|
+
if (rawStash === null || rawStash === '' || rawStash === undefined) {
|
|
11436
|
+
delete proj.liveCheckoutAutoStash;
|
|
11437
|
+
} else {
|
|
11438
|
+
proj.liveCheckoutAutoStash = !!rawStash;
|
|
11439
|
+
}
|
|
11440
|
+
}
|
|
11407
11441
|
}
|
|
11408
11442
|
}
|
|
11409
11443
|
|
|
@@ -13086,6 +13120,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13086
13120
|
const inputs = [
|
|
13087
13121
|
CONFIG_PATH,
|
|
13088
13122
|
path.join(ENGINE_DIR, 'dispatch.json'),
|
|
13123
|
+
// Central work-items.json at MINIONS_DIR root — backs the 'central'
|
|
13124
|
+
// scope (schedule/pipeline/CC-created rootless WIs, per
|
|
13125
|
+
// work-items-store.js#_filePathForScope). Without this, a new
|
|
13126
|
+
// central-scope WI never busts the ETag until dispatch.json changes.
|
|
13127
|
+
shared.centralWorkItemsPath(MINIONS_DIR),
|
|
13089
13128
|
];
|
|
13090
13129
|
for (const p of projects) {
|
|
13091
13130
|
if (p && p.name) {
|
|
@@ -13106,7 +13145,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13106
13145
|
{ method: 'GET', path: '/api/pull-requests', desc: 'Fully-enriched pull requests (per-project files joined + url backfill + _project stamp)', handler: (req, res) => {
|
|
13107
13146
|
const config = queries.getConfig();
|
|
13108
13147
|
const projects = config.projects || [];
|
|
13109
|
-
const inputs = [
|
|
13148
|
+
const inputs = [
|
|
13149
|
+
CONFIG_PATH,
|
|
13150
|
+
// Central pull-requests.json at MINIONS_DIR root — backs the
|
|
13151
|
+
// 'central' scope per pull-requests-store.js#_filePathForScope.
|
|
13152
|
+
// Mirrors the same class of staleness fix as /api/work-items.
|
|
13153
|
+
shared.centralPullRequestsPath(MINIONS_DIR),
|
|
13154
|
+
];
|
|
13110
13155
|
for (const p of projects) {
|
|
13111
13156
|
if (p && p.name) inputs.push(path.join(MINIONS_DIR, 'projects', p.name, 'pull-requests.json'));
|
|
13112
13157
|
}
|
|
@@ -13137,6 +13182,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13137
13182
|
const inputs = [
|
|
13138
13183
|
path.join(ENGINE_DIR, 'metrics.json'),
|
|
13139
13184
|
path.join(ENGINE_DIR, 'dispatch.json'),
|
|
13185
|
+
// getMetrics() enriches from getPullRequests(), which includes the
|
|
13186
|
+
// central pull-requests.json scope — include it here too so a new
|
|
13187
|
+
// central-scope PR busts the ETag (same class of bug as /api/work-items).
|
|
13188
|
+
shared.centralPullRequestsPath(MINIONS_DIR),
|
|
13140
13189
|
];
|
|
13141
13190
|
for (const p of projects) {
|
|
13142
13191
|
if (p && p.name) inputs.push(path.join(MINIONS_DIR, 'projects', p.name, 'pull-requests.json'));
|
|
@@ -13721,7 +13770,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13721
13770
|
}
|
|
13722
13771
|
}},
|
|
13723
13772
|
|
|
13724
|
-
{ method: 'POST', path: '/api/pull-requests/observe', desc: 'Toggle canonical contextOnly flag on a tracked PR (
|
|
13773
|
+
{ method: 'POST', path: '/api/pull-requests/observe', desc: 'Toggle canonical contextOnly flag on a tracked PR (legacy `observe` body param is preserved as the inverse for backward compat)', params: 'host (github|ado), slug, number, contextOnly (boolean; legacy observe (boolean) also accepted)', handler: async (req, res) => {
|
|
13725
13774
|
const body = await readBody(req);
|
|
13726
13775
|
reloadConfig();
|
|
13727
13776
|
try {
|
|
@@ -14866,6 +14915,14 @@ if (require.main === module) {
|
|
|
14866
14915
|
if (!alive) {
|
|
14867
14916
|
console.log(`[watchdog] Engine PID ${control.pid} is dead — auto-restarting...`);
|
|
14868
14917
|
restartEngine();
|
|
14918
|
+
// Crash-loop counter (W-mr2c46590003e3ee) — shares the same rolling
|
|
14919
|
+
// window in control.json as engine/supervisor.js and
|
|
14920
|
+
// engine/watchdog.js so a single crash isn't tallied 2-3x across
|
|
14921
|
+
// the three independent respawn mechanisms; only THIS automatic
|
|
14922
|
+
// dead-PID path counts (the user-triggered "Restart Engine" button
|
|
14923
|
+
// in handleEngineRestart above is deliberate, not self-healing, so
|
|
14924
|
+
// it's intentionally excluded).
|
|
14925
|
+
try { shared.recordEngineRespawn('dashboard:in-process-watchdog'); } catch { /* best-effort */ }
|
|
14869
14926
|
} else {
|
|
14870
14927
|
_markEngineAsDegradedIfFrozen();
|
|
14871
14928
|
}
|
|
@@ -85,6 +85,7 @@ Do **not** invent, regenerate, or share the nonce across dispatches — each spa
|
|
|
85
85
|
| `tests` | string | `pass`, `fail`, `skipped`, `N/A`, or a free-form note like `skipped — relying on PR pipeline`. |
|
|
86
86
|
| `pending` | string | Any remaining work, or `none`. |
|
|
87
87
|
| `followups` | array | Optional. PR-comment follow-up work items the agent dispatched via `POST /api/work-items` with `meta.pr_followup` set. Each entry: `{wi_id, title, reason, parent_comment_id}`. See [PR-comment follow-ups](#pr-comment-follow-ups). |
|
|
88
|
+
| `invalidates` | string[] | Optional. List of work-item IDs (e.g. `["W-abc123"]`) whose goals are superseded by this completion. The engine cancels each listed WI that is currently in `pending` or `queued` status, stamping `cancellationReason: "invalidated-by:<source-wi-id>"`. WIs in any other status (dispatched, done, failed, cancelled) are skipped with a warning — they are NOT cancelled. Missing IDs also log a warning and are skipped. Only processed on successful (`effectiveSuccess`) completions. See [Goal invalidation](#goal-invalidation). |
|
|
88
89
|
| `meta.review` | object | Optional, review tasks only. Records project-local review-skill outcome — see [Review skill outcomes](#review-skill-outcomes). Aliased by the generalized `meta.skill` (W-mq1cczi90006b21f). |
|
|
89
90
|
| `meta.skill` | object | Optional. Records project-local skill outcome for ANY playbook type that surfaces a `## Project skills` block (implement / fix / plan / review / etc.). See [Project skill outcomes](#project-skill-outcomes). |
|
|
90
91
|
| `meta.descriptionAudit` | object | Optional, `fix` / `implement` dispatches that push commits. Records the PR description audit + screenshot-refresh outcome — see [PR description audit](#pr-description-audit). |
|
|
@@ -117,7 +118,7 @@ All `meta.skill` fields are optional and backward-compatible — older agents th
|
|
|
117
118
|
|---|---|---|
|
|
118
119
|
| `meta.skill.invoked` | object | The project skill the agent actually ran. Shape: `{name, path, kind, intent}` where `kind` is one of `skill`, `command`, `slash-command` (mirrors the discovery layer in `engine/discover-project-skills.js`) and `intent` is the bucket it served (`build`, `review`, `fix`, `test`, `plan`, `research`, `deploy`, `observability`, `meta`). Omit when no skill was invoked. |
|
|
119
120
|
| `meta.skill.findings` | number | Count of findings/artifacts the invoked skill returned. Semantics is per-skill; treat as opaque integer. Omit when no skill was invoked. |
|
|
120
|
-
| `meta.skill.skipped` | object | Set when a project skill was available but the agent intentionally chose not to run it (trivial diff, out-of-scope, meta-work on the skill itself, etc.). Shape: `{name, reason}`. Mirror the skip rationale into the PR comment /
|
|
121
|
+
| `meta.skill.skipped` | object | Set when a project skill was available but the agent intentionally chose not to run it (trivial diff, out-of-scope, meta-work on the skill itself, etc.). Shape: `{name, reason}`. **Before recording a skip you MUST validate it against the skill's own documented scope** — re-read the skill's SKILL.md checklist and confirm the skip reason does not contradict any explicit "Flag if…" / "Verify that…" / "Requirements" item; if the diff trips any such item, skipping is not permitted (apply the skill or that item instead). Mirror the skip rationale into the PR comment so a human sees it **before** merge, not only via a later API call — `minions pr comment … --skill-skipped-file <f>` / `--skill-skipped-json <j>` folds a visible `> ⚠️ Skipped project skill: <name> — <reason>` callout into the same comment as the verdict (single renderer `engine/comment-format.js#buildSkippedSkillSection`, byte-identical on GitHub and ADO). The `{name, reason}` in the comment MUST match the one recorded here (no drift). |
|
|
121
122
|
|
|
122
123
|
Dispatchers can suppress the block entirely by setting `meta.skipProjectSkills: true` on the work item — the playbook then renders without the skills block. Use this for meta-work that targets the skill itself; the engine still accepts `meta.skill.skipped` in the report regardless. The PR-82 `meta.skipProjectReviewSkills` flag is preserved as an alias (suppresses the block on every dispatch type, not just review).
|
|
123
124
|
|
|
@@ -149,7 +150,7 @@ All `meta.review` fields are optional and backward-compatible — older agents t
|
|
|
149
150
|
|---|---|---|
|
|
150
151
|
| `meta.review.skillInvoked` | object | The project review skill the agent actually ran. Shape: `{name, path, kind}` where `kind` is one of `skill`, `command`, `slash-command` (mirrors the discovery layer in `engine/discover-project-skills.js`). Omit when no skill was invoked. Aliased by `meta.skill.invoked`. |
|
|
151
152
|
| `meta.review.skillFindings` | number | Count of findings the invoked skill returned. Used later to measure skill quality and the value-add of first-principles review on top. Omit when no skill was invoked. Aliased by `meta.skill.findings`. |
|
|
152
|
-
| `meta.review.skillSkipped` | object | Set when a project review skill was available but the agent intentionally chose not to run it (trivial diff, out-of-scope diff, meta-review of the skill itself, etc.). Shape: `{name, reason}`. Aliased by `meta.skill.skipped`. |
|
|
153
|
+
| `meta.review.skillSkipped` | object | Set when a project review skill was available but the agent intentionally chose not to run it (trivial diff, out-of-scope diff, meta-review of the skill itself, etc.). Shape: `{name, reason}`. Aliased by `meta.skill.skipped`. **The skip reason is validated against the skill's own scope** — before recording a skip the agent must re-read the skill's SKILL.md checklist and confirm the reason does not contradict any explicit "Flag if…" / "Verify that…" / "Requirements" item; if the diff trips any such item, skipping is not permitted. The same `{name, reason}` must also be surfaced in the PR review comment (visible `> ⚠️ Skipped project skill` callout, via `minions pr comment … --skill-skipped-json`) so a human sees it before merge — no drift between the report and the comment. |
|
|
153
154
|
|
|
154
155
|
Dispatchers can suppress the block entirely by setting `meta.skipProjectReviewSkills: true` on the review work item — the playbook then renders identically to the pre-W-mq16xtdx 8-step contract. Use this for meta-reviews of the review skill itself; the engine still accepts `meta.review.skillSkipped` in the report regardless. (W-mq1cczi90006b21f generalized this to `meta.skipProjectSkills`, which suppresses the block on every dispatch type — both flags are honored.)
|
|
155
156
|
|
|
@@ -191,6 +192,30 @@ Record the outcome under `meta.descriptionAudit`. All fields are optional and ba
|
|
|
191
192
|
|
|
192
193
|
Dispatchers can suppress the audit entirely by setting `meta.skipDescriptionAudit: true` on the work item — the playbook then renders an explicit "audit suppressed" notice instead of the audit steps. Use this for skill-meta updates, doc-only fixes whose PR description text won't be affected by the diff, and follow-up dispatches that explicitly own the description themselves.
|
|
193
194
|
|
|
195
|
+
## Goal invalidation
|
|
196
|
+
|
|
197
|
+
P-mqyp0009y025z6a7. An optional `invalidates: string[]` field in the completion report lets a completing work item cancel the goals of sibling or downstream WIs that are no longer needed.
|
|
198
|
+
|
|
199
|
+
```json
|
|
200
|
+
{
|
|
201
|
+
"status": "success",
|
|
202
|
+
"summary": "Implemented approach A; approach B work items are no longer needed.",
|
|
203
|
+
"invalidates": ["W-approach-b-001", "W-approach-b-002"]
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Semantics:**
|
|
208
|
+
|
|
209
|
+
- `invalidates` is an array of work-item ID strings (same format as WI IDs: `W-xxx`).
|
|
210
|
+
- The field is **optional and additive** — existing completion reports without it are unaffected.
|
|
211
|
+
- The engine processes `invalidates[]` only on **successful** completions (code 0, no `agentReportedFailure`, no `skipDoneStatus`).
|
|
212
|
+
- Each listed WI is cancelled only if it is currently in `pending` or `queued` status. WIs in any other status (`dispatched`, `done`, `failed`, `cancelled`, …) are **not** cancelled; a warning is logged instead.
|
|
213
|
+
- Missing WI IDs (IDs not found in any project's work-items) produce a warning but do **not** throw — the rest of the `invalidates[]` list is still processed.
|
|
214
|
+
- Each cancellation stamps `cancellationReason: "invalidated-by:<source-wi-id>"` and `cancelledAt` on the cancelled WI.
|
|
215
|
+
- A `work_items` state event is emitted for each successful cancellation so the dashboard reflects it.
|
|
216
|
+
|
|
217
|
+
**Implementation:** `engine/lifecycle.js#applyGoalInvalidation`, called from `runPostCompletionHooks` after the source WI is marked done.
|
|
218
|
+
|
|
194
219
|
## Harness usage (`harnessUsed`)
|
|
195
220
|
|
|
196
221
|
P-a8f3c2d1. Optional self-report of the harness affordances the agent actually
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
| `capabilities.bareMode` | **`false`** | No `--bare`. Closest equivalent is `--no-custom-instructions` (suppresses AGENTS.md only, not all auto-discovery). |
|
|
27
27
|
| `capabilities.fallbackModel` | **`false`** | No `--fallback-model` flag. |
|
|
28
28
|
| `capabilities.sessionPersistenceControl` | **`false`** | Copilot manages session state internally in `~/.copilot/session-state/`. Engine cannot opt out without `--config-dir`. |
|
|
29
|
+
| `capabilities.imageInput` | **`true`** | Base64 image payloads are materialized to temp files and passed as `--attachment <path>` (repeatable flag, W-mqv7324u0021db5d). See §10a. |
|
|
29
30
|
|
|
30
31
|
| Default | Value |
|
|
31
32
|
|---|---|
|
|
@@ -182,6 +183,7 @@ Empirically confirmed flags for non-interactive Copilot invocations:
|
|
|
182
183
|
| `--stream on` / `--stream off` | optional | Default is `on`. See §4. |
|
|
183
184
|
| `--enable-reasoning-summaries` | optional | Maps from `opts.reasoningSummaries`; only Anthropic models populate `assistant.reasoning_delta`. |
|
|
184
185
|
| `--add-dir <path>` | injected by spawn-agent | Same role as on the Claude path — registers extra read-allowed dirs (skill discovery). |
|
|
186
|
+
| `--attachment <path>` | injected for image inputs | Passes an image file to the model. Repeatable. The adapter writes each base64 image payload to a temp file, emits one `--attachment` per file, and deletes temps after spawn (W-mqv7324u0021db5d). Gated by `capabilities.imageInput`. |
|
|
185
187
|
| `-v` / `--verbose` | **never emit** | Does not exist on Copilot. The Claude adapter emits `--verbose`; the Copilot adapter MUST NOT. |
|
|
186
188
|
|
|
187
189
|
### 3.1 `--autopilot` vs single-shot
|
|
@@ -602,7 +604,8 @@ When implementing `engine/runtimes/copilot.js`:
|
|
|
602
604
|
3. `buildArgs(opts)` always emits:
|
|
603
605
|
`--output-format json -s --allow-all --no-ask-user --autopilot --log-level error`
|
|
604
606
|
plus the conditional flags from §3, plus `--no-custom-instructions` /
|
|
605
|
-
`--disable-builtin-mcps` per `opts.suppressAgentsMd` / `opts.disableBuiltinMcps
|
|
607
|
+
`--disable-builtin-mcps` per `opts.suppressAgentsMd` / `opts.disableBuiltinMcps`,
|
|
608
|
+
plus `--attachment <path>` (repeatable) for each image in `opts.images` (§10a).
|
|
606
609
|
**Never** emit `--verbose`.
|
|
607
610
|
4. `buildPrompt()` injects `<system>...</system>\n\n` block when sysprompt is
|
|
608
611
|
non-empty; passthrough otherwise (§2).
|
|
@@ -643,6 +646,33 @@ When the spike's findings disagree with the plan text, **this document wins**
|
|
|
643
646
|
|
|
644
647
|
---
|
|
645
648
|
|
|
649
|
+
## 10a. Image Attachments — `--attachment` (W-mqv7324u0021db5d)
|
|
650
|
+
|
|
651
|
+
`capabilities.imageInput: true`. The Copilot CLI accepts image files via
|
|
652
|
+
`--attachment <path>` (the flag is repeatable). The adapter's `buildArgs(opts)`
|
|
653
|
+
materializes each base64 payload from `opts.images` to a temp file in
|
|
654
|
+
`opts.tmpDir`, appends `--attachment <path>` per file, and arranges cleanup after
|
|
655
|
+
`spawn` returns.
|
|
656
|
+
|
|
657
|
+
```js
|
|
658
|
+
// Simplified adapter flow (see engine/runtimes/copilot.js _buildAttachmentArgs)
|
|
659
|
+
for (const img of opts.images ?? []) {
|
|
660
|
+
const ext = MIME_TO_EXT[img.mimeType] ?? 'bin';
|
|
661
|
+
const filePath = path.join(opts.tmpDir, `attachment-${i}.${ext}`);
|
|
662
|
+
fs.writeFileSync(filePath, Buffer.from(img.dataBase64, 'base64'));
|
|
663
|
+
args.push('--attachment', filePath);
|
|
664
|
+
}
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
**Supported MIME types** (empirically confirmed against Copilot v1.0.36):
|
|
668
|
+
`image/jpeg`, `image/png`, `image/gif`, `image/webp`.
|
|
669
|
+
|
|
670
|
+
When `capabilities.imageInput` is false (e.g., Codex adapter), the engine's
|
|
671
|
+
`_resolveImageOpts` returns a typed `model-unavailable` error before spawn so
|
|
672
|
+
callers get a clean rejection instead of a silent drop.
|
|
673
|
+
|
|
674
|
+
---
|
|
675
|
+
|
|
646
676
|
## Provenance
|
|
647
677
|
|
|
648
678
|
- Test host: Windows 11, PowerShell 7+, `copilot.exe` 1.0.36 from WinGet.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Author: Rebecca (Architect) | Date: 2026-04-07 | Status: **Accepted — implementation in progress**
|
|
4
4
|
|
|
5
|
-
> **Implementation status (as of 2026-06):** The `node:sqlite` recommendation in §3 has been adopted ahead of schedule. Phases 0–
|
|
5
|
+
> **Implementation status (as of 2026-06):** The `node:sqlite` recommendation in §3 has been adopted ahead of schedule. Phases 0–10 have shipped (events, dispatches, work_items, pull_requests, logs, metrics, watches, schedule_runs + pipeline_runs + managed_processes + worktree_pool, qa_runs + qa_sessions, pr_links, cooldowns + pending_rebases + cc_sessions + doc_sessions, and steering_deliveries (Phases 0–9); plans + prds + prd_items + prd_verify_prs (Phase 10, migration `015-plans-prds.js`) — see `CHANGELOG.md` and `engine/db/migrations/`). The SQLite schema lives under `engine/db/migrations/` and the singleton opens `engine/state.db` in WAL mode. Phase 8 added the first opt-out toggle for the JSON sidecars (`engine.qaDualWriteJson`, default true). Phase 9.4 went further and deleted the silent SQL-unavailable JSON fallbacks in the engine — SQL is now the only reader/writer for everything migrated; the JSON mirror layer is dual-written as a passive mirror and slated for deletion in Phase 9.5. Phase 10 dual-writes PRD state to SQL (`engine/prd-store.js`) and flips reads to SQL when the `prdReadsFromSql` feature flag is ON (default ON — reversible). The "Phase 2: estimated Node 26 LTS" timeline in §3 is now historical context; treat sections 1–3 as design rationale rather than a forward plan.
|
|
6
6
|
|
|
7
7
|
## Executive Summary
|
|
8
8
|
|
|
@@ -119,6 +119,24 @@ evaluation pass) can see what tooling drove a dispatch:
|
|
|
119
119
|
paths review/fix agents append the section themselves per
|
|
120
120
|
`playbooks/shared-rules.md` → "Harness transparency / self-report". Every
|
|
121
121
|
surface consumes the one renderer, so there is no second formatter to drift.
|
|
122
|
+
|
|
123
|
+
**Skipped in-scope project skill (W-mr287sc0000uc214).** The same PR-comment
|
|
124
|
+
surface also folds in a VISIBLE (non-collapsed) `> ⚠️ Skipped project skill:
|
|
125
|
+
<name> — <reason>` callout when the agent self-reported that it intentionally
|
|
126
|
+
skipped an available, in-scope project (review) skill
|
|
127
|
+
(`meta.review.skillSkipped` / `meta.skill.skipped`). This lived only in the
|
|
128
|
+
completion-report JSON before — invisible to a human reading the PR pre-merge.
|
|
129
|
+
`engine/comment-format.js#buildSkippedSkillSection(skillSkipped)` is the single
|
|
130
|
+
neutral renderer (sibling of `buildHarnessUsedSection`); it is threaded through
|
|
131
|
+
`buildMinionsCommentBody` (optional `skillSkipped` arg) and both posters
|
|
132
|
+
(`gh-comment.js` / `ado-comment.js`), and the `minions pr comment` CLI turns
|
|
133
|
+
`--skill-skipped-file` / `--skill-skipped-json` into the record. Two rules
|
|
134
|
+
attach to the skip (see `docs/completion-reports.md` → Review/Project skill
|
|
135
|
+
outcomes): the skip reason must be **validated against the skill's own
|
|
136
|
+
documented checklist** before it is recorded (a skip that contradicts an
|
|
137
|
+
explicit "Flag if…" / "Requirements" item the skill covers is invalid), and
|
|
138
|
+
the `{name, reason}` rendered in the comment must match the one recorded in the
|
|
139
|
+
report (no drift).
|
|
122
140
|
2. **Final agent note** — for a non-clean completion (failure / partial) the
|
|
123
141
|
single final agent report (`engine/lifecycle.js#writeNonCleanAgentReport`)
|
|
124
142
|
folds the grounded harness footprint in as the same `buildHarnessUsedSection`
|
|
@@ -37,7 +37,17 @@ Before spawning, `engine/live-checkout.js#prepareLiveCheckout` runs `git status
|
|
|
37
37
|
- Work item stamped with `_pendingReason: 'live_checkout_dirty'` so the dashboard surfaces the block.
|
|
38
38
|
- Completion summary: `live-checkout refused: N dirty file(s) in <localPath>`.
|
|
39
39
|
|
|
40
|
-
The engine never calls `git reset --hard`, `git clean -fd`,
|
|
40
|
+
The engine never calls `git reset --hard`, `git clean -fd`, or any other state-mutating command against the operator's checkout — not at spawn, not at cleanup, not on timeout, not on engine restart. The dispatch-scoped cleanup paths (`worktreePool.returnToPool`, `worktree-gc.gcDispatchWorktreeIfOrphan`, `_quarantineDirtyWorktree`) are naturally no-ops because `worktreePath` stays `null` end-to-end (`engine.js:1219`). The **periodic** worktree GC, however, is NOT `worktreePath`-gated — it derives its targets from `git worktree list --porcelain`, which for a live project returns the operator's *own* primary checkout. So `engine/cleanup.js#runPeriodicWorktreeSweep` now **filters out live-checkout projects entirely** (`shared.isLiveCheckoutProject`) before handing the list to the three pruners (PL-live-checkout-reliability-hardening), keeping the operator's real checkout out of the GC decision surface rather than relying only on the pruners' path-equality + ownership-marker gates. (The one **opt-in** exception is auto-stash — see [2b](#2b-opt-in-auto-stash-on-dirty-w-mqtvnnj1000357fa) — which the operator explicitly enables and which the engine never reverses on the operator's behalf.)
|
|
41
|
+
|
|
42
|
+
#### 2b. Opt-in auto-stash on dirty (W-mqtvnnj1000357fa)
|
|
43
|
+
|
|
44
|
+
By default the dirty refusal above stands. When the operator opts in, the engine instead **stashes** the dirty changes so the dispatch can proceed without manual intervention:
|
|
45
|
+
|
|
46
|
+
- **Enable** via per-project `project.liveCheckoutAutoStash: true` (takes priority) or the fleet-wide fallback `engine.liveCheckoutAutoStash: true` (default `false`). Resolution is `engine/live-checkout.js#resolveLiveCheckoutAutoStash` — an explicit per-project boolean wins; otherwise the engine value applies; otherwise `false`. Both are surfaced as Settings toggles (the per-project control is a tri-state *Use fleet default / On / Off* select; the fleet-wide one lives under **Settings → Worktrees**).
|
|
47
|
+
- When enabled and the tree is dirty, `spawnAgent` delegates the whole flow to `engine/live-checkout.js#applyLiveCheckoutAutoStash`, which calls `performLiveCheckoutAutoStash` to run `git stash push --include-untracked -m "minions-auto-stash-<dispatchId>-<timestamp>"` in `project.localPath` (`--include-untracked` so the `??` files that `git status --porcelain` counts as dirty are parked too). The stash git command runs **outside any file lock**.
|
|
48
|
+
- On stash **success** the helper re-runs `prepareLiveCheckout` (the tree is now clean), clears any stale `_pendingReason: 'live_checkout_dirty'` stamp (via the injected `clearDirtyStamp` callback), writes a `live-checkout-autostash-<wi-id>` inbox note with the stash name + manual-pop guidance, and returns `{ outcome:'stashed', liveResult }` so dispatch proceeds. If the re-preflight throws, it returns `{ outcome:'threw', error }` and `spawnAgent` fails the dispatch as `LIVE_CHECKOUT_FAILED`.
|
|
49
|
+
- On stash **failure** the error is surfaced (logged, never swallowed), the helper returns `{ outcome:'unchanged', liveResult }`, and the dispatch falls through to the normal retry-once-then-fail dirty path above.
|
|
50
|
+
- The engine **never pops the stash** automatically — that is the operator's choice. The stash name is logged and noted so the operator can `git stash pop` (or `git stash apply`) in `project.localPath` manually.
|
|
41
51
|
|
|
42
52
|
#### 2a. Thrown pre-spawn failures are retryable, NOT dirty (#305)
|
|
43
53
|
|
|
@@ -54,7 +64,7 @@ By default the dirty tree refusal (Guarantee 2) is terminal — the engine never
|
|
|
54
64
|
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
65
|
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
66
|
|
|
57
|
-
This is **DESTRUCTIVE** — it permanently discards the operator's uncommitted changes in the live checkout
|
|
67
|
+
This is **DESTRUCTIVE** — it permanently discards the operator's uncommitted changes in the live checkout. As of W-mqzbbhn2 it is **ON by default**: discarding disposable dirt via reset-to-remote keeps live-checkout branches aligned with origin instead of accumulating `minions: auto-save agent WIP` commits on every dirty exit. Set `liveCheckoutAutoReset: false` (per-project or fleet-wide) to restore the terminal dirty-tree refusal for live checkouts whose local drift you want preserved.
|
|
58
68
|
|
|
59
69
|
- **Fleet-wide:** Dashboard → Settings → `Live-checkout auto-reset (fleet-wide)` (`engine.liveCheckoutAutoReset`).
|
|
60
70
|
- **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.)*
|
|
@@ -82,6 +92,21 @@ Issue #226 only de-risked the *new-branch* path. The *existing-branch* `git chec
|
|
|
82
92
|
|
|
83
93
|
**Branch existence is checked against `refs/heads/<branch>` specifically** (not a bare `rev-parse --verify <branch>`, which DWIM-resolves a same-named tag or remote ref and would silently detach HEAD).
|
|
84
94
|
|
|
95
|
+
#### 3b. Worktree-conflict failures are non-retryable (W-mr28h2j2000y0de1)
|
|
96
|
+
|
|
97
|
+
A second deterministic existing-branch checkout failure mode is a **worktree conflict**. When the target branch is *also* checked out in a **second worktree** somewhere else — a leftover from a prior isolated-worktree dispatch, a manually-created worktree, or a stale worktree left behind by a `checkoutMode` change — a plain `git checkout <branch>` inside the operator checkout refuses with git's own literal wording:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
fatal: 'work/W-…' is already used by worktree at 'C:/office/worktrees/W-…'
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Like the blob-fetch case this is **structural, not transient** — it does *not* clear on retry, ever, until a human or the engine removes/reassigns the other worktree. Root-caused live against production incident W-mr25v4je000c70c5, where the WI retried 4 times across 3 agents over ~40 minutes, failing identically each time while the blocking worktree (which held real, un-pushed operator WIP matching the WI's own file scope) sat untouched.
|
|
104
|
+
|
|
105
|
+
- **Detection + typed result.** `prepareLiveCheckout` matches git's stable phrasing (`_isWorktreeConflictError`) and captures the conflicting worktree path (`_extractConflictingWorktreePath`), returning `{ ok:false, reason:'worktree-conflict', op, branch, conflictingWorktreePath, message, originalRef, originalRefType }` instead of throwing. The same no-half-switch HEAD restore (§3a) runs first, so the operator tree is left on its original ref.
|
|
106
|
+
- **Auto-resolve attempt before refusing.** Review feedback on the original PR (calebt_microsoft) was that classifying the conflict as non-retryable doesn't actually *resolve* it — the operator still has to clean up by hand every time. `spawnAgent` now calls `_tryAutoResolveLiveCheckoutWorktreeConflict` first: it only removes the conflicting worktree when **all** of (1) the path resolves inside this project's configured `worktreeRoot` (the standard location for engine-created worktrees), (2) it carries the engine ownership marker (`shared.hasWorktreeOwnerMarker` — never touches a worktree a human created by hand), and (3) `shared.removeWorktree` itself agrees to remove it (which independently refuses a worktree with a **live** dispatch running inside it via `isWorktreePathLive`, and refuses real-repo-root / non-linked-worktree paths). When the removal succeeds, `prepareLiveCheckout` is retried once and, on success, the dispatch proceeds normally — no failure, no alert, no operator action needed. This resolves the common case (a stale worktree orphaned by a crashed dispatch or a `checkoutMode` change) fully automatically.
|
|
107
|
+
- **Non-retryable classification (fallback).** When auto-resolve isn't safe (foreign/manual worktree, still-live dispatch, or removal failure) or the retried checkout still conflicts, `spawnAgent` completes the dispatch with the dedicated **`FAILURE_CLASS.LIVE_CHECKOUT_WORKTREE_CONFLICT`** (`'live-checkout-worktree-conflict'`; in `dispatch.js`'s `neverRetry` set) plus a `live-checkout-worktree-conflict-<wi-id>` inbox alert and a `_pendingReason: 'live_checkout_worktree_conflict'` stamp — instead of `LIVE_CHECKOUT_FAILED` (retryable) retry-storming to `maxRetries`.
|
|
108
|
+
- **Recovery.** The alert names the conflicting path and offers two options: (1) if that worktree is stale, `git worktree remove <path>` (or `--force` if dirty — with an explicit warning that force discards its uncommitted changes) to free the branch; (2) if it holds real WIP, finish/commit/push directly from that worktree instead of re-dispatching this WI in live mode, or rename/retarget the branch. The engine only ever suggests `git worktree remove` against the **other** worktree — it never mutates `project.localPath` beyond the best-effort `checkout <originalRef>` undo, and the auto-resolve path above never touches a worktree the engine didn't create or that still has an agent running inside it.
|
|
109
|
+
|
|
85
110
|
### 4. No worktree pool, no per-WI subdirectory isolation
|
|
86
111
|
|
|
87
112
|
Live mode shares one checkout per project. There is no pool to recycle, no quarantine directory, no per-WI subdirectory under the project root. The mutating-concurrency cap (Guarantee 1) is the only isolation mechanism: agents take turns in the same directory.
|
|
@@ -103,6 +128,8 @@ Live-mode agents run **in-place** in the operator's checkout, so when a dispatch
|
|
|
103
128
|
|
|
104
129
|
- **Original-ref capture.** `prepareLiveCheckout` records the operator's starting ref *before* the first checkout: `git symbolic-ref --short HEAD` → `{ originalRef:<branch>, originalRefType:'branch' }`, falling back to `git rev-parse HEAD` → `{ originalRef:<sha>, originalRefType:'detached' }`. `spawnAgent` persists `originalRef` / `originalRefType` onto the dispatch record via `mutateDispatch`, so the restore survives an engine restart, where the in-memory spawn closure is gone and only the persisted record remains.
|
|
105
130
|
- **Self-healing dirty recovery (PL-live-checkout-reliability-hardening).** Before the plain checkout, if the tree is **dirty AND HEAD is on the agent branch** (and that branch differs from `originalRef`), the dirt is provably **agent-authored** — the engine created that branch and verified the tree clean before switching to it. The leftovers are committed onto the **agent branch** (`git add -A` + `git commit --no-verify -m "minions: auto-save agent WIP (dispatch …)"`), so the plain `git checkout <originalRef>` then succeeds and the operator tree returns clean. This is the fix for the *"no recovery from dirty checkout"* deadlock, where an agent's crash-leftover WIP refused the restore, stranded the tree dirty on the agent branch, and then made **every future WI** for that project fail `LIVE_CHECKOUT_DIRTY` until a human cleaned it. It only ever mutates the engine-created branch, never the operator's branch, never discards (the WIP lands as a visible, revertable commit / on its PR), and gitignored artifacts are never staged (so a tree dirty only with ignored build output never reaches here). Best-effort: a failed auto-commit falls through to the manual-recovery alert below.
|
|
131
|
+
- **Retry-with-backoff on the WIP auto-save commit (#608).** The `git add -A` + `git commit` above (and the equivalent self-heal commit described below) go through `_commitAgentWipWithRetry`: a bounded retry (3 attempts, linear backoff) that fires only when the commit fails with an `index.lock` / "another git process" message — i.e. a transient collision with a concurrent git invocation — never on a genuine failure like "nothing to commit". Previously a single failed attempt (e.g. a `.git/index.lock` race) silently aborted the auto-save, leaving the tree dirty on the agent branch and letting the pollution below take hold.
|
|
132
|
+
- **Orphaned-agent-branch self-heal at dispatch start (#608).** If dispatch-end restore is ever skipped or interrupted (crash, forced kill, an engine restart racing the restore), the *next* dispatch for that project can start with HEAD already sitting on a stray `work/<wi-id>` branch from the previous run — and, before this fix, `prepareLiveCheckout`'s dirty-tree check ran *before* `originalRef` was even captured, so the function returned `{ reason: 'dirty' }` immediately and the branch was never noticed or cleaned, silently repeating for every subsequent dispatch. `prepareLiveCheckout` now recognizes this case: if the tree is still dirty after the existing auto-clean-artifacts recheck, and the current branch (read from the already-fetched `git status --porcelain -b` header, no extra git call) matches the `work/<id>` naming convention (`_looksLikeAgentBranch`) and differs from this dispatch's own target branch, the WIP is committed onto that stray branch (again via `_commitAgentWipWithRetry`) and HEAD is switched back to `mainRef` before the normal flow continues. This only ever touches a branch matching the engine's own naming convention — an operator's own feature branch (e.g. `feature/x`) is never mistaken for engine litter and is left untouched.
|
|
106
133
|
- **AUTO-RESTORE (best-effort, never `--force`).** At dispatch-end `restoreLiveCheckoutAtDispatchEnd` issues a **plain** `git checkout <originalRef>` — no `--force`, no `-B`, no reset, no clean, no stash. It no-ops when there is nothing to restore: no captured `originalRef`, the agent branch *is* the original ref, or HEAD already sits on the original ref (matched against the branch name *or* the raw sha so the detached-HEAD case is recognized). It is strictly best-effort: every error is swallowed and logged, and a restore never alters the dispatch result.
|
|
107
134
|
- **Fallback notify (only when a safe switch is impossible).** If git declines the plain checkout — most likely because the agent left uncommitted changes a checkout would overwrite — the refusal is **honored**: the tree is left exactly as the agent left it and a deduped `live-checkout-branch-<dispatchId>` inbox alert tells the operator how to switch back manually (`git -C <localPath> checkout <originalRef>`). The engine never forces the switch. An **unexpected** restore error (git missing, repo corruption, a GVFS blob fetch on the switch-back) now also writes this alert, so a non-refusal failure never silently strands the tree.
|
|
108
135
|
- **Terminal-failure alert.** When the dispatch ends in a non-success terminal state, a deduped `live-checkout-failed-<dispatchId>` inbox alert is written so the operator knows a live-mode run failed inside their own checkout (where any partial work is visible). This is independent of the restore and fires even when the restore itself succeeds.
|
|
@@ -137,6 +164,26 @@ The core invariant holds end-to-end through restore: **the engine only ever swit
|
|
|
137
164
|
|
|
138
165
|
Absent / `null` / `''` reads as `'worktree'` (the default) — explicit is preferred. A legacy `"worktreeMode": "live"` is still honored (and `"worktreeMode": "isolated"` reads as `'worktree'`), but new configs should use `checkoutMode`.
|
|
139
166
|
|
|
167
|
+
### Enabling auto-stash on dirty (config.json)
|
|
168
|
+
|
|
169
|
+
```jsonc
|
|
170
|
+
{
|
|
171
|
+
// Fleet-wide fallback (default false) — applies to every live project that
|
|
172
|
+
// doesn't set its own override.
|
|
173
|
+
"engine": { "liveCheckoutAutoStash": true },
|
|
174
|
+
"projects": [{
|
|
175
|
+
"name": "android-aosp",
|
|
176
|
+
"checkoutMode": "live",
|
|
177
|
+
// Per-project override wins over engine.liveCheckoutAutoStash. Omit the
|
|
178
|
+
// field to inherit the fleet setting; `false` forces fail-on-dirty even
|
|
179
|
+
// when the fleet setting is true.
|
|
180
|
+
"liveCheckoutAutoStash": true
|
|
181
|
+
}]
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
With auto-stash on, a dirty tree is `git stash push --include-untracked`'d before dispatch instead of refusing (see [2b](#2b-opt-in-auto-stash-on-dirty-w-mqtvnnj1000357fa)). The engine never pops the stash — recover with `git stash pop` manually.
|
|
186
|
+
|
|
140
187
|
### Recovering from `live_checkout_dirty` refusal
|
|
141
188
|
|
|
142
189
|
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:
|
|
@@ -178,7 +225,7 @@ The engine has no opinion about local branches; this hygiene is the operator's r
|
|
|
178
225
|
Live-checkout mode is deliberately small. These are NOT supported and will not be added:
|
|
179
226
|
|
|
180
227
|
- **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.
|
|
181
|
-
- **No auto-stash on dirty refusal.**
|
|
228
|
+
- **No auto-stash on dirty refusal _by default_.** Out of the box the engine refuses and exits; it never `git stash`es to "make room" for a dispatch, because that silently mutates the operator's tree and conflates engine state with operator state. The operator can **opt in** per-project (`project.liveCheckoutAutoStash`) or fleet-wide (`engine.liveCheckoutAutoStash`) to auto-stash instead of refusing — see [2b](#2b-opt-in-auto-stash-on-dirty-w-mqtvnnj1000357fa). Even then the engine never **pops** the stash on the operator's behalf. (The `liveCheckoutAutoReset` escape hatch — ON by default as of W-mqzbbhn2 — **discards** rather than stashes; set it to `false` per-project / fleet-wide to keep the terminal dirty-tree refusal. See [§2b](#2b-opt-in-auto-reset-on-dirty-livecheckoutautoreset-w-mqvejug6000eeb20).)
|
|
182
229
|
- **No concurrent dispatches per project.** The cap is 1; raising it would require per-WI subdirectories, which live mode explicitly does not provide.
|
|
183
230
|
- **No per-WI subdirectory isolation.** Live mode is one-checkout-per-project by design. If you need isolation, use `checkoutMode: 'worktree'` (the default).
|
|
184
231
|
- **No per-WI override.** `checkoutMode` is per-project only. There is no `meta.checkoutMode` on a work item that overrides the project setting.
|
|
@@ -190,12 +237,13 @@ Live-checkout mode is deliberately small. These are NOT supported and will not b
|
|
|
190
237
|
| File | Purpose |
|
|
191
238
|
|---|---|
|
|
192
239
|
| `engine/shared.js` — `CHECKOUT_MODES`, `validateCheckoutMode`, `resolveCheckoutMode`, `isLiveCheckoutProject` | Enum + validator + back-compat resolver (P-a3f9b201; consolidated W-mqiaw974). |
|
|
193
|
-
| `engine/shared.js` — `resolveLiveCheckoutAutoReset` + `ENGINE_DEFAULTS.liveCheckoutAutoReset` | Pure precedence resolver (per-project boolean > fleet-wide engine default > false) + the fleet-wide default (
|
|
240
|
+
| `engine/shared.js` — `resolveLiveCheckoutAutoReset` + `ENGINE_DEFAULTS.liveCheckoutAutoReset` | Pure precedence resolver (per-project boolean > fleet-wide engine default > false) + the fleet-wide default (ON as of W-mqzbbhn2). Gates the dirty-tree auto-reset in `prepareLiveCheckout` (W-mqvejug6000eeb20). |
|
|
194
241
|
| `engine/shared.js` — `resolveSpawnPaths` | Returns `{ cwd: localPath, worktreeRootDir: null, liveMode: true }` for live projects (P-a3f9b202). |
|
|
195
|
-
| `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). |
|
|
242
|
+
| `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`/`worktree-conflict` classification** for partial-clone hydration failures and cross-worktree branch conflicts, **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). |
|
|
196
243
|
| `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). |
|
|
244
|
+
| `engine/live-checkout.js` — `resolveLiveCheckoutAutoStash`, `performLiveCheckoutAutoStash`, `applyLiveCheckoutAutoStash` | Opt-in auto-stash: resolver (per-project boolean wins, else `engine.liveCheckoutAutoStash`, else false) + `git stash push --include-untracked` runner returning `{ ok, stashMessage, error? }` (never throws on git failure) + the `applyLiveCheckoutAutoStash` orchestrator (stash → re-preflight → clear retry stamp → operator inbox note) returning a `{ outcome:'unchanged'\|'stashed'\|'threw' }` discriminant. Engine never auto-pops (W-mqtvnnj1000357fa). |
|
|
197
245
|
| `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). |
|
|
198
|
-
| `engine.js` — `spawnAgent` live-mode block | Calls `prepareLiveCheckout`, handles dirty / throw branches, gates `git worktree add` on `!liveMode` (P-a3f9b204). |
|
|
246
|
+
| `engine.js` — `spawnAgent` live-mode block | Calls `prepareLiveCheckout`, delegates opt-in auto-stash to `applyLiveCheckoutAutoStash` on a dirty tree (W-mqtvnnj1000357fa), handles dirty / throw branches, gates `git worktree add` on `!liveMode` (P-a3f9b204). |
|
|
199
247
|
| `engine.js` — `spawnAgent` mid-op / detached-HEAD refusal block | Emits `LIVE_CHECKOUT_MID_OPERATION`, writes `live-checkout-blocked-<wi-id>` alert, stamps `_pendingReason: 'live_checkout_mid_operation'` / `'live_checkout_detached_head'` (P-c5a1f3b8). |
|
|
200
248
|
| `engine.js` — `spawnAgent` originalRef persistence | Persists `originalRef` / `originalRefType` onto the dispatch record via `mutateDispatch` so restore survives an engine restart (P-c5a1f3b8). |
|
|
201
249
|
| `engine.js` — `onAgentClose` live-mode restore wiring | Calls `restoreLiveCheckoutAtDispatchEnd` on every terminal result (P-d9e6b2c4). |
|
|
@@ -206,10 +254,11 @@ Live-checkout mode is deliberately small. These are NOT supported and will not b
|
|
|
206
254
|
| `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). |
|
|
207
255
|
| `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). |
|
|
208
256
|
| `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). |
|
|
209
|
-
| `test/unit/{resolve-spawn-paths-live-mode,prepare-live-checkout,spawn-agent-live-mode-wiring}.test.js` | Wiring and contract tests (P-a3f9b208). |
|
|
257
|
+
| `test/unit/{resolve-spawn-paths-live-mode,prepare-live-checkout,spawn-agent-live-mode-wiring,live-checkout-auto-stash}.test.js` | Wiring and contract tests (P-a3f9b208; auto-stash helpers W-mqtvnnj1000357fa). |
|
|
210
258
|
| `engine/shared.js` — `FAILURE_CLASS.LIVE_CHECKOUT_DIRTY` | Non-retryable refusal class. |
|
|
211
259
|
| `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). |
|
|
212
260
|
| `engine/shared.js` — `FAILURE_CLASS.LIVE_CHECKOUT_BLOB_FETCH` | Non-retryable refusal class for an existing-branch checkout that could not hydrate the tree on a blobless GVFS partial clone (auth-less cache fetch, headless) — deterministic, so excluded from mechanical retry (PL-live-checkout-reliability-hardening). |
|
|
261
|
+
| `engine/shared.js` — `FAILURE_CLASS.LIVE_CHECKOUT_WORKTREE_CONFLICT` | Non-retryable refusal class for an existing-branch checkout that refused because the branch is already checked out in another worktree — structural, so excluded from mechanical retry (W-mr28h2j2000y0de1). |
|
|
213
262
|
| `engine.js` — `_liveCheckoutDirtyAttempts` counter | Dedicated two-strike dirty memory (survives the discovery + retry `_pendingReason` scrubs that defeated #434); first dirty failure retries once, second fails non-retryably (PL-live-checkout-reliability-hardening). |
|
|
214
|
-
| `engine/dispatch.js` — `isRetryableFailureReason` neverRetry | Excludes `LIVE_CHECKOUT_DIRTY`, `LIVE_CHECKOUT_MID_OPERATION`, and `
|
|
263
|
+
| `engine/dispatch.js` — `isRetryableFailureReason` neverRetry | Excludes `LIVE_CHECKOUT_DIRTY`, `LIVE_CHECKOUT_MID_OPERATION`, `LIVE_CHECKOUT_BLOB_FETCH`, and `LIVE_CHECKOUT_WORKTREE_CONFLICT` from mechanical retry. |
|
|
215
264
|
| `engine/timeout.js` header comment | Confirms no special live-mode kill handling. |
|
package/engine/ado-comment.js
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* changes) stay `active` so they block/notify the author.
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
|
-
const { buildMinionsCommentBody } = require('./comment-format');
|
|
37
|
+
const { buildMinionsCommentBody, buildSkippedSkillSection } = require('./comment-format');
|
|
38
38
|
const { acquireAdoToken } = require('./ado-token');
|
|
39
39
|
|
|
40
40
|
const ADO_API_VERSION = '7.1';
|
|
@@ -108,6 +108,7 @@ async function _defaultAcquireToken() {
|
|
|
108
108
|
* @param {string} args.kind comment kind (marker)
|
|
109
109
|
* @param {string} [args.workItemId] originating work-item id (marker)
|
|
110
110
|
* @param {object} [args.harnessUsed] grounded harnessUsed record (folded into body)
|
|
111
|
+
* @param {object} [args.skillSkipped] skipped-project-skill record `{name, reason}` (folded into body, visible)
|
|
111
112
|
* @param {number} [args.timeoutMs=30000]
|
|
112
113
|
* @param {Function} [args.acquireToken] () => Promise<string> — injectable token source
|
|
113
114
|
* @param {Function} [args.fetchImpl=fetch] injectable fetch
|
|
@@ -123,6 +124,7 @@ async function postAdoPrComment({
|
|
|
123
124
|
kind,
|
|
124
125
|
workItemId,
|
|
125
126
|
harnessUsed,
|
|
127
|
+
skillSkipped,
|
|
126
128
|
resolved = false,
|
|
127
129
|
timeoutMs = 30000,
|
|
128
130
|
acquireToken = _defaultAcquireToken,
|
|
@@ -134,7 +136,7 @@ async function postAdoPrComment({
|
|
|
134
136
|
_validatePrNumber(prNumber);
|
|
135
137
|
|
|
136
138
|
// buildMinionsCommentBody validates marker fields and throws on bad input.
|
|
137
|
-
const finalBody = buildMinionsCommentBody({ agentId, kind, workItemId, body, harnessUsed });
|
|
139
|
+
const finalBody = buildMinionsCommentBody({ agentId, kind, workItemId, body, harnessUsed, skillSkipped });
|
|
138
140
|
|
|
139
141
|
const token = await acquireToken();
|
|
140
142
|
if (!token || typeof token !== 'string') {
|
|
@@ -184,6 +186,7 @@ module.exports = {
|
|
|
184
186
|
// Re-export the neutral builder so ADO callers have a single import surface,
|
|
185
187
|
// mirroring engine/gh-comment.js.
|
|
186
188
|
buildMinionsCommentBody,
|
|
189
|
+
buildSkippedSkillSection,
|
|
187
190
|
// Internal validators exported for tests.
|
|
188
191
|
_validateOrgBase,
|
|
189
192
|
_validateProject,
|