@yemi33/minions 0.1.2323 → 0.1.2325

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.
@@ -294,7 +294,6 @@ function renderCcHint() {
294
294
  'line-height:1.5',
295
295
  ].join(';');
296
296
  document.body.appendChild(mount);
297
- // eslint-disable-next-line no-unsanitized/property -- reason: static string literal — no user-controlled data is interpolated
298
297
  mount.innerHTML =
299
298
  '<div id="cc-hint-arrow" style="position:absolute;top:-7px;width:12px;height:12px;background:var(--surface);border-left:1px solid var(--blue);border-top:1px solid var(--blue);transform:rotate(45deg)"></div>' +
300
299
  '<div style="font-weight:700;color:var(--blue);margin-bottom:4px">&#x1F44B; Start here</div>' +
package/dashboard.js CHANGED
@@ -28,10 +28,13 @@ const v8 = require('v8');
28
28
  const llm = require('./engine/llm');
29
29
  const { resolveRuntime } = require('./engine/runtimes');
30
30
 
31
- // Dashboard version stamp — captured at module load so it reflects the code actually running
31
+ // Dashboard version stamp — captured at module load so it reflects the code actually running.
32
+ // codeCommit is read via _readGitHeadShort (defined below — function declarations
33
+ // are hoisted, so the call here resolves fine) instead of spawning a `git`
34
+ // subprocess; see that function's doc comment for the rationale.
32
35
  const _dashboardVersion = {
33
36
  codeVersion: (() => { try { return require('./package.json').version; } catch {} try { return require('@yemi33/minions/package.json').version; } catch {} return null; })(),
34
- codeCommit: (() => { try { return require('child_process').execSync('git rev-parse --short HEAD', { cwd: __dirname, encoding: 'utf8', timeout: 5000, windowsHide: true }).trim(); } catch { return null; } })(),
37
+ codeCommit: _readGitHeadShort(__dirname).commit,
35
38
  startedAt: new Date().toISOString(),
36
39
  pid: process.pid,
37
40
  };
@@ -2252,13 +2255,16 @@ let _diskVersionCacheTs = 0;
2252
2255
  const DISK_VERSION_TTL = 60000; // re-check every 60s
2253
2256
 
2254
2257
  // Read the short HEAD commit by parsing `.git` directly — NO `git` subprocess.
2255
- // The old `execSync('git rev-parse --short HEAD', { timeout: 5000 })` spawned a
2256
- // synchronous child process on the dashboard's single event loop; under
2257
- // concurrent agent git activity (worktree add/merge holding repo locks) that
2258
- // subprocess could take its full 5s timeout, and because getDiskVersion runs
2259
- // inside the /api/status rebuild, every in-flight poll blocked for that whole
2260
- // window. Reading `.git/HEAD` + the resolved ref (loose ref, then packed-refs
2261
- // fallback) is a couple of tiny synchronous file reads with zero process spawn.
2258
+ // The old `execSync('git rev-parse --short HEAD', { timeout: 5000 })` (both here
2259
+ // and in the `_dashboardVersion.codeCommit` init near the top of this file)
2260
+ // spawned a synchronous child process on the dashboard's single event loop;
2261
+ // under concurrent agent git activity (worktree add/merge holding repo locks)
2262
+ // that subprocess could take its full 5s timeout, and because getDiskVersion
2263
+ // runs inside the /api/status rebuild, every in-flight poll blocked for that
2264
+ // whole window. Reading `.git/HEAD` + the resolved ref (loose ref, then
2265
+ // packed-refs fallback) is a couple of tiny synchronous file reads with zero
2266
+ // process spawn. Function declarations are hoisted, so `_dashboardVersion`'s
2267
+ // module-load-time init above can call this even though it's defined here.
2262
2268
  // Returns { commit, isGitRepo }: commit is the 7-char short SHA or null.
