@yemi33/minions 0.1.2379 → 0.1.2381

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.
Files changed (100) hide show
  1. package/bin/minions.js +19 -9
  2. package/dashboard/js/refresh.js +3 -4
  3. package/dashboard/js/render-other.js +43 -23
  4. package/dashboard/js/render-prd.js +1 -1
  5. package/dashboard/js/render-work-items.js +13 -6
  6. package/dashboard/js/settings.js +40 -17
  7. package/dashboard.js +435 -768
  8. package/docs/architecture-review-2026-07-09.md +2 -4
  9. package/docs/auto-discovery.md +36 -49
  10. package/docs/blog-first-successful-dispatch.md +1 -1
  11. package/docs/branch-derivation.md +2 -2
  12. package/docs/command-center.md +1 -1
  13. package/docs/completion-reports.md +14 -4
  14. package/docs/constellation-bridge.md +59 -10
  15. package/docs/constellation-style-telemetry.md +6 -6
  16. package/docs/cooldown-merge-semantics.md +4 -0
  17. package/docs/copilot-cli-schema.md +3 -3
  18. package/docs/cross-repo-plans.md +17 -17
  19. package/docs/deprecated.json +2 -2
  20. package/docs/design-state-storage.md +1 -1
  21. package/docs/documentation-audit-2026-07-09.md +2 -2
  22. package/docs/engine-restart.md +20 -8
  23. package/docs/harness-mode.md +1 -1
  24. package/docs/live-checkout-mode.md +45 -26
  25. package/docs/managed-spawn.md +4 -4
  26. package/docs/onboarding.md +1 -2
  27. package/docs/pr-comment-followup.md +3 -3
  28. package/docs/pr-review-fix-loop.md +1 -1
  29. package/docs/proposals/repo-pool-for-live-checkout.md +1 -2
  30. package/docs/qa-runbook-lifecycle.md +4 -4
  31. package/docs/qa-runbooks.md +2 -2
  32. package/docs/rfc-completion-json.md +4 -1
  33. package/docs/runtime-adapters.md +1 -1
  34. package/docs/self-improvement.md +4 -5
  35. package/docs/shared-lifecycle-module-map.md +3 -1
  36. package/docs/slim-ux/architecture-suggestions.md +5 -6
  37. package/docs/slim-ux/concepts.md +23 -25
  38. package/docs/watches.md +7 -7
  39. package/docs/workspace-manifests.md +1 -1
  40. package/docs/worktree-lifecycle.md +1 -1
  41. package/engine/abandoned-pr-reconciliation.js +4 -5
  42. package/engine/ado-status.js +5 -5
  43. package/engine/ado.js +20 -25
  44. package/engine/agent-worker-pool.js +58 -1
  45. package/engine/bridge.js +260 -5
  46. package/engine/cleanup.js +48 -131
  47. package/engine/cli.js +125 -83
  48. package/engine/cooldown.js +9 -16
  49. package/engine/db/index.js +22 -9
  50. package/engine/db/migrations/009-qa.js +1 -1
  51. package/engine/db/migrations/020-qa-session-scopes.js +23 -0
  52. package/engine/db/migrations/021-archived-work-items.js +72 -0
  53. package/engine/db/migrations/022-global-cc-session.js +31 -0
  54. package/engine/db/migrations/023-engine-state.js +30 -0
  55. package/engine/db/migrations/024-prd-ghost-collisions.js +53 -0
  56. package/engine/db/migrations/025-malformed-work-item-phantoms.js +33 -0
  57. package/engine/dispatch-store.js +2 -7
  58. package/engine/dispatch.js +39 -44
  59. package/engine/github.js +20 -27
  60. package/engine/lifecycle.js +279 -354
  61. package/engine/live-checkout.js +193 -149
  62. package/engine/llm.js +1 -1
  63. package/engine/logs-store.js +2 -2
  64. package/engine/managed-spawn.js +2 -23
  65. package/engine/meeting.js +6 -6
  66. package/engine/metrics-store.js +2 -2
  67. package/engine/note-link-backfill.js +6 -11
  68. package/engine/pipeline.js +18 -36
  69. package/engine/playbook.js +13 -16
  70. package/engine/prd-store.js +73 -54
  71. package/engine/preflight.js +2 -5
  72. package/engine/projects.js +15 -62
  73. package/engine/pull-requests-store.js +0 -17
  74. package/engine/qa-runbooks.js +2 -2
  75. package/engine/qa-runs.js +1 -8
  76. package/engine/qa-sessions.js +41 -64
  77. package/engine/queries.js +120 -219
  78. package/engine/routing.js +4 -6
  79. package/engine/scheduler.js +0 -4
  80. package/engine/shared-branch-pr-reconcile.js +2 -3
  81. package/engine/shared.js +268 -699
  82. package/engine/small-state-store.js +89 -10
  83. package/engine/state-operations.js +16 -4
  84. package/engine/stdio-timestamps.js +1 -1
  85. package/engine/timeout.js +5 -12
  86. package/engine/watch-actions.js +20 -22
  87. package/engine/watches-store.js +1 -1
  88. package/engine/watches.js +6 -10
  89. package/engine/work-item-validation.js +52 -0
  90. package/engine/work-items-store.js +127 -29
  91. package/engine/worktree-gc.js +2 -2
  92. package/engine/worktree-pool.js +8 -18
  93. package/engine.js +197 -358
  94. package/minions.js +2 -2
  95. package/package.json +1 -1
  96. package/playbooks/plan-to-prd.md +2 -2
  97. package/playbooks/shared-rules.md +3 -3
  98. package/playbooks/templates/followup-dispatch.md +1 -1
  99. package/playbooks/verify.md +1 -1
  100. package/prompts/cc-system.md +9 -9
@@ -13,11 +13,11 @@ canonical data, or misunderstand routing and dashboard behavior.
13
13
  |---|---|---|
14
14
  | Node.js 18+ was sufficient | Node.js 22.5+ is required for `node:sqlite` | `package.json:68` |
15
15
  | Agent dispatch was described as Claude-only | Copilot is the default fleet runtime; Claude and Codex remain supported | `engine/shared.js:3520`, `engine/runtimes/index.js` |
