@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.
- package/dashboard/js/settings.js +2 -2
- package/dashboard.js +28 -21
- package/docs/README.md +4 -1
- package/docs/command-center.md +30 -1
- package/docs/constants.md +32 -0
- package/docs/deprecated.json +13 -0
- package/docs/design-state-storage.md +3 -1
- package/docs/keep-processes.md +47 -0
- package/engine/cli.js +35 -0
- package/engine/shared.js +84 -19
- package/package.json +1 -1
package/dashboard/js/settings.js
CHANGED
|
@@ -393,8 +393,8 @@ async function openSettings() {
|
|
|
393
393
|
'<h4>Inbox & 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 ??
|
|
397
|
-
settingsField('Status Meetings Retention', 'set-statusMeetingsRetentionDays', e.statusMeetingsRetentionDays ??
|
|
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 & 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
|
-
//
|
|
1836
|
-
// (
|
|
1837
|
-
//
|
|
1838
|
-
//
|
|
1839
|
-
//
|
|
1840
|
-
//
|
|
1841
|
-
//
|
|
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
|
-
//
|
|
1847
|
-
// (
|
|
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
|
|
1935
|
-
// largest /api/status slice
|
|
1936
|
-
//
|
|
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
|
-
//
|
|
1947
|
-
//
|
|
1948
|
-
//
|
|
1949
|
-
//
|
|
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.
|
package/docs/command-center.md
CHANGED
|
@@ -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).
|
|
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.
|
package/docs/deprecated.json
CHANGED
|
@@ -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:
|
|
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
|
-
//
|
|
2188
|
-
// /api/status workItems slice
|
|
2189
|
-
//
|
|
2190
|
-
//
|
|
2191
|
-
//
|
|
2192
|
-
//
|
|
2193
|
-
//
|
|
2194
|
-
|
|
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 —
|
|
2198
|
-
//
|
|
2199
|
-
//
|
|
2200
|
-
//
|
|
2201
|
-
//
|
|
2202
|
-
//
|
|
2203
|
-
//
|
|
2204
|
-
//
|
|
2205
|
-
//
|
|
2206
|
-
//
|
|
2207
|
-
|
|
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.
|
|
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"
|