2263
2269
  function _readGitHeadShort(repoDir) {
2264
2270
  try {
@@ -31,7 +31,7 @@ Canonical envelope (`_buildCcErrorEnvelope` in `dashboard.js`):
31
31
 
32
32
  **No auto-retry policy.** The backend never re-spawns the LLM after an error envelope. The client never silently resends the user's turn. Retry is a single-click manual action — guards against silent budget burn on `budget-exceeded`, infinite loops on `auth-failure`, and accidental re-charges on `context-limit`. The 429 + reconnect paths (rate-limited fetch retry, SSE reconnect-after-disconnect) remain — those are transport-level, not error-envelope-level.
33
33
 
34
- **Session-reset notices (`sessionReset` / `sessionResetReason`).** The streaming `done` event can carry `sessionReset: true` with a `sessionResetReason` of `promptChanged` (Minions updated its CC prompt template), `runtimeChanged` (CC runtime switched), or `idleReap` (W-mr0qs0vw — the persistent `copilot --acp` worker was killed by the idle reaper after `engine.ccWorkerIdleTimeoutMs` of inactivity, so the next message cold-spawns a fresh session with no memory). Both classic (`dashboard/js/command-center.js`) and slim (`dashboard/slim/js/command-send.js` + `chat.js#appendSystemNotice`) render a centered, muted system notice; for `idleReap` the text is "Context window cleared after inactivity — fresh session started." The idle-reap flag is a one-shot drained per turn via `ccWorkerPool.consumeIdleReapNotice(tabId)`.
34
+ **Session-reset notices (`sessionReset` / `sessionResetReason`).** The streaming `done` event can carry `sessionReset: true` with a `sessionResetReason` of `promptChanged` (Minions updated its CC prompt template), `runtimeChanged` (CC runtime switched), or `idleReap` (W-mr0qs0vw — the persistent `copilot --acp` worker was killed by the idle reaper after `engine.ccWorkerIdleTimeoutMs` of inactivity, so the next message cold-spawns a fresh session with no memory). Both classic (`dashboard/js/command-center.js`) and slim (`dashboard/slim/js/command-send.js` + `chat.js#appendSystemNotice`) render a centered, muted system notice; for `idleReap` the text is "Context window cleared after inactivity — fresh session started." The idle-reap flag is a one-shot drained per turn via `ccWorkerPool.consumeIdleReapNotice(tabId)`. **`ccWorkerIdleTimeoutMs: 0`** is an explicit "never idle-reap" sentinel (W-mr1qbj90000d30bd) — it bypasses the normal `[60000, 28800000]` clamp, maps to an `Infinity` window in `cc-worker-pool.js#setIdleTimeoutMs`, and the warm worker then only dies on an explicit `closeTab`. Settings page exposes this as the "Never idle-reap CC workers" toggle.
35
35
 
36
36
  ## Image attachments (PL-cc-image-attachments)
37
37
 
@@ -54,6 +54,10 @@ To kill them now, run: node engine.js kill
54
54
 
55
55
  If an agent is killed as an orphan and the work item retries, cooldowns use exponential backoff (2^failures, max 8x) to prevent spam-retrying broken tasks.
56
56
 
57
+ ### 5. Crash-Loop Counter + Alert (W-mr2azk6i000y06e0)
58
+
59
+ The engine.js *process itself* (not an agent) can crash and get auto-respawned by three independent mechanisms: `engine/supervisor.js#checkEngine` (dead-PID), `engine/supervisor.js#checkEngineHung` (stale-heartbeat), and `engine/watchdog.js` (OS-scheduled external recovery); `dashboard.js`'s in-process 30s watchdog also auto-restarts on a dead PID (the user-triggered Restart Engine button is excluded). Each successful respawn calls `shared.recordEngineRespawn(source)`, which appends `{ts, source}` to `control.json`'s `engineRespawns[]` (rolling window, default `CRASH_LOOP_WINDOW_MS` = 10 min). Once respawns within the window reach `CRASH_LOOP_THRESHOLD` (default 3), `control.crashLoopAlert` is set (dashboard-visible via `/api/status`) and a deduped (one-per-day) `notes/inbox/` alert is written; the alert clears automatically once the window rolls past the incident. Override the window/threshold via `MINIONS_CRASH_LOOP_WINDOW_MS` / `MINIONS_CRASH_LOOP_THRESHOLD` env vars.
60
+
57
61
  ## Safe Restart Pattern
58
62
 
59
63
  ```bash
@@ -32,7 +32,7 @@ Implementation: `engine.js` builds a `liveProjectsInUse` set from the active dis
32
32
 
33
33
  Before spawning, `engine/live-checkout.js#prepareLiveCheckout` runs `git status --porcelain` from `project.localPath`. Any output (staged, unstaged, untracked) **returns** an explicit `{ ok:false, reason:'dirty', dirtyFiles:[…] }` result (it does NOT throw), which refuses the dispatch immediately:
34
34
 
35
- - Non-retryable `FAILURE_CLASS.LIVE_CHECKOUT_DIRTY` (added to `engine/dispatch.js`'s `neverRetry` set so the dispatcher never re-spawns mechanically). **Reserved for this confirmed-dirty result only** — a thrown helper/git error is `LIVE_CHECKOUT_FAILED`, see below (#305).
35
+ - Non-retryable `FAILURE_CLASS.LIVE_CHECKOUT_DIRTY` (added to `engine/dispatch.js`'s `neverRetry` set so the dispatcher never re-spawns mechanically). **Reserved for this confirmed-dirty result only** — a thrown helper/git error is `LIVE_CHECKOUT_FAILED`, see below (#305). **Auto-cleanup exception (W-mqzmkoqt000hbca2, #582):** when `liveCheckoutAutoReset` or `liveCheckoutAutoStash` is enabled (per-project or fleet-wide), `spawnAgent` treats the dirty refusal as retryable instead — the engine re-cleans the tree on every attempt, so the two-strike cap below is skipped and the dispatch keeps retrying up to the normal `maxRetries` cap. The `dispatch.js` `neverRetry` entry is kept only as a defensive fallback for callers that omit the explicit `agentRetryable` override.
36
36
  - Inbox alert written via `dispatch.writeInboxAlert('live-checkout-dirty-<wi-id>', body)`. The body lists the dirty files verbatim from `git status --porcelain`.
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>`.
@@ -69,6 +69,9 @@ This is **DESTRUCTIVE** — it permanently discards the operator's uncommitted c
69
69
  - **Fleet-wide:** Dashboard → Settings → `Live-checkout auto-reset (fleet-wide)` (`engine.liveCheckoutAutoReset`).
70
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.)*
71
71
 
72
+ #### 2c. Stale `.git/index.lock` self-heal (W-mr3tayu4)
73
+
74
+ Before any preflight git command runs, `prepareLiveCheckout` calls the shared, age-gated `shared.removeStaleIndexLock(localPath)` to clear a leftover `.git/index.lock` from a crashed/killed git process. Live mode never resets/cleans the operator tree, so nothing else would clear it, and — since `LIVE_CHECKOUT_FAILED` is retryable — every automatic retry used to hit the same stale lock and fail identically forever until an operator deleted it by hand. The remover only touches locks older than 5 minutes, so a lock held by a currently-running git process is never removed out from under it. `engine.js`'s own worktree-mode lock cleanup now delegates to the same shared helper.
72
75
 
73
76
  ### 3. No auto-pull, no `--force`, no fast-forward
74
77
 
@@ -275,6 +278,6 @@ Live-checkout mode is deliberately small. These are NOT supported and will not b
275
278
  | `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). |
276
279
  | `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). |
277
280
  | `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). |
278
- | `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). |
281
+ | `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) — **unless** `liveCheckoutAutoReset`/`liveCheckoutAutoStash` is enabled, in which case the two-strike cap is skipped entirely and the dirty failure stays retryable up to `maxRetries` (W-mqzmkoqt000hbca2, #582). |
279
282
  | `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. |
280
283
  | `engine/timeout.js` header comment | Confirms no special live-mode kill handling. |
@@ -69,6 +69,10 @@ When all PRD items reach `done` or `failed` status, `checkPlanCompletion()` fire
69
69
  3. **PR work item** created for shared-branch plans (to merge the feature branch)
70
70
  4. **PRD status** persisted as `completed` with `_completionNotified`; files stay active until manual archive
71
71
 
72
+ `checkPlanCompletion()` also **self-heals a stale `completed` PRD**: if a completed plan later gains an unmaterialized item or one of its items regresses off a terminal status, the PRD is reverted to `active` (status flipped, `_completionNotified` cleared) so the materializer recreates the missing work item and the normal completion flow can run again.
73
+
74
+ **Separate lighter-weight path — `advancePrdStatusOnAllItemsDone()` (P-d7e8f9a1).** Called at the end of `syncPrdFromPrs` on the PR-poll cadence, this independently advances a PRD's top-level `status` to `completed` once every `missing_features` item has both (1) a work item in a `DONE_STATUSES` state and (2) its linked PR merged (or no linked PR, for non-code items). It skips frozen PRDs (`paused`, `rejected`, `awaiting-approval`, `completed`, `archived` — `_PRD_FROZEN_STATUSES`) and writes idempotently via `mutateJsonFileLocked` + `skipWriteIfUnchanged`. Unlike `checkPlanCompletion()`, it does not create the verify task, completion summary, or PR work item — those still only come from the `checkPlanCompletion()` path above.
75
+
72
76
  ## Verification Task
73
77
 
74
78
  The verification task (`planItemId: "VERIFY"`) is the final step. It:
@@ -118,7 +122,7 @@ The verify agent does not call `archivePlan()`. After the testing guide and any
118
122
 
119
123
  Manual archive behavior:
120
124
 
121
- 1. For PRD JSON files, the dashboard archives them **in-place** in `prd/` — the file is not moved. The JSON is stamped with `archived: true`, `status: "archived"`, and `archivedAt` (Phase 10 step 4.2b). The `.backup` sidecar is left intact since the file keeps its canonical path.
125
+ 1. For PRD JSON files, the dashboard archives them **in-place** in `prd/` — the file is not moved. The JSON is stamped with `archived: true`, `status: "archived"`, and `archivedAt` (Phase 10 step 4.2b). **No `.backup` sidecar is written or restored for any `prd/*.json`** — Phase 10 step 4.3 dropped the PRD backup/restore lifecycle entirely (root-level PRDs used to get one to survive the now-retired rename race; SQL is canonical, so a stale `.backup` is pure resurrection fuel, not a safety net).
122
126
  2. If the PRD has `source_plan`, the matching `plans/<source>.md` is moved to `plans/archive/<source>.md`.
123
127
  3. Pending or queued work items linked to the archived PRD are cancelled with `_cancelledBy: "plan-archived"`; completed work items remain as history.
124
128
  4. Plan worktrees are cleaned up by the archive handler/lifecycle cleanup path.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2323",
3
+ "version": "0.1.2325",
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"