@yemi33/minions 0.1.2083 → 0.1.2085

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.
@@ -393,8 +393,8 @@ async function openSettings() {
393
393
  '<h4>Inbox &amp; Status Retention</h4>' +
394
394
  '<div class="settings-grid-2">' +
395
395
  settingsField('Consolidation Threshold', 'set-inboxConsolidateThreshold', e.inboxConsolidateThreshold || 5, 'notes', 'Inbox notes before auto-consolidation') +
396
- settingsField('Status WorkItems Retention', 'set-statusWorkItemsRetentionDays', e.statusWorkItemsRetentionDays ?? 7, 'days', 'Trim done/failed/cancelled work items older than N days from the /api/status workItems slice (active items are always shipped). Cuts SPA payload from ~3MB to <500KB. Set to 0 to disable trimming (full list shipped, restoring legacy behavior).') +
397
- settingsField('Status Meetings Retention', 'set-statusMeetingsRetentionDays', e.statusMeetingsRetentionDays ?? 7, 'days', 'Trim completed/archived meetings older than N days from the /api/status meetings slice (active meetings are always shipped). Cuts SPA payload from ~4.3MB to <500KB. Detail modal still fetches full transcripts via /api/meetings/:id. Set to 0 to disable trimming (full list shipped, restoring legacy behavior).') +
396
+ settingsField('Status WorkItems Retention', 'set-statusWorkItemsRetentionDays', e.statusWorkItemsRetentionDays ?? 0, 'days', 'Optional age-based trim for done/failed/cancelled work items in the /api/status workItems slice (active items are always shipped). Default 0 = no trim — the slim projection that strips description/AC/references already cuts the payload by ~80%. Set to a positive integer to also drop terminal items older than N days.') +
397
+ settingsField('Status Meetings Retention', 'set-statusMeetingsRetentionDays', e.statusMeetingsRetentionDays ?? 0, 'days', 'Optional age-based trim for completed/archived meetings in the /api/status meetings slice (active meetings are always shipped). Default 0 = no trim — the slim projection that collapses findings/debate/transcript bodies to {agentId: true} sentinels already cuts the payload by ~80%. Set to a positive integer to also drop terminal meetings older than N days. Detail modal still fetches full bodies via /api/meetings/:id.') +
398
398
  settingsField('Version Check Interval', 'set-versionCheckInterval', e.versionCheckInterval || 3600000, 'ms', 'How often to check npm for updates (default: 1 hour)') +
399
399
  '</div>' +
400
400
  '<h4>Operator &amp; Comments</h4>' +
package/dashboard.js CHANGED
@@ -73,6 +73,11 @@ const TITLE_SUFFIX = IS_DEV_MODE ? ' [DEV]' : '';
73
73
 
74
74
  const PORT = parseInt(process.env.PORT || process.argv[2]) || 7331;
75
75
  let CONFIG = queries.getConfig();
76
+ // Mirror cli.js: clear persisted statusWorkItemsRetentionDays=7 + the matching
77
+ // meetings field (prior default 7) so the new default (0, no trim) reaches the
78
+ // /api/status slimmers without needing an engine bounce first.
79
+ try { shared.applyStatusWorkItemsRetentionMigration(CONFIG); } catch { /* best-effort */ }
80
+ try { shared.applyStatusMeetingsRetentionMigration(CONFIG); } catch { /* best-effort */ }
76
81
  let PROJECTS = _getProjects(CONFIG);
77
82
  const CONFIG_PATH = path.join(MINIONS_DIR, 'config.json');
78
83
  const PINNED_PATH = path.join(MINIONS_DIR, 'pinned.md');
@@ -95,6 +100,8 @@ function ensureConfiguredProjectStateFiles() {
95
100
 
96
101
  function reloadConfig() {
97
102
  CONFIG = queries.getConfig();
103
+ try { shared.applyStatusWorkItemsRetentionMigration(CONFIG); } catch { /* best-effort */ }
104
+ try { shared.applyStatusMeetingsRetentionMigration(CONFIG); } catch { /* best-effort */ }
98
105
  PROJECTS = _getProjects(CONFIG);
99
106
  ensureConfiguredProjectStateFiles();
100
107
  }
@@ -1832,19 +1839,19 @@ function _safeStatusSlice(name, fn, fallback) {
1832
1839
  }
1833
1840
 
1834
1841
  // ── /api/status workItems slimming (W-mphejzmj000718bf) ─────────────────────
1835
- // Strip down the workItems slice shipped on /api/status to (a) drop terminal
1836
- // (done/failed/cancelled) items older than engine.statusWorkItemsRetentionDays
1837
- // and (b) project each surviving item onto a narrow shape that omits the
1838
- // large free-text fields (description, full acceptanceCriteria, full
1839
- // references). The dashboard never renders description/AC/references-detail
1840
- // off the cached slice `wiRow` only needs counts + status/badge fields,
1841
- // and `openWorkItemDetail` fetches the full record on demand via
1842
- // GET /api/work-items/<id>. Live measurement at task time: 796 items / ~3MB
1843
- // → ~50–100 items / <500KB typical. Active items (pending/dispatched/queued)
1844
- // are ALWAYS kept regardless of age.
1842
+ // Project each work item onto a narrow shape that omits the large free-text
1843
+ // fields (description, full acceptanceCriteria, full references) before
1844
+ // shipping on /api/status. The dashboard never renders description/AC/
1845
+ // references-detail off the cached slice — `wiRow` only needs counts +
1846
+ // status/badge fields, and `openWorkItemDetail` fetches the full record on
1847
+ // demand via GET /api/work-items/<id>. This slim projection is the bulk of
1848
+ // the payload savings (~3MB <500KB typical) and runs unconditionally.
1845
1849
  //
1846
- // Set engine.statusWorkItemsRetentionDays = 0 to disable trimming entirely
1847
- // (returns the full list unchanged, restoring legacy behavior).
1850
+ // engine.statusWorkItemsRetentionDays is an optional second-tier filter that
1851
+ // drops terminal (done/failed/cancelled) items older than N days. Default 0
1852
+ // = no trim (legacy behavior, full list shipped). Set to a positive integer
1853
+ // to opt into the date-based trim. Active items (pending/dispatched/queued)
1854
+ // are ALWAYS kept regardless of age.
1848
1855
  const _ACTIVE_WI_STATUSES_FOR_STATUS = new Set(['pending', 'dispatched', 'queued', 'paused', 'decomposed']);
1849
1856
  const _TERMINAL_WI_STATUSES_FOR_STATUS = new Set(['done', 'failed', 'cancelled']);
1850
1857
  function _resolveStatusWorkItemsRetentionDays() {
@@ -1931,9 +1938,9 @@ function _slimWorkItemsForStatus(items) {
1931
1938
  }
1932
1939
 
1933
1940
  // ── /api/status meetings slimming (W-mphlrxx6000a8760) ──────────────────────
1934
- // Mirrors the workItems trim above (PR #2816). Meetings are the second
1935
- // largest /api/status slice after workItems — live measurement: 22 meetings
1936
- // / 4.3MB (60% of the 7.2MB payload). The list renderer in
1941
+ // Mirrors the workItems slim above (PR #2816). Meetings were the second
1942
+ // largest /api/status slice — live measurement: 22 meetings / 4.3MB
1943
+ // (60% of the 7.2MB payload) before slimming. The list renderer in
1937
1944
  // dashboard/js/render-meetings.js:renderMeetings only needs:
1938
1945
  // - id, title, status, round, participants, agenda(short), createdAt,
1939
1946
  // completedAt
@@ -1941,13 +1948,13 @@ function _slimWorkItemsForStatus(items) {
1941
1948
  // ✓/⏳/○ icon — `m.findings?.[p]` truthy check, line 48-50)
1942
1949
  // The detail modal calls `/api/meetings/:id` which serves the full record
1943
1950
  // (findings.content + debate.content + conclusion + transcript bodies), so
1944
- // dropping those from the slice is safe.
1951
+ // dropping those bodies from the slice is safe. The slim projection alone
1952
+ // delivers the bulk of the payload savings and always runs.
1945
1953
  //
1946
- // Active meetings (investigating/debating/concluding) are ALWAYS kept
1947
- // regardless of age. Terminal meetings (completed/archived) only survive
1948
- // if their completedAt/roundStartedAt/createdAt is within the window.
1949
- // Set engine.statusMeetingsRetentionDays = 0 to disable trimming entirely
1950
- // (returns the full list — but still slim-shaped — restoring legacy size).
1954
+ // engine.statusMeetingsRetentionDays is an optional second-tier filter that
1955
+ // drops terminal (completed/archived) meetings older than N days. Default 0
1956
+ // = no trim (legacy behavior, full list shipped — but still slim-shaped).
1957
+ // Active meetings (investigating/debating/concluding) are ALWAYS kept.
1951
1958
  const _ACTIVE_MEETING_STATUSES_FOR_STATUS = new Set(['investigating', 'debating', 'concluding']);
1952
1959
  const _TERMINAL_MEETING_STATUSES_FOR_STATUS = new Set(['completed', 'archived']);
1953
1960
  function _resolveStatusMeetingsRetentionDays() {
package/docs/README.md CHANGED
@@ -15,10 +15,13 @@ Architecture, design proposals, and lifecycle references for people working on t
15
15
 
16
16
  - [command-center.md](command-center.md) — Command Center (CC) chat panel: persistent Sonnet sessions, `--resume` semantics, system-prompt invalidation, and per-tab session storage.
17
17
  - [completion-reports.md](completion-reports.md) — Canonical schema for the per-spawn completion JSON: trust nonce, `failure_class` enum, `noop` semantics, `retryable` / `needs_rerun` shape, and the artifacts array.
18
+ - [constants.md](constants.md) — Cross-cutting status / type / condition constants (`WI_STATUS`, `WORK_TYPE`, `PR_STATUS`, `WATCH_CONDITION`, …) and the no-magic-strings invariant.
18
19
  - [constellation-bridge.md](constellation-bridge.md) — Read-only cross-repo bridge: `engine.constellationBridge.enabled` flag, marker-file contract, and the `minions bridge` subcommand for local debugging.
20
+ - [cooldown-merge-semantics.md](cooldown-merge-semantics.md) — Scoping deliverable defining merge semantics for `saveCooldowns` (longer-of TTL merge, key-level upserts, gitignored on-disk format).
19
21
  - [copilot-cli-schema.md](copilot-cli-schema.md) — Behavior and schema reference for the GitHub Copilot CLI adapter (capability flags, stdin vs `-p`, model discovery, effort levels).
20
- - [design-state-storage.md](design-state-storage.md) — Design proposal evaluating five database options for replacing Minions' file-based JSON state; recommends `node:sqlite` as the medium-term target.
22
+ - [design-state-storage.md](design-state-storage.md) — Design proposal evaluating five database options for replacing Minions' file-based JSON state; recommends `node:sqlite` as the medium-term target (accepted; implementation tracked in CHANGELOG.md Phases 0–7).
21
23
  - [kb-sweep.md](kb-sweep.md) — Knowledge-base consolidation sweep (hash dedup → LLM batch dedup/reclassify → per-entry compress) and the detached runner that keeps it alive across `minions restart`.
24
+ - [keep-processes.md](keep-processes.md) — `meta.keep_processes` sidecar contract: when to use it vs managed-spawn, sidecar schema, caps, and the [`engine/keep-process-sweep.js`](../engine/keep-process-sweep.js) lifecycle.
22
25
  - [managed-spawn.md](managed-spawn.md) — Engine-owned long-running services (managed-spawn primitive): sidecar schema, healthcheck examples, lifecycle, dashboard API, and the WI 1 (build) → WI 2 (test) chained-validation pattern.
23
26
  - [plan-lifecycle.md](plan-lifecycle.md) — Full plan pipeline from `/plan` through PRD materialization, dispatch with dependency gating, verify task, and human archive.
24
27
  - [pr-comment-followup.md](pr-comment-followup.md) — PR-comment follow-up dispatch contract: fix/review agents may spin off a new WI via `POST /api/work-items` with `meta.pr_followup` instead of broadening the current PR or rebutting the comment.
@@ -4,4 +4,33 @@ The Command Center (CC) is the dashboard's conversational chat panel. It opens f
4
4
 
5
5
  CC is intentionally a thin wrapper around the runtime CLI: state changes happen via `Bash`-tool `curl` calls to the dashboard's own REST API, not via parsed delimiter blocks. The end-to-end flow is `dashboard/js/command-center.js` `_ccDoSend()` → `POST /api/command-center` (or `/api/command-center/stream`) in `dashboard.js` (`handleCommandCenter`) → `engine/llm.js` `callLLM({ direct: true })` → claude/copilot CLI session persisted in `engine/cc-sessions.json`. Per-turn API mutations are correlated via the `X-CC-Turn-Id` header and surfaced as standalone `role='action'` chips rendered outside the assistant bubble (`_ccActionResultLine` + `addMsg('action', ...)`).
6
6
 
7
- For canonical detail (system prompt, session lifecycle, turn-ID surfacing pipeline, doc-chat integration, and CC API contract), read [`CLAUDE.md`](../CLAUDE.md) — see the **CC API Contract** and **Sessions** sections — and the source in [`dashboard/js/command-center.js`](../dashboard/js/command-center.js), [`dashboard.js`](../dashboard.js) (`handleCommandCenter`), and [`prompts/cc-system.md`](../prompts/cc-system.md). This pointer file exists so the docs index has an entry for "Command Center"; the implementation details are deliberately not duplicated here because they drift.
7
+ For canonical detail (system prompt, session lifecycle, turn-ID surfacing pipeline, doc-chat integration, and CC API contract), read [`CLAUDE.md`](../CLAUDE.md) — see the **CC API Contract** and **Sessions** sections — and the source in [`dashboard/js/command-center.js`](../dashboard/js/command-center.js), [`dashboard.js`](../dashboard.js) (`handleCommandCenter`), and [`prompts/cc-system.md`](../prompts/cc-system.md).
8
+
9
+ ## Error surfacing contract (W-mpmwxni2000c25c7-d)
10
+
11
+ Both CC and Doc-Chat emit a typed error envelope so the dashboard can render a red `.cc-error` bubble (role=alert) with a Retry button and stop the spinner immediately. Errors arrive in two shapes:
12
+
13
+ 1. **SSE mid-stream** (most common). `writeCcEvent` / `writeDocEvent` write the wire as `event: error\ndata: {…envelope…}\n\n` so consumers using `addEventListener('error', …)` see them as named events. The JSON payload still carries `type: 'error'` for clients that only read the `data:` line, so both parser strategies keep working.
14
+ 2. **Non-2xx POST response** (pre-stream — `readBody` guard, prototype-pollution rejection, etc.). The body is the same envelope as JSON; the dashboard's `if (!res.ok)` branch parses it and stashes it on the thrown Error as `_ccErrorEnvelope`, then renders the same red bubble.
15
+
16
+ Canonical envelope (`_buildCcErrorEnvelope` in `dashboard.js`):
17
+
18
+ ```json
19
+ { "type": "error",
20
+ "message": "Human-readable cause + remediation hint",
21
+ "code": "model-unavailable | auth-failure | context-limit | budget-exceeded | crash | cc-turn-timeout | worker-spawn-failed | acp-handshake-failed | worker-died",
22
+ "retryable": false,
23
+ "availableModels": ["gpt-4o", "gpt-5.4", "..."],
24
+ "runtime": "copilot"
25
+ }
26
+ ```
27
+
28
+ `code` is clamped to the allowlist (`CC_ERROR_CODES` constant); unknown codes collapse to `crash`. `retryable: true` is informational — there is **no auto-retry**; the dashboard always offers a manual Retry button instead. Auto-retrying these errors is a footgun because most are operator-fix categories (auth, budget, missing model) where re-spawning makes no progress.
29
+
30
+ **Watchdog (`engine.ccTurnTimeoutMs`, default 5 min, clamped 10s–1h).** Each turn arms a `setTimeout` that fires `event: error` with `code: 'cc-turn-timeout'`, aborts the in-flight LLM call, and ends the stream when no terminal event (`done`/`error`) arrives in time. Independent of `CC_CALL_TIMEOUT_MS` (the outer 1h hard ceiling); the watchdog is the *visible-to-user* no-progress cap. Surfaced in Settings → CC overrides.
31
+
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
+
34
+ ## Per-turn surfacing pipeline
35
+
36
+ CC handler generates `ccTurnId = 'cct-' + shared.uid()` per request; injected into sysprompt AND prompt body via `_ccTurnHeaderPart(turnId)` (load-bearing: on resumed sessions `engine/llm.js` skips re-sending the sysprompt, so without body injection CC keeps the stale turn ID). Handler reads via `_readCcTurnIdHeader(req)` and calls `_recordCcTurnCreation(turnId, ...)` on success. End-of-turn: `_buildSyntheticActionResultsForTurn` produces synthetic `{action, result}` pairs (`_serverExecuted: true`). Client renders as standalone `role='action'` messages outside the assistant bubble. TTL: 5 min. Endpoints wired: `/api/work-items`, `/api/notes`, `/api/plan`, `/api/knowledge`, `/api/watches`.
@@ -0,0 +1,32 @@
1
+ # Constants — No Magic Strings
2
+
3
+ All cross-cutting status / type / condition values are defined in [`engine/shared.js`](../engine/shared.js). Engine and dashboard code **never** compares against raw strings; tests enforce.
4
+
5
+ ```js
6
+ WI_STATUS = { PENDING, DISPATCHED, DONE, FAILED, PAUSED, QUEUED, DECOMPOSED, CANCELLED }
7
+ DONE_STATUSES = Set([WI_STATUS.DONE, 'in-pr', 'implemented', 'complete']) // legacy aliases on read only
8
+ WORK_TYPE = { IMPLEMENT, IMPLEMENT_LARGE, FIX, REVIEW, VERIFY, PLAN, PLAN_TO_PRD,
9
+ DECOMPOSE, MEETING, EXPLORE, ASK, TEST, DOCS, SETUP }
10
+ PLAN_STATUS = { ACTIVE, AWAITING_APPROVAL, APPROVED, PAUSED, REJECTED, COMPLETED, REVISION_REQUESTED }
11
+ PRD_ITEM_STATUS = { MISSING, UPDATED, DONE }; PRD_MATERIALIZABLE = Set([MISSING, UPDATED])
12
+ PR_STATUS = { ACTIVE, MERGED, ABANDONED, CLOSED, LINKED }; PR_POLLABLE_STATUSES = Set([ACTIVE, LINKED])
13
+ DISPATCH_RESULT = { SUCCESS, ERROR, TIMEOUT }
14
+ WATCH_STATUS = { ACTIVE, PAUSED, TRIGGERED, EXPIRED }
15
+ WATCH_CONDITION = { MERGED, BUILD_FAIL, BUILD_PASS, COMPLETED, FAILED, STATUS_CHANGE, ANY, NEW_COMMENTS,
16
+ VOTE_CHANGE, CONCLUDED, APPROVED, REJECTED, STAGE_COMPLETE, RAN, ENABLED, DISABLED, ACTIVITY_CHANGE,
17
+ HEAD_COMMIT_CHANGE, MERGEABLE_FLIPPED, READY_FOR_MERGE, BEHIND_MASTER, DRAFT_FLIPPED,
18
+ STALLED, RETRY_LIMIT_REACHED, DEPENDENCY_MET, ALL_ITEMS_DONE, ITEM_FAILED_N_TIMES,
19
+ STAGE_ADVANCED, STUCK_IN_STAGE }
20
+ WATCH_ABSOLUTE_CONDITIONS = Set([MERGED, BUILD_FAIL, BUILD_PASS, COMPLETED, FAILED, CONCLUDED, APPROVED,
21
+ REJECTED, READY_FOR_MERGE, RETRY_LIMIT_REACHED, ALL_ITEMS_DONE, ITEM_FAILED_N_TIMES]) // fire-once
22
+ ```
23
+
24
+ ## Engine defaults
25
+
26
+ Retry/timeout limits, sweep cadences, fleet ceilings, and managed-spawn caps live in `ENGINE_DEFAULTS` (also in `engine/shared.js`). Read defaults from there rather than re-declaring; per-deployment overrides go in `config.engine.*` and resolve through the per-knob helpers (`resolveAgentMaxBudget`, etc.).
27
+
28
+ ## Invariants
29
+
30
+ - **Write only `WI_STATUS.DONE`.** Legacy aliases (`in-pr`, `implemented`, `complete`) are accepted on read for backward compat but never written. `updateWorkItemStatus()` validates writes against `WI_STATUS`.
31
+ - **`mutateJsonFileLocked` for RMW.** All shared-JSON status writes go through the locked mutator wrappers (`mutateDispatch`, `mutateWorkItems`, `mutatePullRequests`) — see [`CLAUDE.md`](../CLAUDE.md) → **Concurrency**.
32
+ - **No string comparisons.** `pr.status === 'active'` ⇒ `pr.status === PR_STATUS.ACTIVE`. Source-inspection tests grep for the constant form.
@@ -1,4 +1,17 @@
1
1
  [
2
+ {
3
+ "id": "status-retention-stale-default-migration",
4
+ "description": "applyStatusWorkItemsRetentionMigration + applyStatusMeetingsRetentionMigration: one-time migrations that drop engine.statusWorkItemsRetentionDays=7 and engine.statusMeetingsRetentionDays=7 from loaded config so the new defaults (0, no trim) reach installs whose config.json was persisted while 7 was the baked-in default. The 7-day cutoffs were added alongside the slim projections (W-mphejzmj000718bf for workItems, W-mphlrxx6000a8760 for meetings) but surfaced as data loss to users (completed rows disappearing from /api/status after a week). The slim projections (which deliver the bulk of the payload savings) still run unconditionally; only the date filters were demoted to opt-in. shared.js mutates the in-memory config; engine/cli.js follows up with guarded shared.mutateJsonFileLocked calls on config.json so the fields are also removed on disk — affected installs are permanently corrected the first time the engine boots after this ships.",
5
+ "code": [
6
+ { "file": "engine/shared.js", "note": "applyStatusWorkItemsRetentionMigration / applyStatusMeetingsRetentionMigration definitions + _resetStaleRetentionMigrationFlag / _resetStaleMeetingsRetentionMigrationFlag test helpers — pure, in-memory" },
7
+ { "file": "engine/cli.js", "note": "Two boot blocks inside start() — each applies the in-memory pass THEN mutateJsonFileLocked on config.json to delete the field with skipWriteIfUnchanged so non-7 installs don't churn the file" },
8
+ { "file": "dashboard.js", "note": "Initial CONFIG load + reloadConfig() both apply the in-memory migrations so the dashboard picks up the new defaults even if it boots before the engine writes config.json" }
9
+ ],
10
+ "deprecated": "2026-05-29",
11
+ "targetRemovalDate": "2026-06-01",
12
+ "cleanup": "On or after 2026-06-01 (3 days), delete both migration functions + their reset helpers from engine/shared.js (including the shared.js export lines), the boot calls + mutateJsonFileLocked blocks in engine/cli.js, the call sites in dashboard.js, the tests in test/unit/status-workitems-retention.test.js and test/unit/status-meetings-retention.test.js gated on the function names, and this entry. Three days is enough because the on-disk rewrite happens on first engine boot — any install whose engine has rebooted at least once since this shipped has already had its config.json corrected and is no longer dependent on the in-memory shim.",
13
+ "notes": "Persistent custom values (any non-7 integer) are preserved untouched. The only at-risk users are those who explicitly want a 7-day window — they can re-set via the Settings page after removal. If a user never restarts the engine in the 3-day window, the shim still strips the persisted 7 on dashboard-only boot via the in-memory pass; the on-disk write is a hardening pass for the common case, not a strict requirement."
14
+ },
2
15
  {
3
16
  "id": "config-poll-key-migration",
4
17
  "location": "engine/queries.js:126-163",
@@ -1,6 +1,8 @@
1
1
  # Design: Replacing File-Based State with a Structured Database
2
2
 
3
- > Author: Rebecca (Architect) | Date: 2026-04-07 | Status: Proposal
3
+ > Author: Rebecca (Architect) | Date: 2026-04-07 | Status: **Accepted — implementation in progress**
4
+
5
+ > **Implementation status (as of 2026-05):** The `node:sqlite` recommendation in §3 has been adopted ahead of schedule. Phases 0–7 have shipped (events, dispatches, work_items, pull_requests, logs, metrics, watches, schedule_runs + pipeline_runs + managed_processes + worktree_pool — see `CHANGELOG.md`). The SQLite schema lives under `engine/db/migrations/` and the singleton opens `engine/state.db` in WAL mode. The "Phase 2: estimated Node 26 LTS" timeline in §3 is now historical context; treat sections 1–3 as design rationale rather than a forward plan.
4
6
 
5
7
  ## Executive Summary
6
8
 
@@ -0,0 +1,47 @@
1
+ # `keep_processes` sidecar
2
+
3
+ > Opt-in mechanism for agents that spawn detached children themselves and need to leave them running past their own exit. The companion to engine-owned [managed-spawn](managed-spawn.md).
4
+
5
+ ## When to use `keep_processes`
6
+
7
+ | Need | Use this |
8
+ |---|---|
9
+ | Long-running dev server / emulator the engine should own across restarts | [managed-spawn](managed-spawn.md) |
10
+ | Short-lived helper the *same* agent needs alive past exit (e.g. `gradle --daemon` for the next gradle invocation) | `meta.keep_processes` |
11
+ | Executable outside the managed-spawn allowlist | `meta.keep_processes` |
12
+ | Service with no healthcheck | `meta.keep_processes` |
13
+ | One-shot script that exits on its own | Neither |
14
+
15
+ Both can coexist on the same WI. There's no plan to deprecate `keep_processes`; a future cleanup PR can revisit if managed-spawn fully subsumes its use cases.
16
+
17
+ ## Sidecar contract
18
+
19
+ Set `meta.keep_processes: true` on the WI; agent writes `agents/<id>/keep-pids.json` before exit:
20
+
21
+ ```jsonc
22
+ {
23
+ "pids": [12345], // ≤5 entries
24
+ "purpose": "gradle daemon for follow-up test WI",
25
+ "cwd": "D:/repos/my-android-app", // MUST be a real git worktree
26
+ "ports": [8080], // advisory, engine doesn't bind
27
+ "expires_at": "2026-04-29T00:00:00Z", // TTL ≤1440 min; default 60
28
+ "written_by": "ripley",
29
+ "wi_id": "W-mpqtg16a0007f5bb"
30
+ }
31
+ ```
32
+
33
+ `buildKeepProcessesHint` bakes the Windows-correct detached-spawn pattern (`spawn(..., { detached: true, stdio: 'ignore' }).unref()` plus the `start /B` fallback on cmd-only shells) into the agent prompt automatically — agents should not hand-roll the detach call.
34
+
35
+ ## Lifecycle
36
+
37
+ - **Validation:** sidecar shape + workdir checked in `onAgentClose`. Failure → non-retryable `failure_class: invalid-keep-processes-{workdir,schema}`.
38
+ - **Sweep:** [`engine/keep-process-sweep.js`](../engine/keep-process-sweep.js) reaps at boot and every 30 ticks. Dead PIDs are pruned; expired TTL → process killed via `shared.killGracefully` then `killImmediate`.
39
+ - **No healthcheck:** unlike managed-spawn, `keep_processes` does not gate WI completion on first-healthy. If the child crashes immediately the agent still succeeds — by design (ad-hoc helper semantics).
40
+
41
+ ## Caps
42
+
43
+ - **PIDs:** ≤5 per sidecar.
44
+ - **TTL:** ≤1440 min (24h). Default 60.
45
+ - **Cwd:** must exist and resolve to inside a git worktree (`requireGitWorkdir: true`). Monorepo subdirs walk up to `gitWorktreeMaxParentDepth` (default 6).
46
+
47
+ See also: [`CLAUDE.md`](../CLAUDE.md) → **Agent Spawn**, [managed-spawn.md](managed-spawn.md).
package/engine/cli.js CHANGED
@@ -471,6 +471,41 @@ const commands = {
471
471
  try { shared.applyLegacyCcModelMigration(config, { logger: e.log }); }
472
472
  catch (err) { e.log('warn', `legacy ccModel migration failed: ${err.message}`); }
473
473
 
474
+ // Drop persisted statusWorkItemsRetentionDays=7 (the prior baked-in default)
475
+ // so the new default of 0 (no trim) reaches installs that opened Settings
476
+ // before the flip. Explicit non-7 values are preserved. We mutate in-memory
477
+ // AND rewrite config.json so the fix is permanent — the shim in shared.js
478
+ // can then retire on schedule without users regressing.
479
+ try {
480
+ const applied = shared.applyStatusWorkItemsRetentionMigration(config, { logger: e.log });
481
+ if (applied) {
482
+ const configPath = path.join(shared.MINIONS_DIR, 'config.json');
483
+ shared.mutateJsonFileLocked(configPath, (onDisk) => {
484
+ if (onDisk && onDisk.engine && onDisk.engine.statusWorkItemsRetentionDays === 7) {
485
+ delete onDisk.engine.statusWorkItemsRetentionDays;
486
+ }
487
+ return onDisk;
488
+ }, { defaultValue: {}, skipWriteIfUnchanged: true });
489
+ }
490
+ }
491
+ catch (err) { e.log('warn', `statusWorkItemsRetentionDays migration failed: ${err.message}`); }
492
+
493
+ // Same treatment for statusMeetingsRetentionDays — the meetings slice had
494
+ // the same 7-day baked-in default and the same data-loss UX.
495
+ try {
496
+ const applied = shared.applyStatusMeetingsRetentionMigration(config, { logger: e.log });
497
+ if (applied) {
498
+ const configPath = path.join(shared.MINIONS_DIR, 'config.json');
499
+ shared.mutateJsonFileLocked(configPath, (onDisk) => {
500
+ if (onDisk && onDisk.engine && onDisk.engine.statusMeetingsRetentionDays === 7) {
501
+ delete onDisk.engine.statusMeetingsRetentionDays;
502
+ }
503
+ return onDisk;
504
+ }, { defaultValue: {}, skipWriteIfUnchanged: true });
505
+ }
506
+ }
507
+ catch (err) { e.log('warn', `statusMeetingsRetentionDays migration failed: ${err.message}`); }
508
+
474
509
  // Auto-heal projects missing workSources (cloned-repo / hand-rolled-config
475
510
  // footgun): without this block, discoverFromWorkItems / discoverFromPrs
476
511
  // bail silently and the engine looks healthy but never dispatches. The
package/engine/shared.js CHANGED
@@ -2184,27 +2184,32 @@ const ENGINE_DEFAULTS = {
2184
2184
  // the override and falls back to auto-resolution.
2185
2185
  operatorLogin: null,
2186
2186
  // ── /api/status workItems retention (W-mphejzmj000718bf) ────────────────────
2187
- // Trim done/failed/cancelled work items older than this many days from the
2188
- // /api/status workItems slice to cut the SPA payload (live: 796 items / 3MB
2189
- // ~50–100 items / <500KB typical). Active items (pending/dispatched/queued)
2190
- // are ALWAYS shipped regardless of ageonly terminal items past the
2191
- // window are dropped. The detail modal fetches the full record on demand
2192
- // via GET /api/work-items/<id> when description/references/AC are needed.
2193
- // 0 disables the trim (full list shipped, restoring legacy behavior).
2194
- statusWorkItemsRetentionDays: 7,
2187
+ // Optional age-based trim for done/failed/cancelled work items in the
2188
+ // /api/status workItems slice. Default 0 = no trim (full list shipped). The
2189
+ // bulk of the payload savings (~3MB ~500KB) comes from _slimWorkItemForStatus
2190
+ // dropping description / full acceptanceCriteria / references that slim
2191
+ // projection runs unconditionally. The date filter on top was a second-tier
2192
+ // optimization that surfaced as data loss to users (completed items vanishing
2193
+ // from /api/status after 7 days) so it now opts in via a positive integer.
2194
+ // Active items (pending/dispatched/queued) are ALWAYS shipped regardless of
2195
+ // age. The detail modal fetches the full record on demand via
2196
+ // GET /api/work-items/<id> when description/references/AC are needed.
2197
+ statusWorkItemsRetentionDays: 0,
2195
2198
 
2196
2199
  // ── /api/status meetings retention (W-mphlrxx6000a8760) ─────────────────────
2197
- // Same shape as statusWorkItemsRetentionDays — mirrors the trim+slim pass
2198
- // for the meetings slice (live: 22 meetings / 4.3MB ~5 meetings / <500KB
2199
- // typical). Active meetings (investigating/debating/concluding) are ALWAYS
2200
- // shipped regardless of age only terminal meetings (completed/archived)
2201
- // past the window are dropped. The detail modal fetches the full record
2202
- // (findings, debate, conclusion, transcript bodies) on demand via
2203
- // GET /api/meetings/<id> when opened. A top-level meetingsTotal field is
2204
- // synthesized so the sidebar activity dot still fires when ANY meeting
2205
- // (including those dropped from the slim slice) gains a new round.
2206
- // 0 disables the trim (full list shipped, restoring legacy behavior).
2207
- statusMeetingsRetentionDays: 7,
2200
+ // Same shape as statusWorkItemsRetentionDays — optional age-based trim for
2201
+ // completed/archived meetings in the /api/status meetings slice. Default 0
2202
+ // = no trim (full list shipped). The slim projection (which collapses
2203
+ // ~95KB+ per-round findings/debate/transcript bodies down to {agentId: true}
2204
+ // sentinels) delivers the bulk of the payload savings and always runs.
2205
+ // The date filter on top was demoted to opt-in for the same reason as the
2206
+ // workItems trim: vanishing completed meetings read as data loss. Active
2207
+ // meetings (investigating/debating/concluding) are ALWAYS shipped regardless
2208
+ // of age. The detail modal fetches the full record (findings, debate,
2209
+ // conclusion, transcript bodies) on demand via GET /api/meetings/<id>.
2210
+ // A top-level meetingsTotal field is synthesized so the sidebar activity
2211
+ // dot still fires when ANY meeting gains a new round.
2212
+ statusMeetingsRetentionDays: 0,
2208
2213
  };
2209
2214
 
2210
2215
  // ─── Runtime Fleet Resolution (P-3b8e5f1d) ──────────────────────────────────
@@ -2401,6 +2406,64 @@ function _resetLegacyCcModelMigrationFlag() {
2401
2406
  _legacyCcModelMigrationLogged = false;
2402
2407
  }
2403
2408
 
2409
+ // ─── Stale statusWorkItemsRetentionDays Default Migration ────────────────────
2410
+ //
2411
+ // The retention default was 7 from W-mphejzmj000718bf until users reported the
2412
+ // trim hid completed work items from /api/status, which read as data loss.
2413
+ // We flipped the baked-in default to 0 (no trim). Installs that opened the
2414
+ // Settings page while the default was 7 have `engine.statusWorkItemsRetentionDays: 7`
2415
+ // persisted in their config.json — the resolver would return 7 and they'd
2416
+ // still see the trim. This shim drops a literal `7` at load time so the new
2417
+ // default of 0 applies. Operators who explicitly set a non-7 value (e.g. 14
2418
+ // or 30) are left untouched. No on-disk rewrite.
2419
+
2420
+ let _staleRetentionMigrationLogged = false;
2421
+
2422
+ function applyStatusWorkItemsRetentionMigration(config, { logger = log } = {}) {
2423
+ if (!config || !config.engine || typeof config.engine !== 'object') return false;
2424
+ const e = config.engine;
2425
+ if (e.statusWorkItemsRetentionDays !== 7) return false;
2426
+ delete e.statusWorkItemsRetentionDays;
2427
+ if (!_staleRetentionMigrationLogged) {
2428
+ _staleRetentionMigrationLogged = true;
2429
+ try {
2430
+ logger('warn', 'statusWorkItemsRetentionDays=7 was the previous default — clearing in-memory so the new default (0, no trim) applies. Re-save Settings to persist or set a positive value to opt back in.');
2431
+ } catch { /* logger may not be wired during tests — best-effort */ }
2432
+ }
2433
+ return true;
2434
+ }
2435
+
2436
+ /** Test helper: reset the dedup flag so repeated tests can re-trigger the log. */
2437
+ function _resetStaleRetentionMigrationFlag() {
2438
+ _staleRetentionMigrationLogged = false;
2439
+ }
2440
+
2441
+ // Same shape as applyStatusWorkItemsRetentionMigration above, for the meetings
2442
+ // slice. The prior baked-in default of 7 caused completed/archived meetings to
2443
+ // vanish from /api/status after a week; we flipped the default to 0 and strip
2444
+ // the literal 7 from persisted configs so the new behavior applies.
2445
+
2446
+ let _staleMeetingsRetentionMigrationLogged = false;
2447
+
2448
+ function applyStatusMeetingsRetentionMigration(config, { logger = log } = {}) {
2449
+ if (!config || !config.engine || typeof config.engine !== 'object') return false;
2450
+ const e = config.engine;
2451
+ if (e.statusMeetingsRetentionDays !== 7) return false;
2452
+ delete e.statusMeetingsRetentionDays;
2453
+ if (!_staleMeetingsRetentionMigrationLogged) {
2454
+ _staleMeetingsRetentionMigrationLogged = true;
2455
+ try {
2456
+ logger('warn', 'statusMeetingsRetentionDays=7 was the previous default — clearing in-memory so the new default (0, no trim) applies. Re-save Settings to persist or set a positive value to opt back in.');
2457
+ } catch { /* logger may not be wired during tests — best-effort */ }
2458
+ }
2459
+ return true;
2460
+ }
2461
+
2462
+ /** Test helper: reset the dedup flag so repeated tests can re-trigger the log. */
2463
+ function _resetStaleMeetingsRetentionMigrationFlag() {
2464
+ _staleMeetingsRetentionMigrationLogged = false;
2465
+ }
2466
+
2404
2467
  // ─── Runtime Config Preflight Warnings ──────────────────────────────────────
2405
2468
  //
2406
2469
  // Emit non-fatal warnings about runtime/CLI configuration drift. Consumed by
@@ -5454,6 +5517,8 @@ module.exports = {
5454
5517
  resolveAgentCli, resolveCcCli, resolveCcUseWorkerPool, resolveAgentModel, resolveCcModel,
5455
5518
  resolveAgentMaxBudget, resolveAgentBareMode,
5456
5519
  applyLegacyCcModelMigration, _resetLegacyCcModelMigrationFlag,
5520
+ applyStatusWorkItemsRetentionMigration, _resetStaleRetentionMigrationFlag,
5521
+ applyStatusMeetingsRetentionMigration, _resetStaleMeetingsRetentionMigrationFlag,
5457
5522
  runtimeConfigWarnings,
5458
5523
  projectWorkSourceWarnings,
5459
5524
  backfillProjectWorkSourceDefaults,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2083",
3
+ "version": "0.1.2085",
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"