@yemi33/minions 0.1.2147 → 0.1.2149
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 +8 -0
- package/dashboard/js/settings.js +9 -0
- package/dashboard/slim/body.html +2 -1
- package/dashboard/slim/js/command-send.js +3 -2
- package/dashboard/slim/js/history.js +9 -2
- package/dashboard/slim/styles.css +88 -10
- package/dashboard.js +5 -0
- package/docs/README.md +1 -0
- package/docs/completion-reports.md +44 -12
- package/docs/project-skills.md +133 -0
- package/engine/ado.js +19 -0
- package/engine/discover-project-skills.js +490 -0
- package/engine/discover-review-skills.js +15 -269
- package/engine/dispatch.js +2 -0
- package/engine/github.js +14 -0
- package/engine/lifecycle.js +17 -0
- package/engine/playbook-intents.js +76 -0
- package/engine/playbook.js +59 -20
- package/engine/shared.js +288 -0
- package/engine/worktree-gc.js +241 -21
- package/engine.js +294 -32
- package/package.json +1 -1
- package/playbooks/fix.md +4 -0
- package/playbooks/implement-shared.md +4 -0
- package/playbooks/implement.md +4 -0
- package/playbooks/plan-to-prd.md +4 -0
- package/playbooks/plan.md +4 -0
- package/playbooks/review.md +3 -3
- package/bin/minions.js.rej +0 -16
- package/dashboard/slim/body.html.rej +0 -11
- package/dashboard/slim/js/command-send.js.rej +0 -12
- package/dashboard/slim/js/history.js.rej +0 -26
- package/dashboard/slim/styles.css.rej +0 -124
- package/docs/README.md.rej +0 -9
- package/docs/onboarding.md.rej +0 -10
package/engine.js
CHANGED
|
@@ -918,6 +918,109 @@ async function pruneStaleWorktreeForBranch(rootDir, branchName, gitOpts) {
|
|
|
918
918
|
return removed;
|
|
919
919
|
}
|
|
920
920
|
|
|
921
|
+
// W-mq5n1zx5 — Layer 1b: status-probe command builder. Honors the
|
|
922
|
+
// ENGINE_DEFAULTS.statusProbeUseNoOptionalLocks toggle so operators can
|
|
923
|
+
// disable the flag if a future git release breaks something. Default ON:
|
|
924
|
+
// `--no-optional-locks` tells git to skip the .git/index.lock acquire
|
|
925
|
+
// around the untracked-cache refresh that runs inside `status`. Without
|
|
926
|
+
// the flag, a 6–12s probe under aggressive AV scanning is normal; with
|
|
927
|
+
// it, typical probes drop to <500ms, which removes the timeout that
|
|
928
|
+
// leaks the git.exe descendant that later pins packfile handles and
|
|
929
|
+
// breaks the quarantine rename.
|
|
930
|
+
function _statusPorcelainCmd() {
|
|
931
|
+
return ENGINE_DEFAULTS.statusProbeUseNoOptionalLocks
|
|
932
|
+
? 'git --no-optional-locks status --porcelain'
|
|
933
|
+
: 'git status --porcelain';
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// W-mq5n1zx5 — Layer 1a: rename worktree dir with jittered backoff. The
|
|
937
|
+
// raw `fs.renameSync` used to throw Windows EBUSY/EPERM/EACCES if a
|
|
938
|
+
// lingering `git.exe` descendant (typically leaked by a status-probe
|
|
939
|
+
// timeout) still held a packfile handle. We retry with capped exponential
|
|
940
|
+
// backoff + random jitter so the descendant has time to exit on its own.
|
|
941
|
+
// Worst-case wall time ≈ baseMs * (2^attempts) ≈ 16s when attempts=6,
|
|
942
|
+
// baseMs=250 — small enough not to wedge a tick, large enough to clear
|
|
943
|
+
// the typical race. Throws the LAST error on exhaustion so the caller
|
|
944
|
+
// can decide whether to fall back to `git worktree remove --force`.
|
|
945
|
+
async function _renameWithRetry(src, dst, opts = {}) {
|
|
946
|
+
const attempts = Number(opts.attempts) > 0
|
|
947
|
+
? Number(opts.attempts)
|
|
948
|
+
: ENGINE_DEFAULTS.quarantineRenameRetryAttempts;
|
|
949
|
+
const baseMs = Number(opts.baseMs) > 0
|
|
950
|
+
? Number(opts.baseMs)
|
|
951
|
+
: ENGINE_DEFAULTS.quarantineRenameRetryBaseMs;
|
|
952
|
+
let lastErr;
|
|
953
|
+
for (let i = 0; i < attempts; i++) {
|
|
954
|
+
try { fs.renameSync(src, dst); return { attempts: i + 1 }; }
|
|
955
|
+
catch (e) {
|
|
956
|
+
if (!['EBUSY', 'EPERM', 'EACCES', 'ENOTEMPTY'].includes(e.code)) throw e;
|
|
957
|
+
lastErr = e;
|
|
958
|
+
if (i < attempts - 1) {
|
|
959
|
+
const delay = baseMs * (2 ** i) + Math.random() * 200;
|
|
960
|
+
await new Promise(r => setTimeout(r, delay));
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
throw lastErr;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// W-mq5n1zx5 — Layer 2a: on Windows, kill any live `git.exe` descendants
|
|
968
|
+
// whose command line points at the quarantine target path. We never
|
|
969
|
+
// tracked the PID of the `git status --porcelain` child that the probe
|
|
970
|
+
// timed out on, so we can't kill by PID — instead we shell out to
|
|
971
|
+
// PowerShell's CIM cmdlets to find matching processes and Stop-Process
|
|
972
|
+
// them. Cheap (<2s) and idempotent — if no descendants are alive, the
|
|
973
|
+
// CIM query returns nothing and exits 0. POSIX is a no-op (the EBUSY
|
|
974
|
+
// race is Windows-specific). Best-effort; failure is logged but never
|
|
975
|
+
// blocks the quarantine.
|
|
976
|
+
function _killGitDescendantsForWorktree(worktreePath) {
|
|
977
|
+
if (process.platform !== 'win32') return { killed: 0, skipped: true };
|
|
978
|
+
if (!ENGINE_DEFAULTS.statusProbeKillDescendantsWin32) return { killed: 0, skipped: true };
|
|
979
|
+
if (!worktreePath) return { killed: 0, skipped: true };
|
|
980
|
+
// PowerShell expects single-quoted literals; escape any embedded single
|
|
981
|
+
// quote by doubling it (PowerShell's standard single-quote escape).
|
|
982
|
+
const safePath = String(worktreePath).replace(/'/g, "''");
|
|
983
|
+
const ps = [
|
|
984
|
+
"$ErrorActionPreference='SilentlyContinue';",
|
|
985
|
+
`$matches = Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'git.exe' -and $_.CommandLine -like '*${safePath}*' };`,
|
|
986
|
+
"if ($matches) { $matches | ForEach-Object { Stop-Process -Id $_.ProcessId -Force; $_.ProcessId }; }",
|
|
987
|
+
].join(' ');
|
|
988
|
+
try {
|
|
989
|
+
const out = shared.execSilent(`powershell -NoProfile -Command "${ps.replace(/"/g, '\\"')}"`, { timeout: 2000, encoding: 'utf8' });
|
|
990
|
+
const killed = String(out || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean).length;
|
|
991
|
+
if (killed > 0) log('info', `_killGitDescendantsForWorktree: killed ${killed} git.exe descendant(s) holding ${worktreePath}`);
|
|
992
|
+
return { killed };
|
|
993
|
+
} catch (e) {
|
|
994
|
+
log('warn', `_killGitDescendantsForWorktree: powershell probe failed: ${e.message}`);
|
|
995
|
+
return { killed: 0, error: e.message };
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// W-mq5n1zx5 — Layer 3a: bump the rolling counters for quarantine rename
|
|
1000
|
+
// outcomes (attempts, success, successAfterRetry, fallbackForceRemove,
|
|
1001
|
+
// totalFailure). Best-effort; metrics failures must not break the
|
|
1002
|
+
// quarantine path. The shape lives under metrics._engine.worktreeQuarantineOutcomes
|
|
1003
|
+
// so it sits beside the other _engine.* engine-internal telemetry.
|
|
1004
|
+
function _bumpQuarantineOutcome(key, delta = 1) {
|
|
1005
|
+
try {
|
|
1006
|
+
shared.mutateMetrics((metrics) => {
|
|
1007
|
+
if (!metrics._engine) metrics._engine = {};
|
|
1008
|
+
if (!metrics._engine.worktreeQuarantineOutcomes) {
|
|
1009
|
+
metrics._engine.worktreeQuarantineOutcomes = {
|
|
1010
|
+
attempts: 0, success: 0, successAfterRetry: 0,
|
|
1011
|
+
fallbackForceRemove: 0, totalFailure: 0,
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
const o = metrics._engine.worktreeQuarantineOutcomes;
|
|
1015
|
+
o[key] = (o[key] || 0) + delta;
|
|
1016
|
+
return metrics;
|
|
1017
|
+
});
|
|
1018
|
+
} catch (e) {
|
|
1019
|
+
log('warn', `_bumpQuarantineOutcome(${key}): ${e.message}`);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
|
|
921
1024
|
// ─── assertCleanSharedWorktree (#2439) ──────────────────────────────────────
|
|
922
1025
|
// Engine-side preflight that prevents shared-branch (and PR-targeted reused —
|
|
923
1026
|
// see opts.quarantineOnUnsafe, issue #2996) dispatches from spawning into a
|
|
@@ -980,7 +1083,7 @@ async function assertCleanSharedWorktree(rootDir, worktreePath, branchName, disp
|
|
|
980
1083
|
// 1. Status probe (filesystem)
|
|
981
1084
|
let statusOut = '';
|
|
982
1085
|
try {
|
|
983
|
-
const r = await execAsync(
|
|
1086
|
+
const r = await execAsync(_statusPorcelainCmd(), { ...gitOpts, cwd: worktreePath, timeout: statusTimeoutMs });
|
|
984
1087
|
statusOut = (r || '').toString().trim();
|
|
985
1088
|
} catch (e) {
|
|
986
1089
|
// W-mq1habhf: previously this bailed out with the bad worktree intact,
|
|
@@ -1186,7 +1289,7 @@ async function assertCleanSharedWorktree(rootDir, worktreePath, branchName, disp
|
|
|
1186
1289
|
|
|
1187
1290
|
// 7. Re-verify
|
|
1188
1291
|
try {
|
|
1189
|
-
const r2 = await execAsync(
|
|
1292
|
+
const r2 = await execAsync(_statusPorcelainCmd(), { ...gitOpts, cwd: worktreePath, timeout: statusTimeoutMs });
|
|
1190
1293
|
const after = (r2 || '').toString().trim();
|
|
1191
1294
|
if (after) {
|
|
1192
1295
|
result.reason = 'dirty-after-clean';
|
|
@@ -1218,9 +1321,23 @@ async function assertCleanSharedWorktree(rootDir, worktreePath, branchName, disp
|
|
|
1218
1321
|
// branch (assertCleanSharedWorktree returns 'other-dispatch-active' before
|
|
1219
1322
|
// invoking this). Renaming an active worktree out from under a running
|
|
1220
1323
|
// agent would be catastrophic.
|
|
1324
|
+
//
|
|
1325
|
+
// W-mq5n1zx5 — layered defense against Windows EBUSY: (Layer 2a) kill
|
|
1326
|
+
// lingering git.exe descendants whose command-line points at the worktree
|
|
1327
|
+
// before touching the path; (Layer 1a) retry the rename with jittered
|
|
1328
|
+
// backoff; (Layer 2b) if every retry still fails, fall back to
|
|
1329
|
+
// `git worktree remove --force` so git tears down its own metadata + dir
|
|
1330
|
+
// rather than leaving a half-quarantined tree behind; (Layer 3a) emit
|
|
1331
|
+
// outcome counters to metrics._engine.worktreeQuarantineOutcomes;
|
|
1332
|
+
// (Layer 3b) when EVERY path fails, write a dedicated inbox alert naming
|
|
1333
|
+
// the manual recovery command. The throw-or-return contract is unchanged
|
|
1334
|
+
// for callers: a thrown error means the caller's `result.quarantineError`
|
|
1335
|
+
// field is populated and `result.quarantined` stays false (env-blocked
|
|
1336
|
+
// path); a normal return means quarantine succeeded.
|
|
1221
1337
|
async function _quarantineDirtyWorktree(rootDir, worktreePath, branchName, gitOpts, diag = {}) {
|
|
1222
1338
|
const ts = Date.now();
|
|
1223
1339
|
const quarantinedPath = `${worktreePath}-quarantine-${ts}`;
|
|
1340
|
+
_bumpQuarantineOutcome('attempts', 1);
|
|
1224
1341
|
|
|
1225
1342
|
// Capture HEAD sha BEFORE renaming so we can back up the local branch ref.
|
|
1226
1343
|
let headSha = '';
|
|
@@ -1231,24 +1348,112 @@ async function _quarantineDirtyWorktree(rootDir, worktreePath, branchName, gitOp
|
|
|
1231
1348
|
log('warn', `_quarantineDirtyWorktree: rev-parse HEAD failed for ${worktreePath}: ${e.message} — backup ref will be skipped`);
|
|
1232
1349
|
}
|
|
1233
1350
|
|
|
1234
|
-
// Rename the worktree dir. Once this succeeds the worktree is functionally
|
|
1235
|
-
// quarantined; subsequent failures only affect ref bookkeeping.
|
|
1236
1351
|
// W-mq5rwwss000f30a7 — never quarantine (=rename out from under) a worktree
|
|
1237
1352
|
// that a live dispatch still claims. Quarantine breaks the agent's cwd just
|
|
1238
|
-
// as completely as removeWorktree would.
|
|
1353
|
+
// as completely as removeWorktree would. Check before any destructive work
|
|
1354
|
+
// (including the kill-descendants sweep below) so we don't disturb a live
|
|
1355
|
+
// agent's git child processes either.
|
|
1239
1356
|
if (shared.isWorktreePathLive(worktreePath)) {
|
|
1240
1357
|
log('warn', `_quarantineDirtyWorktree: skip — live dispatch in ${worktreePath}`);
|
|
1241
1358
|
shared._writeWorktreeSkipLiveInboxNote(worktreePath, '_quarantineDirtyWorktree');
|
|
1242
1359
|
return { quarantinedPath: null, backupRef: null, skipped: true };
|
|
1243
1360
|
}
|
|
1244
|
-
fs.renameSync(worktreePath, quarantinedPath);
|
|
1245
1361
|
|
|
1246
|
-
//
|
|
1247
|
-
//
|
|
1362
|
+
// W-mq5n1zx5 Layer 2a: pre-emptively kill any git.exe descendants whose
|
|
1363
|
+
// command line points at the worktree. The status-probe child that timed
|
|
1364
|
+
// out earlier may still be alive holding packfile handles; if so, the
|
|
1365
|
+
// rename below will fail with EBUSY/EPERM until the descendant exits.
|
|
1366
|
+
// POSIX is a no-op. Best-effort; failure here is non-fatal.
|
|
1367
|
+
_killGitDescendantsForWorktree(worktreePath);
|
|
1368
|
+
|
|
1369
|
+
// W-mq5n1zx5 Layer 1a: rename with jittered backoff. Replaces the bare
|
|
1370
|
+
// `fs.renameSync(worktreePath, quarantinedPath)` that used to throw
|
|
1371
|
+
// Windows EBUSY uncatchably and burn the WI's auto-recovery budget.
|
|
1372
|
+
let renameAttempts = 0;
|
|
1373
|
+
let renameError = null;
|
|
1248
1374
|
try {
|
|
1249
|
-
|
|
1375
|
+
const r = await _renameWithRetry(worktreePath, quarantinedPath);
|
|
1376
|
+
renameAttempts = r.attempts || 1;
|
|
1250
1377
|
} catch (e) {
|
|
1251
|
-
|
|
1378
|
+
renameError = e;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// W-mq5n1zx5 Layer 2b: if the rename retries exhausted, fall back to
|
|
1382
|
+
// `git worktree remove --force`. This DESTROYS the worktree contents
|
|
1383
|
+
// (no forensics dir), so it's last-resort only — gated on the
|
|
1384
|
+
// quarantineForceRemoveFallback engine default. The next dispatch will
|
|
1385
|
+
// create a fresh worktree on the origin tip, which is the same outcome
|
|
1386
|
+
// the rename path would produce, just without the recovery dir.
|
|
1387
|
+
let forceRemoved = false;
|
|
1388
|
+
let forceRemoveError = null;
|
|
1389
|
+
if (renameError && ENGINE_DEFAULTS.quarantineForceRemoveFallback) {
|
|
1390
|
+
log('warn', `_quarantineDirtyWorktree: rename failed after ${renameAttempts || ENGINE_DEFAULTS.quarantineRenameRetryAttempts} attempt(s) (${renameError.code || renameError.message}); falling back to git worktree remove --force`);
|
|
1391
|
+
try {
|
|
1392
|
+
await shared.shellSafeGit(['worktree', 'remove', '--force', worktreePath], { ...gitOpts, cwd: rootDir, timeout: 30000 });
|
|
1393
|
+
forceRemoved = true;
|
|
1394
|
+
log('warn', `_quarantineDirtyWorktree: git worktree remove --force succeeded for ${worktreePath} (no quarantine dir preserved)`);
|
|
1395
|
+
} catch (e) {
|
|
1396
|
+
forceRemoveError = e;
|
|
1397
|
+
log('error', `_quarantineDirtyWorktree: git worktree remove --force ALSO failed: ${e.message}`);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// If neither the rename retries nor the force-remove fallback succeeded,
|
|
1402
|
+
// we are env-blocked: the worktree dir is still on disk and git still
|
|
1403
|
+
// thinks it owns it. Write a dedicated inbox alert (Layer 3b) and rethrow
|
|
1404
|
+
// the original rename error so the caller's quarantineError branch fires.
|
|
1405
|
+
if (renameError && !forceRemoved) {
|
|
1406
|
+
_bumpQuarantineOutcome('totalFailure', 1);
|
|
1407
|
+
const sanitizedRefSegmentEnv = sanitizeBranch(branchName).replace(/\//g, '-');
|
|
1408
|
+
const failBody = [
|
|
1409
|
+
'# Engine quarantine TOTAL FAILURE — manual recovery required (W-mq5n1zx5)',
|
|
1410
|
+
'',
|
|
1411
|
+
`- Dispatch: ${diag.dispatchId || '<unknown>'}`,
|
|
1412
|
+
`- Branch: ${branchName}`,
|
|
1413
|
+
`- Worktree path (STILL PRESENT): ${worktreePath}`,
|
|
1414
|
+
`- Reason: ${diag.reason || 'unsafe'}`,
|
|
1415
|
+
`- Rename attempts: ${renameAttempts || ENGINE_DEFAULTS.quarantineRenameRetryAttempts}`,
|
|
1416
|
+
`- Last rename error: ${renameError.code || ''} ${renameError.message}`,
|
|
1417
|
+
`- Force-remove fallback: ${ENGINE_DEFAULTS.quarantineForceRemoveFallback ? `tried, failed (${forceRemoveError ? forceRemoveError.message : 'unknown'})` : 'disabled'}`,
|
|
1418
|
+
'',
|
|
1419
|
+
'## Manual recovery',
|
|
1420
|
+
'',
|
|
1421
|
+
'1. Find any live `git.exe` (or other) processes still holding the dir:',
|
|
1422
|
+
` \`Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like '*${worktreePath}*' }\``,
|
|
1423
|
+
'2. Stop them: `Stop-Process -Id <pid> -Force`',
|
|
1424
|
+
'3. Force-remove the dir:',
|
|
1425
|
+
` Windows: \`Remove-Item -Recurse -Force "${worktreePath}"\` (or \`rmdir /s /q "${worktreePath}"\` from cmd.exe)`,
|
|
1426
|
+
` POSIX: \`rm -rf "${worktreePath}"\``,
|
|
1427
|
+
`4. Prune git's stale worktree metadata: \`git -C "${rootDir}" worktree prune\``,
|
|
1428
|
+
'',
|
|
1429
|
+
'After this, the next dispatch for the branch will create a fresh worktree.',
|
|
1430
|
+
].join('\n');
|
|
1431
|
+
try {
|
|
1432
|
+
shared.writeToInbox('engine', `worktree-quarantine-failed-${diag.dispatchId || sanitizedRefSegmentEnv}`, failBody);
|
|
1433
|
+
} catch (e) {
|
|
1434
|
+
log('warn', `_quarantineDirtyWorktree: totalFailure writeToInbox failed: ${e.message}`);
|
|
1435
|
+
}
|
|
1436
|
+
throw renameError;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// Track outcome bucket for the successful path (rename or force-remove).
|
|
1440
|
+
if (forceRemoved) {
|
|
1441
|
+
_bumpQuarantineOutcome('fallbackForceRemove', 1);
|
|
1442
|
+
} else if (renameAttempts > 1) {
|
|
1443
|
+
_bumpQuarantineOutcome('successAfterRetry', 1);
|
|
1444
|
+
}
|
|
1445
|
+
_bumpQuarantineOutcome('success', 1);
|
|
1446
|
+
|
|
1447
|
+
// Prune git's stale worktree metadata so the next `git worktree add` for
|
|
1448
|
+
// the same branch isn't blocked by "branch is already used by worktree".
|
|
1449
|
+
// Skipped on the force-removed path — `git worktree remove --force` already
|
|
1450
|
+
// wipes its own metadata entry.
|
|
1451
|
+
if (!forceRemoved) {
|
|
1452
|
+
try {
|
|
1453
|
+
await shared.shellSafeGit(['worktree', 'prune'], { ...gitOpts, cwd: rootDir, timeout: 15000 });
|
|
1454
|
+
} catch (e) {
|
|
1455
|
+
log('warn', `_quarantineDirtyWorktree: worktree prune after rename: ${e.message}`);
|
|
1456
|
+
}
|
|
1252
1457
|
}
|
|
1253
1458
|
|
|
1254
1459
|
// Backup the local branch HEAD to refs/minions/quarantine/<sanitized>/<ts>
|
|
@@ -1293,7 +1498,9 @@ async function _quarantineDirtyWorktree(rootDir, worktreePath, branchName, gitOp
|
|
|
1293
1498
|
`- Dispatch: ${diag.dispatchId || '<unknown>'}`,
|
|
1294
1499
|
`- Branch: ${branchName}`,
|
|
1295
1500
|
`- Original worktree: ${worktreePath}`,
|
|
1296
|
-
|
|
1501
|
+
forceRemoved
|
|
1502
|
+
? `- Force-removed via \`git worktree remove --force\` (no quarantine dir preserved — W-mq5n1zx5 Layer 2b)`
|
|
1503
|
+
: `- Quarantined to: ${quarantinedPath}${renameAttempts > 1 ? ` (rename took ${renameAttempts} attempt(s) — W-mq5n1zx5 Layer 1a)` : ''}`,
|
|
1297
1504
|
`- Reason: ${diag.reason || 'unsafe'}`,
|
|
1298
1505
|
`- Local commits ahead of origin: ${diag.ahead || 0}`,
|
|
1299
1506
|
`- Local commits behind origin: ${diag.behind || 0}`,
|
|
@@ -1305,12 +1512,20 @@ async function _quarantineDirtyWorktree(rootDir, worktreePath, branchName, gitOp
|
|
|
1305
1512
|
'',
|
|
1306
1513
|
backupRefCreated
|
|
1307
1514
|
? `The local branch HEAD was backed up to \`${backupRef}\` (${headSha.slice(0, 12)}) and \`refs/heads/${branchName}\` was reset to \`refs/remotes/origin/${branchName}\`.`
|
|
1308
|
-
: `Backup ref was NOT created (HEAD sha unavailable). The local branch ref was still reset to \`refs/remotes/origin/${branchName}\` if possible. Inspect the quarantined directory directly to recover unpushed work
|
|
1515
|
+
: `Backup ref was NOT created (HEAD sha unavailable). The local branch ref was still reset to \`refs/remotes/origin/${branchName}\` if possible.${forceRemoved ? ' Worktree contents were destroyed by the force-remove fallback — no quarantine dir to inspect.' : ' Inspect the quarantined directory directly to recover unpushed work.'}`,
|
|
1309
1516
|
'',
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1517
|
+
forceRemoved
|
|
1518
|
+
? `1. The worktree dir was force-removed; there is no quarantine dir to inspect. If the local branch ref still has divergent commits, recover from \`${backupRefCreated ? backupRef : 'reflog'}\`.`
|
|
1519
|
+
: `1. Inspect the quarantined diff: \`git -C "${rootDir}" diff refs/remotes/origin/${branchName}${backupRefCreated ? ` ${backupRef}` : ''} -- .\``,
|
|
1520
|
+
forceRemoved
|
|
1521
|
+
? ''
|
|
1522
|
+
: `2. Cherry-pick the commits you want from \`${backupRefCreated ? backupRef : `<inspect ${quarantinedPath}>`}\` onto a fresh ${branchName} worktree.`,
|
|
1523
|
+
forceRemoved
|
|
1524
|
+
? ''
|
|
1525
|
+
: `3. Inspect uncommitted edits inside the quarantined dir directly: \`${quarantinedPath}\`.`,
|
|
1526
|
+
forceRemoved
|
|
1527
|
+
? ''
|
|
1528
|
+
: `4. Delete the quarantined dir when done: \`Remove-Item -Recurse -Force "${quarantinedPath}"\` (Windows) or \`rm -rf "${quarantinedPath}"\` (POSIX).`,
|
|
1314
1529
|
backupRefCreated ? `5. Delete the backup ref when done: \`git -C "${rootDir}" update-ref -d ${backupRef}\`.` : '',
|
|
1315
1530
|
].filter(Boolean).join('\n');
|
|
1316
1531
|
try {
|
|
@@ -1319,8 +1534,13 @@ async function _quarantineDirtyWorktree(rootDir, worktreePath, branchName, gitOp
|
|
|
1319
1534
|
log('warn', `_quarantineDirtyWorktree: writeToInbox failed: ${e.message}`);
|
|
1320
1535
|
}
|
|
1321
1536
|
|
|
1322
|
-
log('warn', `Quarantined dirty worktree ${worktreePath} → ${quarantinedPath} (branch ${branchName}, ${diag.ahead || 0} ahead, ${diag.behind || 0} behind, ${dirtyFiles.length} dirty files)`);
|
|
1323
|
-
return {
|
|
1537
|
+
log('warn', `Quarantined dirty worktree ${worktreePath} → ${forceRemoved ? '<force-removed>' : quarantinedPath} (branch ${branchName}, ${diag.ahead || 0} ahead, ${diag.behind || 0} behind, ${dirtyFiles.length} dirty files${renameAttempts > 1 ? `, rename retries=${renameAttempts}` : ''}${forceRemoved ? ', force-remove fallback' : ''})`);
|
|
1538
|
+
return {
|
|
1539
|
+
quarantinedPath: forceRemoved ? null : quarantinedPath,
|
|
1540
|
+
backupRef: backupRefCreated ? backupRef : null,
|
|
1541
|
+
forceRemoved,
|
|
1542
|
+
renameAttempts,
|
|
1543
|
+
};
|
|
1324
1544
|
}
|
|
1325
1545
|
|
|
1326
1546
|
async function recoverPartialWorktree(rootDir, worktreePath, branchName, gitOpts) {
|
|
@@ -2070,8 +2290,23 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2070
2290
|
// no-upstream / dirty-after-clean stay non-retryable because they
|
|
2071
2291
|
// protect potentially-unpushed agent work.
|
|
2072
2292
|
const isStatusProbeFailed = cleanResult.reason === 'status-failed';
|
|
2073
|
-
|
|
2074
|
-
|
|
2293
|
+
// W-mq5n1zx5 Layer 1c: when quarantine itself errored (rename retries
|
|
2294
|
+
// + force-remove fallback all failed) the worktree dir is still on
|
|
2295
|
+
// disk and the failure is purely environmental — a lingering git.exe
|
|
2296
|
+
// descendant or AV scan holding handles. Route through a dedicated
|
|
2297
|
+
// WORKTREE_QUARANTINE_ENV_BLOCKED class so the dispatch-side retry
|
|
2298
|
+
// counter doesn't bump (agentRetryable:false → non-retryable branch
|
|
2299
|
+
// → no _retriesByAgent / _retryCount mutation) and the WI auto-
|
|
2300
|
+
// recovery loop in discoverFromWorkItems re-queues this item under
|
|
2301
|
+
// the existing _quarantineRecoveryCount cap. This stops env failures
|
|
2302
|
+
// from burning the per-agent retry budget the way they did before.
|
|
2303
|
+
const isQuarantineEnvBlocked = !!(cleanResult.quarantineError && !cleanResult.quarantined);
|
|
2304
|
+
const failureClassValue = isQuarantineEnvBlocked
|
|
2305
|
+
? FAILURE_CLASS.WORKTREE_QUARANTINE_ENV_BLOCKED
|
|
2306
|
+
: (isDivergent ? FAILURE_CLASS.WORKTREE_DIVERGENT : FAILURE_CLASS.WORKTREE_DIRTY);
|
|
2307
|
+
const failureClassName = isQuarantineEnvBlocked
|
|
2308
|
+
? 'WORKTREE_QUARANTINE_ENV_BLOCKED'
|
|
2309
|
+
: (isDivergent ? 'WORKTREE_DIVERGENT' : 'WORKTREE_DIRTY');
|
|
2075
2310
|
const reasonMsg = cleanResult.quarantined
|
|
2076
2311
|
? `${failureClassName}: reused worktree at ${worktreePath} was dirty/divergent (${cleanResult.reason}; ${cleanResult.ahead || 0} ahead, ${cleanResult.behind || 0} behind, ${cleanResult.dirtyFiles?.length || 0} dirty file(s)${previewFiles ? ': ' + previewFiles : ''}) — quarantined to ${cleanResult.quarantinedPath}. Next dispatch will start fresh.`
|
|
2077
2312
|
: `${failureClassName}: reused worktree at ${worktreePath} is dirty/divergent (${cleanResult.reason}; ${cleanResult.ahead || 0} ahead, ${cleanResult.behind || 0} behind, ${cleanResult.dirtyFiles?.length || 0} file(s)${previewFiles ? ': ' + previewFiles : ''}). Quarantine ${cleanResult.quarantineError ? 'errored: ' + cleanResult.quarantineError : (cleanResult.quarantineSkipped ? 'was skipped — another live dispatch claims the worktree (see notes/inbox/ engine-worktree-skip-live note).' : 'was not attempted (' + cleanResult.reason + ').')}`;
|
|
@@ -2081,7 +2316,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2081
2316
|
id,
|
|
2082
2317
|
DISPATCH_RESULT.ERROR,
|
|
2083
2318
|
reasonMsg.slice(0, 500),
|
|
2084
|
-
`Engine preflight refused to dispatch into a dirty/divergent reused worktree (#2996). Reason: ${cleanResult.reason}.${cleanResult.quarantined ? ` Worktree quarantined to ${cleanResult.quarantinedPath}; backup ref ${cleanResult.backupRef || '(skipped)'}. See notes/inbox/ for recovery instructions.` : (cleanResult.quarantineSkipped ? ' Quarantine was skipped because another live dispatch claims this worktree path; this dispatch will not auto-retry until the live claimant clears.' : '')}`,
|
|
2319
|
+
`Engine preflight refused to dispatch into a dirty/divergent reused worktree (#2996). Reason: ${cleanResult.reason}.${cleanResult.quarantined ? ` Worktree quarantined to ${cleanResult.quarantinedPath}; backup ref ${cleanResult.backupRef || '(skipped)'}. See notes/inbox/ for recovery instructions.` : (cleanResult.quarantineSkipped ? ' Quarantine was skipped because another live dispatch claims this worktree path; this dispatch will not auto-retry until the live claimant clears.' : '')}${isQuarantineEnvBlocked ? ' Environmental quarantine failure (Windows EBUSY); WI auto-recovery loop will re-queue without bumping per-agent retry counter.' : ''}`,
|
|
2085
2320
|
{ agentRetryable: isStatusProbeFailed && cleanResult.quarantined, failureClass: failureClassValue },
|
|
2086
2321
|
);
|
|
2087
2322
|
cleanupTempAgent(agentId);
|
|
@@ -2260,7 +2495,10 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2260
2495
|
let stashed = false;
|
|
2261
2496
|
if (!depMergeFailed && !skipDepMerge && prunedDeps.length > 0) {
|
|
2262
2497
|
try {
|
|
2263
|
-
const
|
|
2498
|
+
const statusArgs = ENGINE_DEFAULTS.statusProbeUseNoOptionalLocks
|
|
2499
|
+
? ['--no-optional-locks', 'status', '--porcelain']
|
|
2500
|
+
: ['status', '--porcelain'];
|
|
2501
|
+
const statusOut = gitOutputToString(await shared.shellSafeGit(statusArgs, { ..._gitOpts, cwd: worktreePath })).trim();
|
|
2264
2502
|
if (statusOut) {
|
|
2265
2503
|
await shared.shellSafeGit(['stash', 'push', '--include-untracked', '-m', 'engine: stash before dep re-merge'], { ..._gitOpts, cwd: worktreePath });
|
|
2266
2504
|
stashed = true;
|
|
@@ -2501,7 +2739,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2501
2739
|
if (worktreePath && fs.existsSync(worktreePath)) {
|
|
2502
2740
|
_phaseT.dirtyProbeStart = Date.now();
|
|
2503
2741
|
try {
|
|
2504
|
-
const dirtyResult = await execAsync(
|
|
2742
|
+
const dirtyResult = await execAsync(_statusPorcelainCmd(), { ..._gitOpts, cwd: worktreePath, timeout: 10000 });
|
|
2505
2743
|
const dirtyOutput = (dirtyResult.stdout || '').trim();
|
|
2506
2744
|
if (dirtyOutput) {
|
|
2507
2745
|
const dirtyFiles = dirtyOutput.split('\n').map(l => l.trim()).filter(Boolean);
|
|
@@ -5666,19 +5904,28 @@ async function discoverFromPrs(config, project) {
|
|
|
5666
5904
|
// starvation guarantee.
|
|
5667
5905
|
const conflictCauseKey = getPrAutomationCauseKey('merge-conflict', pr);
|
|
5668
5906
|
const key = getPrAutomationDispatchKey(`conflict-fix-${project?.name || 'default'}-${prDisplayId}`, conflictCauseKey);
|
|
5669
|
-
// W-mpritzcr0004afc5 (#2955): per-cause same-head guard mirroring
|
|
5670
|
-
// build-failure block above. `_conflictFixedAt` is a 10-min wall-clock
|
|
5671
|
-
// suppression for ADO/GH mergeStatus lag; `_lastDispatchByCause` is
|
|
5672
|
-
//
|
|
5673
|
-
// base+
|
|
5907
|
+
// W-mpritzcr0004afc5 (#2955) + #3079: per-cause same-head guard mirroring
|
|
5908
|
+
// the build-failure block above. `_conflictFixedAt` is a 10-min wall-clock
|
|
5909
|
+
// suppression for ADO/GH mergeStatus lag; `_lastDispatchByCause` is now
|
|
5910
|
+
// a (head+base+targetRef)-pinned suppression for repeated agent noops on
|
|
5911
|
+
// an unchanged source+base+target tuple. #3079: prior version compared
|
|
5912
|
+
// only headSha, so retargeting a PR (e.g. parent-branch → main after
|
|
5913
|
+
// parent merge) never released the pause even though the base SHA and
|
|
5914
|
+
// target ref had changed and conflicts may have become resolvable.
|
|
5915
|
+
const currentGuardKey = shared.prMergeConflictGuardKey(pr);
|
|
5674
5916
|
const currentHeadSha = String(pr.headSha || pr._adoSourceCommit || pr._adoHeadCommit || '').trim();
|
|
5675
5917
|
const lastConflictDispatch = pr._lastDispatchByCause?.[shared.PR_FIX_CAUSE.MERGE_CONFLICT];
|
|
5676
|
-
|
|
5677
|
-
|
|
5918
|
+
// Prefer the new mergeConflictKey (post-#3079); fall back to legacy
|
|
5919
|
+
// headSha compare so PRs paused before this fix don't loop forever.
|
|
5920
|
+
const lastGuardKey = lastConflictDispatch?.mergeConflictKey;
|
|
5921
|
+
const guardKeyMatches = !!(lastGuardKey && currentGuardKey && lastGuardKey === currentGuardKey);
|
|
5922
|
+
const legacyHeadMatches = !lastGuardKey && !!(lastConflictDispatch?.headSha
|
|
5678
5923
|
&& currentHeadSha
|
|
5679
5924
|
&& lastConflictDispatch.headSha === currentHeadSha);
|
|
5925
|
+
const skipConflictFix = !!(lastConflictDispatch?.outcome === 'noop'
|
|
5926
|
+
&& (guardKeyMatches || legacyHeadMatches));
|
|
5680
5927
|
if (skipConflictFix) {
|
|
5681
|
-
log('info', `Skipping conflict-fix for ${pr.id}: last merge-conflict dispatch was noop on the same
|
|
5928
|
+
log('info', `Skipping conflict-fix for ${pr.id}: last merge-conflict dispatch was noop on the same source+base+target (${(lastConflictDispatch.reason || '').slice(0, 120)})`);
|
|
5682
5929
|
continue;
|
|
5683
5930
|
}
|
|
5684
5931
|
// Suppress re-dispatch for 10 min after last attempt — ADO/GitHub recomputes
|
|
@@ -5966,6 +6213,18 @@ function renderProjectWorkItemPromptForAgent(item, workType, agentId, config, pr
|
|
|
5966
6213
|
// failure via the qa-session-draft-failed / qa-session-execute-failed
|
|
5967
6214
|
// path. (See playbooks/qa-session-draft.md → "Failure path" section.)
|
|
5968
6215
|
..._buildRunnerBriefVars(item, project),
|
|
6216
|
+
// W-mq16xtdx001a347e + W-mq1cczi90006b21f — escape hatches for dispatches
|
|
6217
|
+
// that should NOT be steered toward project-local skills (e.g. when the
|
|
6218
|
+
// diff under review IS that skill, so a meta-review needs first-principles).
|
|
6219
|
+
// Default OFF — the project skills block is the whole point of these WIs
|
|
6220
|
+
// and should surface on every applicable dispatch by default.
|
|
6221
|
+
//
|
|
6222
|
+
// Both meta names are honored:
|
|
6223
|
+
// - meta.skipProjectReviewSkills (PR-82 alias, review-only suppression)
|
|
6224
|
+
// - meta.skipProjectSkills (W-mq1cczi90006b21f, generic suppression)
|
|
6225
|
+
// Either flag suppresses both blocks on the dispatch.
|
|
6226
|
+
skip_project_review_skills: !!(item.meta && item.meta.skipProjectReviewSkills),
|
|
6227
|
+
skip_project_skills: !!(item.meta && (item.meta.skipProjectSkills || item.meta.skipProjectReviewSkills)),
|
|
5969
6228
|
};
|
|
5970
6229
|
const cpResult = buildWorkItemDispatchVars(item, vars, config, {
|
|
5971
6230
|
worktreePath: vars.worktree_path || root,
|
|
@@ -6139,8 +6398,10 @@ function discoverFromWorkItems(config, project) {
|
|
|
6139
6398
|
const fr = String(item.failReason || '');
|
|
6140
6399
|
const isQuarantineFail = item._failureClass === FAILURE_CLASS.WORKTREE_DIRTY
|
|
6141
6400
|
|| item._failureClass === FAILURE_CLASS.WORKTREE_DIVERGENT
|
|
6401
|
+
|| item._failureClass === FAILURE_CLASS.WORKTREE_QUARANTINE_ENV_BLOCKED
|
|
6142
6402
|
|| /\bWORKTREE_DIRTY\b/.test(fr)
|
|
6143
|
-
|| /\bWORKTREE_DIVERGENT\b/.test(fr)
|
|
6403
|
+
|| /\bWORKTREE_DIVERGENT\b/.test(fr)
|
|
6404
|
+
|| /\bWORKTREE_QUARANTINE_ENV_BLOCKED\b/.test(fr);
|
|
6144
6405
|
if (isQuarantineFail) {
|
|
6145
6406
|
item._quarantineRecoveryCount = (item._quarantineRecoveryCount || 0) + 1;
|
|
6146
6407
|
const cap = ENGINE_DEFAULTS.quarantineAutoRecoveryMax || 2;
|
|
@@ -8270,6 +8531,7 @@ module.exports = {
|
|
|
8270
8531
|
gitOutputToString, gitErrorOutput, classifyDepMergeFailureOutput, listUnmergedFiles, // exported for testing
|
|
8271
8532
|
buildDepConflictFixItem, deriveConflictFixKey, // exported for testing (W-mpcwojgr000a0244)
|
|
8272
8533
|
isWorktreeRetryableError, removeStaleIndexLock, syncReusedWorktree, assertCleanSharedWorktree, _quarantineDirtyWorktree, // exported for testing
|
|
8534
|
+
_renameWithRetry, _statusPorcelainCmd, _killGitDescendantsForWorktree, _bumpQuarantineOutcome, // exported for testing (W-mq5n1zx5)
|
|
8273
8535
|
pruneStaleWorktreeForBranch, // exported for testing
|
|
8274
8536
|
findExistingWorktree, // exported for testing
|
|
8275
8537
|
probeBranchOnRemote, // exported for testing (W-mphnm6a1000281b8)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2149",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|
package/playbooks/fix.md
CHANGED
|
@@ -44,6 +44,10 @@ Before editing, split the feedback into:
|
|
|
44
44
|
|
|
45
45
|
## Health Check
|
|
46
46
|
|
|
47
|
+
{{#project_skills_block}}
|
|
48
|
+
{{project_skills_block}}
|
|
49
|
+
|
|
50
|
+
{{/project_skills_block}}
|
|
47
51
|
Before starting work, run `git status` and verify the worktree is clean and on the expected branch (`{{pr_branch}}`). If the worktree is dirty or on the wrong branch, report the issue and stop.
|
|
48
52
|
|
|
49
53
|
### Branch-mismatch guard (issue #2999)
|
|
@@ -45,6 +45,10 @@ Do ALL work in the worktree.
|
|
|
45
45
|
|
|
46
46
|
## Health Check
|
|
47
47
|
|
|
48
|
+
{{#project_skills_block}}
|
|
49
|
+
{{project_skills_block}}
|
|
50
|
+
|
|
51
|
+
{{/project_skills_block}}
|
|
48
52
|
Before starting work, run `git status` and verify the worktree is clean and on the expected branch (`{{branch_name}}`). If the worktree is dirty or on the wrong branch, report the issue and stop.
|
|
49
53
|
|
|
50
54
|
## Working Style
|
package/playbooks/implement.md
CHANGED
|
@@ -38,6 +38,10 @@ If this feature spans multiple projects, inspect the relevant repos, make change
|
|
|
38
38
|
|
|
39
39
|
## Health Check
|
|
40
40
|
|
|
41
|
+
{{#project_skills_block}}
|
|
42
|
+
{{project_skills_block}}
|
|
43
|
+
|
|
44
|
+
{{/project_skills_block}}
|
|
41
45
|
Before starting work, run `git status` and verify the worktree is clean and on the expected branch. If the worktree is dirty or on the wrong branch, report the issue and stop.
|
|
42
46
|
|
|
43
47
|
## Working Style
|
package/playbooks/plan-to-prd.md
CHANGED
|
@@ -17,6 +17,10 @@ A user has provided a plan. Analyze it against the codebase and produce a struct
|
|
|
17
17
|
|
|
18
18
|
## Instructions
|
|
19
19
|
|
|
20
|
+
{{#project_skills_block}}
|
|
21
|
+
{{project_skills_block}}
|
|
22
|
+
|
|
23
|
+
{{/project_skills_block}}
|
|
20
24
|
1. **Read the plan carefully** — understand the goals, scope, and requirements
|
|
21
25
|
- If the plan declares `Project: <name>` (including `**Project:** <name>`), the engine has resolved `{{project_name}}` from that declaration. Preserve `{{project_name}}` for the top-level `project`, default item `project`, filename, and implementation framing; do not let contextual mentions of another product or repository override it.
|
|
22
26
|
2. **Check for an existing PRD** — if the engine provides `existing_prd_json` below, a PRD already exists for this plan. See "Reusing an Existing PRD" section for how to preserve item IDs and done statuses. If no existing PRD is provided, this is a fresh run — all items start as `"missing"`.
|
package/playbooks/plan.md
CHANGED
|
@@ -27,6 +27,10 @@ A user has described a feature they want built. Your job is to create a detailed
|
|
|
27
27
|
- Identify the core goal, constraints, and success criteria
|
|
28
28
|
- Note any ambiguities that need to be called out
|
|
29
29
|
|
|
30
|
+
{{#project_skills_block}}
|
|
31
|
+
{{project_skills_block}}
|
|
32
|
+
|
|
33
|
+
{{/project_skills_block}}
|
|
30
34
|
### 2. Explore the Codebase
|
|
31
35
|
- Read `CLAUDE.md` at repo root and relevant directories
|
|
32
36
|
- Map the areas of code that this feature will touch
|
package/playbooks/review.md
CHANGED
|
@@ -27,10 +27,10 @@ Use subagents only for genuinely parallel, independent tasks (e.g., reviewing un
|
|
|
27
27
|
git diff {{main_branch}}...origin/{{pr_branch}}
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
{{#
|
|
31
|
-
{{
|
|
30
|
+
{{#project_skills_block}}
|
|
31
|
+
{{project_skills_block}}
|
|
32
32
|
|
|
33
|
-
{{/
|
|
33
|
+
{{/project_skills_block}}
|
|
34
34
|
2. Think about deploy risk before commenting:
|
|
35
35
|
- What user-visible behavior changed?
|
|
36
36
|
- What dependencies, callers, or tests could be affected?
|
package/bin/minions.js.rej
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
diff a/bin/minions.js b/bin/minions.js (rejected hunks)
|
|
2
|
-
@@ -852,6 +852,14 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
3
|
-
|
|
4
|
-
Dashboard:
|
|
5
|
-
minions dash Start web dashboard (default :7331)
|
|
6
|
-
+
|
|
7
|
-
+ Watchdog (out-of-process recovery):
|
|
8
|
-
+ minions watchdog install [--interval=5]
|
|
9
|
-
+ Register OS scheduler task to probe + heal every N minutes
|
|
10
|
-
+ (Windows Task Scheduler / macOS launchd / Linux systemd --user)
|
|
11
|
-
+ minions watchdog uninstall Remove the scheduled task (idempotent)
|
|
12
|
-
+ minions watchdog status Show registration + last-run details from the OS scheduler
|
|
13
|
-
+ minions watchdog tick One-shot probe + recovery (used by the scheduler; safe to run by hand)
|
|
14
|
-
${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
|
|
15
|
-
Dev mode (this checkout, contributors only):
|
|
16
|
-
minions --dev <cmd> Run against this checkout instead of ~/.minions/
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
diff a/dashboard/slim/body.html b/dashboard/slim/body.html (rejected hunks)
|
|
2
|
-
@@ -8,7 +8,8 @@
|
|
3
|
-
silently break (handler attaches before crash, button still renders,
|
|
4
|
-
click fires but no global handler). addEventListener attaches inside
|
|
5
|
-
the same scope and is observable in DevTools when wiring fails. -->
|
|
6
|
-
- <button id="slim-new-chat-btn" class="icon-btn" title="New chat (opens a new tab)">✎</button>
|
|
7
|
-
+ <button id="slim-back-classic-btn" class="topbar-back-btn" title="Return to the classic dashboard">← Classic dashboard</button>
|
|
8
|
-
+ <button id="slim-report-bug-btn" class="topbar-back-btn" title="Report a bug in Minions">Report Bug</button>
|
|
9
|
-
<button id="slim-settings-btn" class="icon-btn" title="Settings">⚙</button>
|
|
10
|
-
</div>
|
|
11
|
-
</div>
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
diff a/dashboard/slim/js/command-send.js b/dashboard/slim/js/command-send.js (rejected hunks)
|
|
2
|
-
@@ -195,8 +195,8 @@
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
// ── Wiring ──────────────────────────────────────────────────────
|
|
6
|
-
- var newChatBtn = document.getElementById('slim-new-chat-btn');
|
|
7
|
-
- if (newChatBtn) newChatBtn.addEventListener('click', function() { newTab(); });
|
|
8
|
-
+ // (The header "new chat" button was replaced by "Report Bug"; new tabs are
|
|
9
|
-
+ // created via the "+" affordance in the chat tab bar — see renderTabBar.)
|
|
10
|
-
sendBtn.addEventListener('click', sendMessage);
|
|
11
|
-
stopBtn.addEventListener('click', abortActive);
|
|
12
|
-
inputEl.addEventListener('keydown', function(ev) {
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
diff a/dashboard/slim/js/history.js b/dashboard/slim/js/history.js (rejected hunks)
|
|
2
|
-
@@ -96,7 +83,12 @@
|
|
3
|
-
chip.title = 'review';
|
|
4
|
-
} else {
|
|
5
|
-
chip.className = 'completions-card-type-chip';
|
|
6
|
-
- chip.textContent = typeEmoji(c.type);
|
|
7
|
-
+ // Known types → Fluent icon via CSS mask (data-type); unknown → "•" glyph.
|
|
8
|
-
+ if (c.type && TYPE_ICON_SET[c.type]) {
|
|
9
|
-
+ chip.setAttribute('data-type', c.type);
|
|
10
|
-
+ } else {
|
|
11
|
-
+ chip.textContent = '•';
|
|
12
|
-
+ }
|
|
13
|
-
if (c.type) chip.title = c.type;
|
|
14
|
-
}
|
|
15
|
-
railTop.appendChild(chip);
|
|
16
|
-
@@ -105,7 +97,9 @@
|
|
17
|
-
railBottom.className = 'completions-card-rail-bottom';
|
|
18
|
-
var icon = document.createElement('span');
|
|
19
|
-
icon.className = 'completions-card-status-icon ' + status;
|
|
20
|
-
- icon.textContent = status === 'active' ? '●' : (status === 'ok' ? '✓' : (status === 'warn' ? '⚠' : '✕'));
|
|
21
|
-
+ // ok/warn/fail render as Fluent icons via the status class (CSS mask in
|
|
22
|
-
+ // styles.css); the live 'active' state keeps its pulsing dot.
|
|
23
|
-
+ if (status === 'active') icon.textContent = '●';
|
|
24
|
-
icon.title = status === 'active' ? 'Running' : (status === 'ok' ? 'Success' : (status === 'warn' ? 'Partial' : 'Failure'));
|
|
25
|
-
var sep = document.createElement('span');
|
|
26
|
-
sep.className = 'completions-card-rail-sep';
|