16
- | Root and per-project JSON trackers were described as primary state | Migrated runtime state is primary in `engine/state.db`; JSON trackers are passive mirrors | `engine/dispatch-store.js:49-68`, `CLAUDE.md` "State storage" |
16
+ | Root and per-project JSON trackers were described as primary state | `engine/state.db` is the sole runtime-state authority; retired trackers are migration inputs only | `engine/db/migrations/018-sql-only-cutover.js`, `CLAUDE.md` "State storage" |
17
17
  | `minions preflight` was documented as a public command | `minions doctor` is public; lighter checks run internally during initialization and startup | `bin/minions.js:1207-1264`, `engine/cli.js:232-255` |
18
18
  | Git repository host default was documented as ADO | The fallback host is GitHub | `engine/projects.js:504`, `engine/projects.js:564` |
19
19
  | `WORK_TYPE` documentation omitted complex build fixes | Added `BUILD_FIX_COMPLEX` | `engine/shared.js:4370-4396`, `routing.md:10-28` |
20
- | Command Center sessions were described as a JSON-file store | Sessions are stored in SQLite; the JSON file is a mirror | `engine/shared.js:1830`, `engine/db/migrations/011-remaining-state.js` |
20
+ | Command Center sessions were described as a JSON-file store | Per-tab and global sessions are stored only in SQLite | `engine/small-state-store.js`, `engine/db/migrations/022-global-cc-session.js` |
21
21
  | Watches were described as checking every three ticks and persisting to JSON | The default cadence is 18 ticks; persistence routes through the SQL watch store | `engine/shared.js` `watchPollEvery`, `engine/watches-store.js` |
22
22
  | Two local documentation links targeted files that are not shipped | Removed the runtime `notes.md` link and the stale managed-spawn plan link | `docs/README.md`, `docs/managed-spawn.md` |
23
23
 
@@ -10,11 +10,11 @@ When the engine restarts, it loses its in-memory process handles (`activeProcess
10
10
 
11
11
  | State | Storage | Survives Restart |
12
12
  |-------|---------|-----------------|
13
- | Dispatch queue (pending/active/completed) | `engine/state.db` (`dispatches`; `engine/dispatch.json` is a passive mirror) | Yes |
13
+ | Dispatch queue (pending/active/completed) | `engine/state.db` (`dispatches`) | Yes |
14
14
  | Agent status (working/idle/error) | Derived from `engine/state.db` | Yes |
15
15
  | Agent live output | `agents/*/live-output.log` | Yes (mtime used for orphan cleanup) |
16
16
  | Process handles (`ChildProcess`) | In-memory Map | **No** |
17
- | Cooldown timestamps | In-memory Map | **No** (repopulated from `engine/cooldowns.json`) |
17
+ | Cooldown timestamps | In-memory Map + `engine/state.db` | Yes (repopulated from SQL) |
18
18
 
19
19
  ## Protection Mechanisms
20
20
 
@@ -37,7 +37,20 @@ After the grace period expires, a dispatch with a tracked live process keeps run
37
37
 
38
38
  If there is no live tracked process, the engine uses `live-output.log` mtime as indirect evidence. Once the log is stale for `engine.heartbeatTimeout`, the dispatch is treated as an orphan and marked failed.
39
39
 
40
- ### 3. Stop Warning
40
+ ### 3. Intentional Restart Drains Pooled Leases
41
+
42
+ `engine.js stop` writes a shutdown request into `control.json`; it does not
43
+ tree-kill the daemon. The running engine stops dispatching new work and drains
44
+ fleet ACP-pool leases for up to `engine.shutdownTimeout` (5 minutes by
45
+ default). Cold-spawn agents are detached and are not part of this wait, so they
46
+ can survive for restart reattachment. A pooled lease cannot be reattached:
47
+ when the drain deadline expires it is cancelled and terminalized as a
48
+ retryable timeout before the old engine exits.
49
+
50
+ Unexpected crashes remain fail-safe: persisted `item.pooled` dispatches never
51
+ trust the worker PID and are reaped through the normal retry path on startup.
52
+
53
+ ### 4. Stop Warning
41
54
 
42
55
  `engine.js stop` checks for active dispatches and warns:
43
56
  ```
@@ -45,16 +58,15 @@ WARNING: 2 agent(s) are still working:
45
58
  - Dallas: [office-bohemia] Build & test PR PR-4959092
46
59
  - Rebecca: [office-bohemia] Review PR PR-4964594
47
60
 
48
- These agents will continue running but the engine won't monitor them.
49
- On next start, they'll get a 20-min grace period before being marked as orphans.
61
+ Detached agents will continue for restart reattachment; pooled agents will drain before exit.
50
62
  To kill them now, run: node engine.js kill
51
63
  ```
52
64
 
53
- ### 4. Exponential Backoff on Failures
65
+ ### 5. Exponential Backoff on Failures
54
66
 
55
67
  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
68
 
57
- ### 5. Crash-Loop Counter + Alert (W-mr2azk6i000y06e0)
69
+ ### 6. Crash-Loop Counter + Alert (W-mr2azk6i000y06e0)
58
70
 
59
71
  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
72
 
@@ -80,7 +92,7 @@ T+0s engine.js stop (warns about active agents)
80
92
  Engine process exits. Agents keep running as OS processes.
81
93
 
82
94
  T+30s Code changes made. engine.js start.
83
- Engine reads dispatch.json — finds 2 active items.
95
+ Engine reads SQL dispatch state — finds 2 active items.
84
96
  Sets grace period: 20 min from now.
85
97
  Logs: "2 active dispatch(es) from previous session"
86
98
 
@@ -88,5 +88,5 @@ Within a single tick the standard scheduled-work dedup is keyed by `_scheduleId`
88
88
 
89
89
  - Tri-agent items are **schedule-driven** — there's no manual "fire a harness mission" entry point. Add a schedule with `harness_mode: "tri_agent"` to opt in.
90
90
  - Iteration pairs always reuse the original mission's artifact path, threshold, max-iterations, and rubric. The evaluator's verdict feedback is appended to the next generator's prompt.
91
- - Mission state lives entirely on disk: the work-items.json trio + the artifact file. No new DB tables.
91
+ - Mission work-item state uses the existing SQL store; mission artifacts remain file-backed. No new tables are required.
92
92
  - Each iteration's evaluator is a separate work item, so dispatch retries, cooldowns, and steering apply normally to every role.
@@ -20,34 +20,34 @@ For these repos, the operator usually already has one canonical checkout that bu
20
20
 
21
21
  ## Contract (six guarantees)
22
22
 
23
- Live mode is opinionated about what the engine will and will not do to the operator's tree. The contract has six guarantees; the engine enforces all six and fails dispatches that would violate them.
23
+ Live mode is opinionated about what the engine will and will not do to the operator's tree. The contract has six guarantees; the engine enforces them by recovering when explicitly configured, otherwise leaving blocked work pending or failing closed.
24
24
 
25
25
  ### 1. Per-project mutating-concurrency cap of 1
26
26
 
27
- Only one mutating dispatch (fix / implement / review / test / verify / decompose / docs) runs per live-mode project at a time. Read-only types (meeting / ask / explore / plan / plan-to-prd) are excluded from the cap because they never write to disk.
27
+ Only one worktree-requiring dispatch (fix / build-fix-complex / implement / review / test / verify / decompose / setup / docs) runs per live-mode project at a time. Read-only types (meeting / ask / explore / plan / plan-to-prd) are excluded from the cap because they never write to disk.
28
28
 
29
29
  Implementation: `engine.js` builds a `liveProjectsInUse` set from the active dispatch list at every allocation pass and per-tick re-annotation; pending items whose project is already in the set are skipped with a `skipReason` and re-tried on the next tick. (`engine.js:7198`, `engine.js:7460`.)
30
30
 
31
- ### 2. Refuse-on-dirty (engine never resets the operator tree)
31
+ ### 2. Dirty-tree policy
32
32
 
33
- **W-mrcifup2000c218a — detected at ALLOCATION time first.** Before a live-mode work item is even dispatched, `engine.js`'s dispatch-existing-pending loop calls `engine/live-checkout.js#checkLiveCheckoutDirty` (a cheap, read-only, non-mutating `git status --porcelain=v1 -b` probe same command, same auto-cleanable-artifact allowance as the in-spawn check below) in the same pass that already gates on `live_checkout_busy`. A dirty hit stamps `_pendingReason: 'live_checkout_dirty'` on the pending item and `continue`s — **no dispatch attempt, no retry consumed, no failure recorded** — exactly like the busy gate. The item is re-checked every tick and dispatches normally the moment the tree is clean. A `live-checkout-dirty-<wi-id>` inbox alert is written (deduped per day, same as the in-spawn alert below) so a persistent dirty condition is still surfaced to a human even though the WI itself stays pending.
33
+ **W-mrcifup2000c218a — detected at ALLOCATION time first.** The allocator runs a cheap read-only dirty probe. When both auto-stash and auto-reset are disabled, a dirty hit stamps `_pendingReason: 'live_checkout_dirty'`, writes a deduped inbox alert, and leaves the WI pending without consuming a retry. When either recovery policy is enabled, dirt alone does not block allocation so `spawnAgent` can perform the configured recovery; the independent stale-base precheck still blocks committed base drift.
34
34
 
35
35
  The in-spawn check below remains as a **defense-in-depth fallback** for the race window between the allocation-time probe and the actual git operations inside `spawnAgent` (e.g. the tree goes dirty in the few seconds between the pre-check and the spawn) — it is now the *exception* path, not the common path.
36
36
 
37
- 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:
37
+ Before spawning, `engine/live-checkout.js#prepareLiveCheckout` first refuses an in-progress repository operation or detached HEAD, captures the original ref, then runs `git status --porcelain` from `project.localPath`. Any dirty output (staged, unstaged, untracked) **returns** an explicit `{ ok:false, reason:'dirty', dirtyFiles:[…] }` result unless explicit reset recovery is requested. `spawnAgent` uses that result to run auto-stash first and optional auto-reset second. Only when configured recovery is unavailable or fails does the confirmed-dirty result become a dispatch refusal:
38
38
 
39
39
  - 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.
40
40
  - Inbox alert written via `dispatch.writeInboxAlert('live-checkout-dirty-<wi-id>', body)`. The body lists the dirty files verbatim from `git status --porcelain`.
41
41
  - Work item stamped with `_pendingReason: 'live_checkout_dirty'` so the dashboard surfaces the block.
42
42
  - Completion summary: `live-checkout refused: N dirty file(s) in <localPath>`.
43
43
 
44
- 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.)
44
+ Outside the explicit auto-stash and auto-reset policies, the engine never resets, cleans, or stashes the operator checkout. Dispatch-scoped cleanup paths are no-ops because `worktreePath` stays null, and periodic worktree GC filters live-checkout projects out entirely.
45
45
 
46
- #### 2b. Opt-in auto-stash on dirty (W-mqtvnnj1000357fa)
46
+ #### 2b. Auto-stash on dirty (W-mqtvnnj1000357fa)
47
47
 
48
- 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:
48
+ Auto-stash is **ON by default**. The engine stashes dirty changes so dispatch can proceed without manual intervention; operators can opt out per project or fleet-wide:
49
49
 
50
- - **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**).
50
+ - Configure via per-project `project.liveCheckoutAutoStash` (takes priority) or fleet-wide `engine.liveCheckoutAutoStash` (default `true`). Both are surfaced in Settings.
51
51
  - 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**.
52
52
  - 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`.
53
53
  - 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.
@@ -61,25 +61,26 @@ A thrown error from `prepareLiveCheckout` — a required-arg/ref-validation guar
61
61
 
62
62
  #### 2b. Opt-in auto-reset on dirty (`liveCheckoutAutoReset`, W-mqvejug6000eeb20)
63
63
 
64
- By default the dirty tree refusal (Guarantee 2) is terminal the engine never touches the operator's uncommitted work. **Opt in** to automatic recovery with `liveCheckoutAutoReset`. When enabled, a dirty live tree is **force-reset to the remote** instead of refusing the dispatch:
64
+ Auto-stash is the default non-destructive recovery. `liveCheckoutAutoReset` is an additional **opt-in, destructive fallback** used only when auto-stash is disabled, skipped, or fails and the tree remains dirty. It removes uncommitted WIP without changing the current branch or any commit:
65
65
 
66
66
  1. `prepareLiveCheckout` detects the dirty tree (same porcelain preflight as Guarantee 2).
67
67
  2. It resolves the effective flag via `shared.resolveLiveCheckoutAutoReset(project, engine)` — **per-project `project.liveCheckoutAutoReset` (boolean) wins**, else the fleet-wide `engine.liveCheckoutAutoReset`, else `false`. (engine.js does not thread project/engine config into the helper, so the production path self-resolves by loading `config.json` and matching the project by `localPath`; it **fails closed** to `false` on any config read error so a glitch can never silently trigger a destructive reset.)
68
- 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.
69
- 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`.
68
+ 3. Before mutation, `prepareLiveCheckout` refuses an in-progress merge/rebase/cherry-pick/revert/bisect or detached HEAD.
69
+ 4. If ON: `git reset --hard HEAD` discards tracked index/worktree changes and `git clean -fd` removes ordinary untracked files. The helper then **always re-runs the porcelain preflight once**, even when either command reports an error, because a failed command may have partially changed the tree. If the tree is verified clean, dispatch proceeds normally. Otherwise it returns the safe `{ ok:false, reason:'dirty' }` refusal with the actual remaining paths when available.
70
+ 5. Every destructive attempt writes a uniquely keyed `live-checkout-autoreset-<attempt>-<dispatch-or-WI-id>` inbox note recording each command outcome plus the before/after dirty paths, including partial failures.
70
71
 
71
- This is **DESTRUCTIVE** — it permanently discards the operator's uncommitted changes in the live checkout. As of W-mqzbbhn2 it was **ON by default**; as of **W-mrawgw4q000a6bff it is OFF by default again** (`ENGINE_DEFAULTS.liveCheckoutAutoReset: false`) — the destructive-by-default behavior caused unwanted data loss when an operator had legitimate local drift they expected the engine to leave alone. Set `liveCheckoutAutoReset: true` (per-project or fleet-wide) to opt back in to reset-to-remote recovery.
72
+ This is **DESTRUCTIVE** — tracked WIP and untracked files can be permanently unrecoverable. It does **not** fetch, switch branches, reset to a remote ref, or erase local commits; the stale-base guard still refuses a local base with unpushed committed drift. As of W-mqzbbhn2 it was **ON by default**; as of **W-mrawgw4q000a6bff it is OFF by default again** (`ENGINE_DEFAULTS.liveCheckoutAutoReset: false`). Set `liveCheckoutAutoReset: true` (per-project or fleet-wide) to opt back in.
72
73
 
73
74
  - **Fleet-wide:** Dashboard → Settings → `Live-checkout auto-reset (fleet-wide)` (`engine.liveCheckoutAutoReset`).
74
75
  - **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.)*
75
76
 
76
- **Precedence when both `liveCheckoutAutoStash` and `liveCheckoutAutoReset` are enabled (W-mrawgw4q000a6bff).** `engine.js spawnAgent` now tries the **non-destructive** auto-stash first: it resolves `liveCheckoutAutoStash` up front and, if enabled, calls the initial `prepareLiveCheckout` with `autoReset` forced `false` so the destructive reset never runs before stash gets a chance. `applyLiveCheckoutAutoStash` then attempts the stash (see [2b auto-stash](#2b-opt-in-auto-stash-on-dirty-w-mqtvnnj1000357fa) above) and its own internal re-preflight also forces `autoReset: false`. Only if the stash itself fails or was skipped (`outcome:'unchanged'`) **and** the tree is still confirmed dirty **and** `resolveLiveCheckoutAutoReset` resolves `true` does `spawnAgent` invoke `prepareLiveCheckout` a second time with `autoReset: true` as an explicit, last-resort destructive fallback. **Behavior change:** previously (reset-before-stash, reset ON by default) the common both-enabled case force-reset the branch to origin on every dirty dispatch; now the branch is only synced to origin when stash fails — the common case just parks the dirty changes non-destructively and leaves the branch where it was. There is deliberately **no automatic `git merge --ff-only origin/<branch>`** after a successful stash — auto-stash's contract is "park changes so dispatch proceeds," not "sync to origin"; that stronger guarantee remains `liveCheckoutAutoReset`'s job, available as an explicit opt-in fallback.
77
+ **Precedence when both `liveCheckoutAutoStash` and `liveCheckoutAutoReset` are enabled (W-mrawgw4q000a6bff).** `engine.js spawnAgent` tries the **non-destructive** auto-stash first: it resolves `liveCheckoutAutoStash` up front and, if enabled, calls the initial `prepareLiveCheckout` with `autoReset` forced `false`. `applyLiveCheckoutAutoStash` attempts the stash and its own internal re-preflight also forces `autoReset: false`. Only if the stash fails or is skipped (`outcome:'unchanged'`), the tree remains dirty, and `resolveLiveCheckoutAutoReset` resolves `true` does `spawnAgent` invoke `prepareLiveCheckout` again with `autoReset: true`. Neither recovery policy syncs the branch to origin.
77
78
 
78
79
  #### 2c. Stale `.git/index.lock` self-heal (W-mr3tayu4)
79
80
 
80
81
  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.
81
82
 
82
- ### 3. No auto-pull, no `--force`, no fast-forward
83
+ ### 3. No implicit remote sync or force checkout
83
84
 
84
85
  After the clean-tree check, `prepareLiveCheckout`:
85
86
 
@@ -247,7 +248,7 @@ This mirrors, in CC-instruction form, what `prepareLiveCheckout` enforces engine
247
248
 
248
249
  1. Open the Minions dashboard → **Settings** → **Projects** → expand the target project.
249
250
  2. Set **Checkout mode** → **Live checkout**. A yellow warning chip appears immediately:
250
- > ⚠ Live mode: dispatches run directly in this repo's checkout. Only one mutating dispatch runs at a time. Dirty working trees block dispatch — commit or stash before running.
251
+ > ⚠ Live mode: dispatches run directly in this repo's checkout. Only one mutating dispatch runs at a time. Dirty-tree behavior follows the auto-stash/reset settings.
251
252
  3. Click **Save**. The dashboard POSTs the change through `mergeSettingsConfigUpdate`; `shared.validateCheckoutMode` rejects anything other than `'worktree'` or `'live'` with HTTP 400 (the legacy `'isolated'` value is silently coerced to `'worktree'`).
252
253
 
253
254
  ### Enabling live mode (config.json)
@@ -258,10 +259,9 @@ This mirrors, in CC-instruction form, what `prepareLiveCheckout` enforces engine
258
259
  "name": "android-aosp",
259
260
  "localPath": "/home/yemi/aosp",
260
261
  "checkoutMode": "live",
261
- // "liveCheckoutAutoReset": true, // OPT-IN, DESTRUCTIVE — on a dirty tree,
262
- // git fetch origin + git reset --hard origin/<branch> instead of refusing
263
- // (discards uncommitted operator changes; logs a live-checkout-autoreset
264
- // inbox note). Overrides the fleet-wide engine.liveCheckoutAutoReset.
262
+ // "liveCheckoutAutoReset": true, // OPT-IN, DESTRUCTIVE fallback if
263
+ // auto-stash is disabled/fails, reset tracked WIP to HEAD and clean
264
+ // untracked files (preserves branch + commits; logs an inbox note).
265
265
  // …
266
266
  }]
267
267
  }
@@ -269,11 +269,30 @@ This mirrors, in CC-instruction form, what `prepareLiveCheckout` enforces engine
269
269
 
270
270
  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`.
271
271
 
272
+ ### Enabling hybrid validation
273
+
274
+ Hybrid mode routes only selected canonical work-item types on the live checkout; every unselected type uses an isolated worktree. The normal configuration selects `test` and optionally creates one PR-targeted validation WI automatically:
275
+
276
+ ```jsonc
277
+ {
278
+ "projects": [{
279
+ "name": "android-aosp",
280
+ "checkoutMode": "live",
281
+ "liveValidation": {
282
+ "type": "test",
283
+ "autoDispatch": true
284
+ }
285
+ }]
286
+ }
287
+ ```
288
+
289
+ `type` may be a string or array, but every entry must be a live-capable canonical `WORK_TYPE`. `build-and-test` is a playbook alias and is normalized to `test` only for backward compatibility. `autoDispatch` must be boolean and requires `test` in the configured types; the generated WI has type `test`, carries structured `targetPr` / `pr_id` / URL-reference fields, targets the source PR branch, and renders the `build-and-test` playbook. Successful direct PR-backed `fix` and `build-fix-complex` dispatches, including human-feedback fixes, use the dispatch ID as their dedup key and do not add a nonexistent WI dependency.
290
+
272
291
  ### Enabling auto-stash on dirty (config.json)
273
292
 
274
293
  ```jsonc
275
294
  {
276
- // Fleet-wide fallback (default false) — applies to every live project that
295
+ // Fleet-wide fallback (default true) — applies to every live project that
277
296
  // doesn't set its own override.
278
297
  "engine": { "liveCheckoutAutoStash": true },
279
298
  "projects": [{
@@ -287,11 +306,11 @@ Absent / `null` / `''` reads as `'worktree'` (the default) — explicit is prefe
287
306
  }
288
307
  ```
289
308
 
290
- 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.
309
+ With auto-stash on, a dirty tree is `git stash push --include-untracked`'d before dispatch instead of refusing (see [2b](#2b-auto-stash-on-dirty-w-mqtvnnj1000357fa)). The engine never pops the stash — recover with `git stash pop` manually.
291
310
 
292
311
  ### Recovering from `live_checkout_dirty` refusal
293
312
 
294
- 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:
313
+ 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. Enable auto-stash for non-destructive automatic recovery; enable [auto-reset](#2b-opt-in-auto-reset-on-dirty-livecheckoutautoreset-w-mqvejug6000eeb20) only as a destructive fallback. To recover manually from the project checkout:
295
314
 
296
315
  ```bash
297
316
  # Option A — preserve work for later
@@ -305,7 +324,7 @@ git checkout -- .
305
324
  git clean -fd
306
325
  ```
307
326
 
308
- Then re-dispatch. The next tick will re-pick the pending work item; nothing else needs clearing on the engine side (the inbox alert is informational only).
327
+ The next tick automatically re-picks the pending work item; nothing needs clearing on the engine side.
309
328
 
310
329
  ### Branch-leak mitigation
311
330
 
@@ -344,9 +363,9 @@ Live-checkout mode is deliberately small. These are NOT supported and will not b
344
363
  | `engine/shared.js` — `CHECKOUT_MODES`, `validateCheckoutMode`, `resolveCheckoutMode`, `isLiveCheckoutProject` | Enum + validator + back-compat resolver (P-a3f9b201; consolidated W-mqiaw974). |
345
364
  | `engine/shared.js` — `resolveLiveCheckoutAutoReset` + `ENGINE_DEFAULTS.liveCheckoutAutoReset` | Pure precedence resolver (per-project boolean > fleet-wide engine default > false) + the fleet-wide default (OFF as of W-mrawgw4q000a6bff — was ON as of W-mqzbbhn2). Gates the dirty-tree auto-reset in `prepareLiveCheckout` (W-mqvejug6000eeb20); in `engine.js spawnAgent` only fires as a fallback after auto-stash fails/is disabled (W-mrawgw4q000a6bff). |
346
365
  | `engine/shared.js` — `resolveSpawnPaths` | Returns `{ cwd: localPath, worktreeRootDir: null, liveMode: true }` for live projects (P-a3f9b202). |
347
- | `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). |
366
+ | `engine/live-checkout.js` — `prepareLiveCheckout` | Pure helper by default: pre-mutation mid-operation / detached-HEAD preflight, original-ref capture, dirty check, **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**, and opt-in destructive WIP cleanup (`reset --hard HEAD` + `clean -fd` + re-check + audit note) that preserves the branch and committed history. Git probes use a **50 MB maxBuffer**. |
348
367
  | `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). |
349
- | `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 (stashre-preflight → clear retry stamp → operator inbox note) returning a `{ outcome:'unchanged'\|'stashed'\|'threw' }` discriminant. Engine never auto-pops (W-mqtvnnj1000357fa). |
368
+ | `engine/live-checkout.js` — `resolveLiveCheckoutAutoStash`, `performLiveCheckoutAutoStash`, `applyLiveCheckoutAutoStash` | Auto-stash resolver (per-project boolean, then fleet setting, then default true) + `git stash push --include-untracked` runner + stash/re-preflight orchestration. Engine never auto-pops (W-mqtvnnj1000357fa). |
350
369
  | `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). |
351
370
  | `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). |
352
371
  | `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). |
@@ -172,7 +172,7 @@ Single quotes (`'…'`) work the same way. No `powershell -Command "& '<path>' <
172
172
  ┌───────────────────────────────────────────────────────────────────┐
173
173
  │ 3. spawnManagedSpec(spec) per spec │
174
174
  │ - Uses the proven Windows detached-spawn pattern │
175
- │ - Records each in engine/managed-processes.json (locked write)
175
+ │ - Records each in the SQL managed-process store
176
176
  │ - Stdio → engine/managed-logs/<name>.log (append fd, NOT pipe) │
177
177
  └───────────────────────────────────┬───────────────────────────────┘
178
178
 
@@ -241,7 +241,7 @@ The list endpoint returns an ETag; pass it as `If-None-Match` on follow-up polls
241
241
  | Kill (PID terminated, removed from state) | `POST /api/managed-processes/kill` `{"name":"…"}` | Per-row "Kill" button |
242
242
  | Restart (kill old PID, respawn from saved state, kick first healthcheck) | `POST /api/managed-processes/restart` `{"name":"…"}` | Per-row "Restart" button |
243
243
 
244
- Killing a spec from outside Minions (raw `Stop-Process`) leaves a stale row in `managed-processes.json` until the next 30-tick sweep notices the dead PID. Prefer the API.
244
+ Killing a spec from outside Minions (raw `Stop-Process`) leaves a stale SQL row until the next 30-tick sweep notices the dead PID. Prefer the API.
245
245
 
246
246
  ### View logs
247
247
 
@@ -282,9 +282,9 @@ All knobs live under `engine.managedSpawn` in `engine/shared.js` (`ENGINE_DEFAUL
282
282
 
283
283
  ## Performance budget
284
284
 
285
- - **Per-tick contribution**: zero unless the 30-tick sweep fires. The sweep is `O(N)` over specs in `managed-processes.json` with one `process.kill(pid, 0)` per spec.
285
+ - **Per-tick contribution**: zero unless the 30-tick sweep fires. The sweep is `O(N)` over managed specs with one `process.kill(pid, 0)` per spec.
286
286
  - **Healthcheck loops**: per-spec, self-scheduled (not tick-coupled). 10 specs × 1s pre-healthy cadence ⇒ 10 probes/s peak. Post-healthy drops to one probe per `healthBackoffSec` (30s).
287
- - **Lock contention** on `managed-processes.json`: writes batch on transitions (healthy ↔ unhealthy), and `last_health_at` only persists every `healthBackoffSec` to avoid flap. See [`test/perf/managed-spawn-load.test.js`](../test/perf/managed-spawn-load.test.js) for the 10-spec / 3-project load assertion.
287
+ - **Write contention**: SQL updates batch on transitions (healthy ↔ unhealthy), and `last_health_at` only persists every `healthBackoffSec` to avoid flap. See [`test/perf/managed-spawn-load.test.js`](../test/perf/managed-spawn-load.test.js) for the 10-spec / 3-project load assertion.
288
288
  - **Hard cap recommendation**: 50 specs across one engine. Above that, `process.kill(pid, 0)` shells out enough on Windows to dent tick latency.
289
289
 
290
290
  ## Source map
@@ -30,8 +30,7 @@ minions config set-cli claude
30
30
  ```
31
31
 
32
32
  `minions init` creates `~/.minions/`. Primary migrated runtime state lives in
33
- `engine/state.db` (SQLite, WAL mode). Files such as `engine/dispatch.json` and
34
- `engine/metrics.json` are passive diagnostic mirrors; `engine/control.json`
33
+ `engine/state.db` (SQLite, WAL mode). Retired JSON mirrors are removed; `engine/control.json`
35
34
  remains a live JSON control file.
36
35
 
37
36
  ## 2. Link a project
@@ -66,7 +66,7 @@ playbook contract. The poller is unchanged; the carve-out is purely additive.
66
66
  - Validates `meta.pr_followup` shape (all four fields required, ≤ 512
67
67
  chars each). Malformed → `400 { error: "<message>" }`.
68
68
  - Checks for an existing follow-up with the same `parent_comment_id` in
69
- the target project's `work-items.json`. Duplicate → `409 {
69
+ the target project's SQL work-item scope. Duplicate → `409 {
70
70
  error: "followup_already_dispatched", existingWiId: "W-…" }` (no second WI
71
71
  created).
72
72
  - Persists the validated `meta.pr_followup`, and stores
@@ -104,7 +104,7 @@ playbook contract. The poller is unchanged; the carve-out is purely additive.
104
104
  ```
105
105
  `engine/lifecycle.js processCompletionFollowups` logs each entry at `info`
106
106
  and warns if the claimed `wi_id` is missing from any project's
107
- `work-items.json` (catches reverted or non-landing dispatches).
107
+ SQL work-item store (catches reverted or non-landing dispatches).
108
108
 
109
109
  ## Server contract reference
110
110
 
@@ -131,7 +131,7 @@ Any missing/empty field, oversized field, or non-string value → `400` with a
131
131
  descriptive message.
132
132
 
133
133
  **Dedup.** A second `POST /api/work-items` with the same
134
- `meta.pr_followup.parent_comment_id` in the same project's `work-items.json`
134
+ `meta.pr_followup.parent_comment_id` in the same project's SQL scope
135
135
  returns `409 { error: "followup_already_dispatched", existingWiId: "<id>" }`.
136
136
  This is checked under the same `mutateWorkItems` lock used to insert the new
137
137
  WI, so two pollers racing on the same comment cannot both win.
@@ -5,7 +5,7 @@ How the engine manages the lifecycle of a PR from creation through review, fix,
5
5
  ## 1. Implement agent creates PR
6
6
 
7
7
  - Agent pushes code, output contains PR URL
8
- - `syncPrsFromOutput()` (lifecycle.js) extracts URL via regex, creates `pull-requests.json` entry, links to work item via `addPrLink()`
8
+ - `syncPrsFromOutput()` (lifecycle.js) extracts the URL, creates a SQL PR record, and links it to the work item via `addPrLink()`
9
9
 
10
10
  ## 2. Engine discovers PR needs review
11
11
 
@@ -87,8 +87,7 @@ additive.
87
87
  ### Persistence (survives restart)
88
88
 
89
89
  - Dispatch state lives in **SQLite** `engine/state.db` `dispatches` table via
90
- `engine/dispatch-store.js` (`readDispatchSectioned` / `applyDispatchMutation`),
91
- mirrored read-only to `dispatch.json` (source: `engine/dispatch-store.js:49,244,298`).
90
+ `engine/dispatch-store.js` (`readDispatchSectioned` / `applyDispatchMutation`).
92
91
  Each active dispatch is one JSON record. **Because `liveProjectsInUse` is
93
92
  rebuilt from `dispatch.active` every tick, recording the claimed enlistment
94
93
  path on the active dispatch record makes the per-path claim automatically
@@ -9,7 +9,7 @@ W-mpdad3mq000m53bb).
9
9
 
10
10
  ## Runbook location
11
11
 
12
- `qa-runbooks.json` (engine state, JSON list keyed by `id`). Each entry:
12
+ `<MINIONS_DIR>/projects/<project-name>/runbooks/<runbook-id>.json`. Each entry:
13
13
  `{ id, name, target, steps, expectedArtifacts, createdAt, createdBy }`.
14
14
  CRUD via `GET/POST /api/qa/runbooks` (POST returns the new runbook with
15
15
  engine-assigned `id`). `target` is a name from `/api/managed-processes` or
@@ -97,7 +97,7 @@ plain prose, the engine sets up a live target, an agent drafts a
97
97
  runner-native test file, and (with user approval) a second agent executes
98
98
  it. Sessions are a thin orchestration layer on top of the same
99
99
  `managed-spawn` + `qa-run-result.json` infrastructure that powers
100
- runbooks above — they reuse `qa-runs.json`, `engine/qa-artifacts/`, and
100
+ runbooks above — they reuse the SQL QA-run store, `engine/qa-artifacts/`, and
101
101
  the existing `engine/lifecycle.js#runPostCompletionHooks` qa-run sidecar
102
102
  hook. Surfaced on `/qa` (sessions card list above the runbooks/runs
103
103
  tables) and proxied by the Command Center natural-language shortcut.
@@ -161,7 +161,7 @@ routing default):
161
161
  **Multi-project fan-out (W-mpq6xqzj000606d0):** when `spec.projects`
162
162
  contains more than one project name, SETUP is fanned out — one work item
163
163
  per project, queued in parallel into each project's own
164
- `work-items.json`. The first project (`spec.projects[0]`) is the
164
+ the target project's SQL work-item scope. The first project (`spec.projects[0]`) is the
165
165
  **primary** (`meta.qaSession.primary === true`); the rest are
166
166
  **co-services** (`primary === false`). Each WI's
167
167
  `meta.qaSession.coServices` lists the co-service project names (only the
@@ -230,7 +230,7 @@ errors are mapped to HTTP via `_qaSessionsErrorToStatus`:
230
230
 
231
231
  ## File locations
232
232
 
233
- - **Session state**: `engine/qa-sessions.json` (single file, all projects,
233
+ - **Session state**: `engine/state.db` `qa_sessions` (all projects,
234
234
  capped at `QA_SESSIONS_MAX_RECORDS = 500` via `createSession`-time
235
235
  rotation by `createdAt`).
236
236
  - **Drafted test files**: `engine/qa-tests/<sessionId>/test.<ext>`. Created
@@ -11,7 +11,7 @@ Runbooks are per-project test plans. Each runbook is a single JSON file at:
11
11
  <MINIONS_DIR>/projects/<project-name>/runbooks/<runbook-id>.json
12
12
  ```
13
13
 
14
- This mirrors the `projects/<name>/pull-requests.json` precedent — anything
14
+ This follows the project-scoped PR-store precedent — anything
15
15
  scoped to a single project lives under its `projects/<name>/` state dir
16
16
  rather than a root-level `runbooks/` directory. Two reasons:
17
17
 
@@ -99,7 +99,7 @@ unlink).
99
99
 
100
100
  The deferred follow-up items (W-mpeiwz6k0005bf34-b/c/d) have since landed. Brief pointers — see [CLAUDE.md](../CLAUDE.md) → "QA validation runs" for the deep dive:
101
101
 
102
- - **Run dispatch + persistence** (`engine/qa-runs.js`): `POST /api/qa/runbooks/run` creates a `qa-runs.json` record with `status ∈ pending|dispatched|running|passed|failed|error` and dispatches a `qa-validate` work item against the runbook's `target`. Read via `GET /api/qa/runs?limit=N&status=...` and `GET /api/qa/runs/<id>`.
102
+ - **Run dispatch + persistence** (`engine/qa-runs.js`): `POST /api/qa/runbooks/run` creates a SQL QA-run record with `status ∈ pending|dispatched|running|passed|failed|error` and dispatches a `qa-validate` work item against the runbook's `target`. Read via `GET /api/qa/runs?limit=N&status=...` and `GET /api/qa/runs/<id>`.
103
103
  - **Artifact contract**: the `qa-validate` agent writes `agents/<id>/qa-run.json` before exit; the engine copies listed files into `engine/qa-artifacts/<runId>/` and serves them via `GET /api/qa/artifacts/<runId>/<file>` (path-traversal-gated, 403 on escape). Per-file size cap: `engine.qaArtifactsMaxBytes`.
104
104
  - **UI**: `/qa` dashboard page (`dashboard/pages/qa.html`, `dashboard/js/qa.js`) polls `GET /api/qa/runs` every 5s while active; auto-detects screenshots/videos/logs for inline preview.
105
105
  - **Playbook**: `playbooks/qa-validate.md` (routed via the synthetic `qa-validate` task-type in `routing.md`).
@@ -1,8 +1,11 @@
1
1
  # RFC: `completion.json` — Structured Agent Control-Plane Protocol
2
2
 
3
- > Author: Dallas (Engineer) | Date: 2026-04-27 | Status: **awaiting-approval**
3
+ > Author: Dallas (Engineer) | Date: 2026-04-27 | Status: **historical design record**
4
4
  > Plan: `minions-2026-04-27.json` | Plan item: `P-7a8b9c1d` (SEC-07 / C1)
5
5
  > Note: this RFC lives in `docs/` rather than `plans/` because the repo's `.gitignore` excludes `plans/`. The task description explicitly allowed "or similar".
6
+ > Storage references below describe the pre-SQL implementation evaluated by the
7
+ > RFC. Current completion processing writes one-shot sidecars and persists runtime
8
+ > records in `engine/state.db`.
6
9
 
7
10
  ## TL;DR
8
11
 
@@ -139,7 +139,7 @@ process or a pooled lease. Assignment strategy, in order:
139
139
  4. Otherwise the request queues FIFO until a `release()` or worker crash frees
140
140
  a slot.
141
141
 
142
- Dispatch records persist `item.pooled = true` on disk (`dispatch.json`), not
142
+ Dispatch records persist `item.pooled = true` in SQLite, not
143
143
  just the in-memory `activeProcesses._pooled` flag (which is wiped on engine
144
144
  restart) — the on-disk flag is what tells the restart-reattach path
145
145
  (`engine/cli.js`) that a live worker PID is a pooled lease, not a resumable
@@ -12,7 +12,7 @@ Agent completes task
12
12
  ├─ 1. Learnings Inbox → notes.md (all future agents see it)
13
13
  ├─ 2. Per-Agent History → history.md (agent sees its own past)
14
14
  ├─ 3. Review Feedback Loop → author gets reviewer's findings
15
- └─ 4. Quality Metrics → engine/metrics.json (tracks performance)
15
+ └─ 4. Quality Metrics → engine/state.db (tracks performance)
16
16
  ```
17
17
 
18
18
  ## 1. Learnings Inbox → notes.md
@@ -182,7 +182,7 @@ Human comments on PR with "@minions fix the error handling here"
182
182
 
183
183
  ## 5. Quality Metrics
184
184
 
185
- The engine tracks per-agent performance metrics in `engine/metrics.json`. Updated after every task completion and PR review.
185
+ The engine tracks per-agent performance metrics in the SQL metrics store. It updates them after every task completion and PR review.
186
186
 
187
187
  ### Metrics tracked
188
188
 
@@ -330,7 +330,7 @@ When a git merge or rebase produces conflicts in yarn.lock.
330
330
  | Stale flag auto-reset | 5 min (300,000ms) | `_consolidationInFlight` flag auto-resets after this duration to unblock future runs |
331
331
  | notes.md max size | 50KB | Trims to cap above this; overflow archived to `notes/archive/notes-overflow.md` (not discarded) |
332
332
  | Agent history entries | 20 | Max entries kept in history.md |
333
- | Metrics file | `engine/metrics.json` | Auto-created on first completion |
333
+ | Metrics store | `engine/state.db` (`metrics`) | Created by database migrations |
334
334
 
335
335
  ## Files
336
336
 
@@ -340,6 +340,5 @@ When a git merge or rebase produces conflicts in yarn.lock.
340
340
  | `notes/archive/*.md` | Archived inbox files | Engine (consolidation) |
341
341
  | `notes.md` | Accumulated team knowledge | Engine (consolidation) |
342
342
  | `agents/<name>/history.md` | Per-agent task history | Engine (post-completion) |
343
- | `engine/metrics.json` | Quality metrics per agent | Engine (post-completion + review) |
343
+ | `engine/state.db` `metrics` | Quality metrics per agent | Engine (post-completion + review) |
344
344
  | `notes/inbox/feedback-*.md` | Review feedback for authors | Engine (post-review) |
345
-
@@ -1,6 +1,8 @@
1
1
  # `engine/shared.js` / `engine/lifecycle.js` module-boundary audit
2
2
 
3
- **Status:** documentation only no function bodies were moved or edited as part of this audit.
3
+ **Status:** historical point-in-time audit. It predates the SQL-only state cutover cleanup;
4
+ removed JSON-routing helpers and old line numbers below are retained only as audit evidence, not
5
+ as the current API contract. Use `engine/shared.js` and `engine/lifecycle.js` for current symbols.
4
6
 
5
7
  **Scope:** `engine/shared.js` (9,877 lines, 338 top-level functions) and `engine/lifecycle.js`
6
8
  (6,988 lines, 121 top-level functions) as of this audit's HEAD commit. The PRD item that requested
@@ -81,7 +81,7 @@ shape:
81
81
  ```
82
82
 
83
83
  Plus `/api/events?since=…&limit=…` for paged reads. The engine already writes most of these
84
- moments to `engine/log.json` (the audit ring buffer); we just need to enrich them and expose
84
+ moments to the SQL log store; we just need to enrich them and expose
85
85
  them. The History panel becomes one fetch.
86
86
 
87
87
  **Cost.** ~1.5 days. The hard part is catching every event-emit site — dispatch start, dispatch
@@ -98,8 +98,7 @@ each lifecycle path emits an event.
98
98
  **Problem.** Carlos called out "Work Items" and "Pipelines" as conceptually overlapping in the
99
99
  existing UI. Looking at the data: a Pipeline is a sequence of stages where each stage *is*
100
100
  either a Work Item, a Meeting, or a Plan. So a Pipeline isn't really a separate concept — it's
101
- a Work Item with a stage list. Yet they have entirely separate stores
102
- (`engine/dispatch.json` vs `engine/pipeline-runs.json`), separate API surfaces
101
+ a Work Item with a stage list. Yet they have separate SQL tables, separate API surfaces
103
102
  (`/api/work-items*` vs `/api/pipelines*`), and separate UIs.
104
103
 
105
104
  **Proposal.** Don't merge the *storage* — that's a deep migration. Instead, introduce a
@@ -199,11 +198,11 @@ becomes a "freshness" badge on the Knowledge entry rather than a separate tab.
199
198
 
200
199
  **Problem.** Schedule = "fire on a cron pattern." Watch = "fire when a condition flips." They
201
200
  share most of the lifecycle (definition, fire history, pause/resume, expire/stopAfter). The
202
- two state files (`schedule-runs.json` / `watches.json`) and parallel CRUD APIs add maintenance
201
+ two SQL stores and parallel CRUD APIs add maintenance
203
202
  without adding real concepts.
204
203
 
205
- **Proposal.** Generalize to **Trigger**, with two `kind`s: `cron` and `event`. Backend stays
206
- in two files for now (avoid the migration), but the API and UI present them as one.
204
+ **Proposal.** Generalize to **Trigger**, with two `kind`s: `cron` and `event`. Backend tables
205
+ stay separate, but the API and UI present them as one.
207
206
 
208
207
  ```jsonc
209
208
  {