@yemi33/minions 0.1.2143 → 0.1.2145

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 CHANGED
@@ -240,6 +240,27 @@ function removePinnedEntryLocked(title) {
240
240
  return !missing;
241
241
  }
242
242
 
243
+ // Edit a pinned note in place: replace the entry keyed on originalTitle with a
244
+ // fresh title/content/level block, atomically (single lock — never a remove+add
245
+ // race that could drop the note if the second write failed). Returns true if the
246
+ // original was found and replaced; false means it was appended as a new entry
247
+ // (upsert) so an edit never silently loses the content.
248
+ function updatePinnedEntryLocked({ originalTitle, title, content, level }, now = new Date()) {
249
+ const levelTag = level === 'critical' ? '🔴 ' : level === 'warning' ? '🟡 ' : '';
250
+ const block = '\n\n### ' + levelTag + title + '\n\n' + content + '\n\n*Pinned by human on ' + now.toISOString().slice(0, 10) + '*';
251
+ let found = false;
252
+ mutateTextFileLocked(PINNED_PATH, existing => {
253
+ const base = existing || PINNED_DEFAULT_CONTENT;
254
+ const regex = new RegExp('\\n\\n###\\s*(?:🔴\\s*|🟡\\s*)?' + originalTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\n[\\s\\S]*?(?=\\n\\n###|$)', 'i');
255
+ if (regex.test(base)) {
256
+ found = true;
257
+ return base.replace(regex, block);
258
+ }
259
+ return base + block;
260
+ }, { defaultValue: PINNED_DEFAULT_CONTENT });
261
+ return found;
262
+ }
263
+
243
264
  function setKbPinsLocked(pins) {
244
265
  return mutateJsonFileLocked(KB_PINS_PATH, () => pins, { defaultValue: [], skipWriteIfUnchanged: true });
245
266
  }
@@ -9786,7 +9807,18 @@ What would you like to discuss or change? When you're happy, say "approve" and I
9786
9807
  async function serveSlimUx(req, res) {
9787
9808
  try {
9788
9809
  const { buildSlimHtml } = require('./dashboard-build');
9789
- const html = buildSlimHtml()
9810
+ // Inject the same feature-flag bootstrap the classic dashboard ships so
9811
+ // Slim UX can gate client behavior (e.g. the slim-ux-promo welcome popup)
9812
+ // off window.MINIONS_FEATURES. Built per request so a flag toggle takes
9813
+ // effect on the next slim load with no restart.
9814
+ const slimFeaturesBoot = (() => {
9815
+ const boot = { flags: {}, defaults: {} };
9816
+ try {
9817
+ for (const f of features.listFeatures(CONFIG)) { boot.flags[f.id] = f.enabled; boot.defaults[f.id] = f.default; }
9818
+ } catch { /* keep empty bootstrap on registry error */ }
9819
+ return boot;
9820
+ })();
9821
+ const html = buildSlimHtml({ featuresJson: JSON.stringify(slimFeaturesBoot) })
9790
9822
  .replace(/\{\{favicon_emoji\}\}/g, FAVICON_EMOJI)
9791
9823
  .replace(/\{\{title_suffix\}\}/g, TITLE_SUFFIX);
9792
9824
  res.statusCode = 200;
@@ -11330,6 +11362,17 @@ What would you like to discuss or change? When you're happy, say "approve" and I
11330
11362
  invalidateStatusCache({ includeSlow: true });
11331
11363
  return jsonReply(res, 200, { ok: true });
11332
11364
  }},
11365
+ { method: 'POST', path: '/api/pinned/update', desc: 'Edit a pinned note (replace the entry keyed on originalTitle)', params: 'originalTitle, title, content, level?', handler: async (req, res) => {
11366
+ const body = await readBody(req);
11367
+ const originalTitle = (body.originalTitle || '').trim();
11368
+ const title = (body.title || '').trim();
11369
+ const { content, level } = body;
11370
+ if (!originalTitle || !title || !content) return jsonReply(res, 400, { error: 'originalTitle, title and content required' });
11371
+ const found = updatePinnedEntryLocked({ originalTitle, title, content, level });
11372
+ // pinned.md is in slow-state cache — opt-in invalidation so the edit is visible immediately
11373
+ invalidateStatusCache({ includeSlow: true });
11374
+ return jsonReply(res, 200, { ok: true, updated: found });
11375
+ }},
11333
11376
 
11334
11377
  // KB pin state (server-side so CC can pin items)
11335
11378
  { method: 'GET', path: '/api/kb-pins', desc: 'Get pinned KB item keys', handler: async (req, res) => {
@@ -0,0 +1,9 @@
1
+ diff a/docs/README.md b/docs/README.md (rejected hunks)
2
+ @@ -16,6 +16,7 @@ Architecture, design proposals, and lifecycle references for people working on t
3
+ - [command-center.md](command-center.md) — Command Center (CC) chat panel: persistent Sonnet sessions, `--resume` semantics, system-prompt invalidation, and per-tab session storage.
4
+ - [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.
5
+ - [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.
6
+ +- [constellation-style-telemetry.md](constellation-style-telemetry.md) — Feasibility study for a local-first usage/analytics layer modeled on Constellation's telemetry (typed event log + daily rollup + dashboard view), and why a 1:1 PostgreSQL/multi-tenant port is the wrong goal for Minions.
7
+ - [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).
8
+ - [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.
9
+ - [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`.
@@ -1,6 +1,6 @@
1
1
  # Auto-Discovery & Execution Pipeline
2
2
 
3
- > Last verified: 2026-05-28 against `engine.js` `tickInner()` and `routing.md`.
3
+ > Last verified: 2026-06-06 against `engine.js` `tickInner()` and `routing.md`.
4
4
 
5
5
  How the minions engine finds work and dispatches agents automatically.
6
6
 
@@ -16,7 +16,7 @@ tick()
16
16
  1c. meetingTimeouts() Advance round-based meetings whose timer fired
17
17
  2. consolidateInbox() Merge learnings into notes.md (Haiku-powered)
18
18
  2.1 autoSweepKb() Periodic KB sweep (opt-in via engine.autoConsolidateMemory, 4h cadence)
19
- 2.5 runCleanup() Periodic cleanup (every 10 ticks ≈ 10min)
19
+ 2.5 runCleanup() Periodic cleanup (every 60 ticks ≈ 10min)
20
20
  2.52 sweepKeepProcesses() keep_processes TTL/dead-PID sweep (every 180 ticks)
21
21
  2.53 sweepManagedSpawn() managed_spawn TTL/dead-PID/log-rotate sweep (every 180 ticks)
22
22
  2.55 checkWatches() Persistent watch jobs (every 18 tick-equivalents)
@@ -0,0 +1,161 @@
1
+ # Constellation-style telemetry for Minions — feasibility study
2
+
3
+ **Status:** Design proposal (not implemented). **Audience:** Contributors.
4
+ **Question answered:** Constellation (`~/code/constellation`) has a mature telemetry/usage
5
+ stack. Should Minions build something similar, and if so, what shape?
6
+
7
+ ## TL;DR
8
+
9
+ A **1:1 port is the wrong goal.** Constellation's telemetry is built for a *hosted,
10
+ multi-tenant, server-side fleet*: PostgreSQL storage, WebSocket ingestion,
11
+ per-constellation/per-device tenancy, and a bearer-authed global-admin dashboard.
12
+ Minions is the structural opposite — *single-machine, local-first, zero-deps-beyond-Node,
13
+ JSON-file state, loopback dashboard*.
14
+
15
+ But the **valuable, portable subset is very feasible**, and Minions already has ~60% of it.
16
+ The realistic target is a **local-first usage/analytics layer**: a typed append-only event
17
+ log + a retention/rollup discipline + a dedicated dashboard "Usage" page, optionally a
18
+ `/metrics` Prometheus endpoint. **No database, no new runtime dependencies, no new network
19
+ egress** (preserves the "nothing leaves the machine" property — Minions' own code has no
20
+ external telemetry today).
21
+
22
+ ## What Constellation does
23
+
24
+ Database-first, no external SaaS (no Kusto/App Insights/Sentry/Segment/Datadog). Events are
25
+ fire-and-forget INSERTs into PostgreSQL on the WebSocket hot path, spread across ~8 tables:
26
+
27
+ | Table | Retention | Role |
28
+ |---|---|---|
29
+ | `events` | 7d | Session/device/tool-call/auth lifecycle (`event_type` + JSON `payload`) |
30
+ | `sessions` | 30d | Session state + metadata |
31
+ | `execution_log` | 30d | Mission/agent runs: `status`, `cost_usd`, `duration_ms`, `error_category` |
32
+ | `session_launches` | 30d | Mission-wizard lifecycle + structured error capture (`error_code/category/message/stage`) |
33
+ | `app_events` | 30d | Cross-app analytics (e.g. PRism PR review/resolve) |
34
+ | `device_connection_log` | 90d | Per-day (device, agent_version) fingerprints |
35
+ | `metrics_daily` | **unlimited** | Rolled-up daily aggregates that survive retention |
36
+
37
+ Key design properties:
38
+
39
+ - **Rollup-before-delete invariant.** A 6-hourly scheduler (`packages/server/src/utils/retention.ts`
40
+ `startCleanupScheduler`) runs `runMetricsRollup()` (`packages/server/src/utils/metrics-rollup.ts`)
41
+ **before** `runDataCleanup()`. If the rollup throws, retention is skipped that tick — live rows
42
+ are never deleted before they're aggregated. A sentinel watermark row tracks the last fully
43
+ aggregated UTC date; today is excluded (live queries cover it).
44
+ - **Structured error taxonomy.** `WIZARD_ERROR_CATEGORIES` in `packages/shared/src/constants.ts`
45
+ (`validation`, `parse_error`, `agent_unavailable`, `agent_error`, `file_write`, `unknown_tool`,
46
+ `timeout`, `unknown`) drives failure dashboards.
47
+ - **Emit API.** Direct DB writes (`db.execute('INSERT INTO events ...')`) at ~60–70 call sites;
48
+ a typed `logAuditEvent()` helper (`packages/server/src/utils/audit.ts`) for security events;
49
+ a frontend `recordPrismEvent()` using `navigator.sendBeacon` →
50
+ `POST /api/v1/prism/telemetry` (`packages/server/src/api/prism.ts`).
51
+ - **Admin surface.** `GET /api/v1/global-admin/overview?window=24h|3d|7d|30d|90d|all`
52
+ (`packages/server/src/api/global-admin.ts`) returns windowed totals + daily time-series +
53
+ top-failing missions + recent wizard failures; queries live tables inside retention and
54
+ `metrics_daily` beyond it, merging by dimension. Separately, `GET /metrics`
55
+ (`packages/server/src/api/metrics.ts`) exposes Prometheus counters from an in-memory registry
56
+ (`terminal-metrics.ts`) — no Prometheus client library.
57
+ - **Privacy.** Path truncation + field redaction (`packages/shared/src/privacy.ts`), configurable
58
+ retention windows, no sampling.
59
+ - **Dependencies.** Only telemetry-relevant runtime dep is `pg`. No analytics SDKs anywhere.
60
+
61
+ ## What Minions already has (≈60%)
62
+
63
+ | Constellation concept | Minions equivalent today |
64
+ |---|---|
65
+ | `metrics_daily` rollups | `metrics.json._daily.<YYYY-MM-DD>` → `{costUsd, inputTokens, outputTokens, cacheRead, tasks}` |
66
+ | Per-entity aggregates | Per-agent counters: `tasksCompleted/Errored`, `prsCreated/Approved/Rejected/Merged`, `reviewsDone`, `totalCostUsd`, `totalInputTokens/OutputTokens/CacheRead`, `totalRuntimeMs`, `model`, `timedTasks` |
67
+ | App/engine analytics | `metrics.json._engine`: `command-center`, `agent-dispatch`, `consolidation`, `agent_memory_reconcile` |
68
+ | Emit helpers | `llm.trackEngineUsage()`, `trackReviewMetric()`, `updateMetrics()` (`engine/lifecycle.js`, `engine/llm.js`, `engine/shared.js`) |
69
+ | Structured error taxonomy | `FAILURE_CLASS` constants (recorded per work-item; not yet aggregated as a metric) |
70
+ | Diagnostic stream | `log.json` ring buffer (2500, secret-redacted via `redactSecrets()`) + `engine/dashboard-diagnostics.log` |
71
+ | Admin overview query | `getMetrics()` (`engine/queries.js`) surfaced via `/api/status` |
72
+
73
+ ## The gap (the 40% worth building)
74
+
75
+ 1. **No typed, append-only *event log*.** Minions has aggregate *counters* and a generic
76
+ *diagnostic* log, but nothing like Constellation's `events`/`execution_log`/`session_launches`:
77
+ domain events with `event_type` + structured `payload` + timestamp that can be **sliced over
78
+ time** ("dispatches by type per day", "`failure_class` trend", "plan→PRD failures with full
79
+ error text"). This is the single biggest capability difference.
80
+ 2. **No dedicated usage/analytics *view*.** Dashboard pages are home/work/prs/plans/inbox/tools/
81
+ schedule/watches/pipelines/meetings + qa/engine. There is no cost trend, failure-category
82
+ breakdown, or throughput chart. `getMetrics()` data is collected but barely visualized.
83
+ 3. **No retention discipline on a structured event stream.** The `_daily` rollup exists, but there
84
+ is no domain-event log to retain/prune under the rollup-before-delete invariant.
85
+
86
+ ## Portability matrix
87
+
88
+ | Constellation component | Verdict | Rationale |
89
+ |---|---|---|
90
+ | Typed event taxonomy + error categories | **Port (adapt)** | Reuse existing `WORK_TYPE` / `FAILURE_CLASS`; high value, low cost |
91
+ | Append-only event log | **Port → JSONL** | `engine/events.jsonl` (one event/line, atomic append); matches zero-dep ethos, not a DB table |
92
+ | Daily rollup + **rollup-before-delete** | **Adopt the discipline** | Generalize existing `_daily` to also fold event counts / failure categories; prune JSONL after rollup |
93
+ | Retention windows | **Port (config knob)** | `engine.telemetryRetentionDays`; reuse an existing `cleanup` tick phase |
94
+ | Usage/analytics dashboard page | **Port (scaled down)** | New `dashboard/pages/usage.html` + renderer over `getMetrics()` + rollups; inline SVG, no chart lib |
95
+ | `GET /metrics` Prometheus endpoint | **Optional, easy** | ~40 lines, in-memory text exposition |
96
+ | PostgreSQL backend | **Drop** | Violates "zero deps beyond Node built-ins"; JSON/JSONL suffice at single-machine volume. (See `design-state-storage.md` — if a DB ever lands, `node:sqlite` is the candidate, and this log would migrate with it.) |
97
+ | Multi-tenant constellation/device model | **Drop** | Minions is one fleet on one machine |
98
+ | WebSocket ingestion + `sendBeacon` | **Drop / reuse existing** | Engine already observes agents directly; dashboard already POSTs diagnostics |
99
+ | Bearer-auth global-admin | **Drop** | Local loopback dashboard, no tenancy |
100
+
101
+ ## Recommended shape (phased, local-first)
102
+
103
+ ### Phase 1 — Event log + emit helper (small, ~½ day)
104
+ Add `engine/telemetry.js` exporting `recordEvent(type, payload)` that appends a single redacted
105
+ JSON line to `engine/events.jsonl`. Event types live in a `TELEMETRY_EVENT` constant set (no magic
106
+ strings, per repo convention). Reuse `shared.redactSecrets()`. Wire ~10–15 high-value sites already
107
+ on the tick path: dispatch start/done/fail (+`failure_class`), PR poll outcomes, plan→PRD,
108
+ review verdicts, CC turns. Gitignored runtime file (add to the State Files list in `CLAUDE.md`).
109
+
110
+ ### Phase 2 — Rollup + retention (small, ~½ day)
111
+ Extend the existing daily-rollup path so it also folds event counts + failure-category tallies into
112
+ `metrics.json._daily`. Prune `events.jsonl` lines older than `telemetryRetentionDays` **after** the
113
+ rollup write succeeds (Constellation's invariant). Slots into an existing periodic tick phase.
114
+ All `metrics.json` writes stay inside `mutateJsonFileLocked()`.
115
+
116
+ ### Phase 3 — Usage dashboard page (medium, ~1–1.5 days)
117
+ New sidebar page reading `getMetrics()` + `_daily`: cost/token trend, dispatches-by-type,
118
+ `failure_class` breakdown, per-agent throughput, CC usage. Inline SVG sparklines — **no chart
119
+ dependency**. Follow the fragment-assembly model (`dashboard/pages/usage.html` + a
120
+ `dashboard/js/render-usage.js`) and bump `RENDER_VERSIONS` per the render-cache rules. Add a
121
+ Settings toggle if gated behind a feature flag.
122
+
123
+ ### Phase 4 (optional) — `GET /metrics` (~2 hrs)
124
+ Prometheus text exposition of the same counters for external scrapers. Strict-CSP-exempt route like
125
+ the other HTML entry points; counters from an in-memory registry, no client library.
126
+
127
+ ## Effort & risk
128
+
129
+ - **Effort:** Phase 1 ~½ day · Phase 2 ~½ day · Phase 3 ~1–1.5 days · Phase 4 ~2 hrs.
130
+ **Total ≈ 2.5–3 days** for the full local-first system; **Phases 1–2 alone deliver the core
131
+ capability in ~1 day.**
132
+ - **Risk: low.** No new deps, no schema migrations, no network surface, no new external egress.
133
+ Watch-items: (a) JSONL append volume — bounded by retention + rollup; (b) `mutateJsonFileLocked`
134
+ concurrency rules for the rollup write; (c) source-inspection tests for any new dashboard page;
135
+ (d) CSP/route placement if Phase 4 ships.
136
+ - **Cross-repo:** lands on `yemi33` origin and auto-mirrors to `opg`; nothing here touches
137
+ enterprise-only files.
138
+
139
+ ## Honest caveats
140
+
141
+ - Constellation's *real* power is the **central warehouse aggregating many machines** — a SaaS
142
+ capability Minions structurally cannot replicate without a server + DB + tenancy, which would
143
+ break its design philosophy. If cross-machine fleet analytics is ever the actual goal, that is a
144
+ separate, much larger project (a hosted collector), not a port. (`constellation-bridge.md`
145
+ already defines a read-only cross-repo bridge; a telemetry collector would be a different beast.)
146
+ - Much of Constellation's table sprawl (`sessions`, `devices`, `app_events`) maps to things Minions
147
+ either doesn't have or already tracks in JSON. **Don't recreate tables 1:1** — port the *event-log
148
+ + rollup + view* pattern, not the schema.
149
+
150
+ ## References
151
+
152
+ - Constellation rollup: `packages/server/src/utils/metrics-rollup.ts`,
153
+ `packages/server/src/utils/retention.ts`
154
+ - Constellation emit/admin: `packages/server/src/utils/audit.ts`,
155
+ `packages/server/src/api/global-admin.ts`, `packages/server/src/api/metrics.ts`,
156
+ `packages/server/src/api/prism.ts`, `packages/dashboard/src/prism/prism-telemetry.ts`
157
+ - Constellation taxonomy/privacy: `packages/shared/src/constants.ts`,
158
+ `packages/shared/src/privacy.ts`
159
+ - Minions today: `engine/metrics.json`, `getMetrics()` (`engine/queries.js`),
160
+ `trackEngineUsage()` (`engine/llm.js`), `FAILURE_CLASS` (`engine/shared.js`),
161
+ related design doc `design-state-storage.md`.
@@ -1,6 +1,6 @@
1
1
  # Engine Restart & Agent Survival
2
2
 
3
- > Last verified: 2026-05-16 against `engine.js` (`engineRestartGraceUntil`, line 168) and `engine/shared.js` `ENGINE_DEFAULTS` (`restartGracePeriod: 1200000`, `heartbeatTimeout: 300000`, `agentTimeout: 18000000`).
3
+ > Last verified: 2026-06-06 against `engine.js` (`engineRestartGraceUntil`, line 177) and `engine/shared.js` `ENGINE_DEFAULTS` (`restartGracePeriod: 1200000`, `heartbeatTimeout: 300000`, `agentTimeout: 18000000`).
4
4
 
5
5
  ## The Problem
6
6
 
package/docs/kb-sweep.md CHANGED
@@ -37,7 +37,7 @@ The remaining survivors are sent to Claude Haiku in batches of `LLM_BATCH_SIZE =
37
37
 
38
38
  Each action archives the file via the same `_archiveKbFile()` helper used by Pass 1; reclassification rewrites the `category:` frontmatter line and moves the file into the new category directory (source: [`engine/kb-sweep.js:243-279`](../engine/kb-sweep.js#L243)).
39
39
 
40
- Reclassification targets are validated against `shared.KB_CATEGORIES` (`architecture`, `conventions`, `project-notes`, `build-reports`, `reviews` — source: [`engine/shared.js:1012`](../engine/shared.js#L1012)); unknown categories are silently dropped.
40
+ Reclassification targets are validated against `shared.KB_CATEGORIES` (`architecture`, `conventions`, `project-notes`, `build-reports`, `reviews` — source: [`engine/shared.js:2139`](../engine/shared.js#L2139)); unknown categories are silently dropped.
41
41
 
42
42
  If a batch returns invalid JSON or the runtime is unavailable, that batch is skipped with a warning and the rest of the sweep continues (source: [`engine/kb-sweep.js:139-151`](../engine/kb-sweep.js#L139)).
43
43
 
@@ -116,7 +116,7 @@ Memory still wins when present; the disk file is a fallback (source: [`engine/kb
116
116
 
117
117
  ## Automatic Periodic Sweep (opt-in)
118
118
 
119
- The engine tick loop can also auto-spawn the KB sweep without dashboard interaction. Gated by `engine.autoConsolidateMemory` (default `false` — source: [`engine/shared.js:1835`](../engine/shared.js#L1835)):
119
+ The engine tick loop can also auto-spawn the KB sweep without dashboard interaction. Gated by `engine.autoConsolidateMemory` (default `false` — source: [`engine/shared.js:2253`](../engine/shared.js#L2253)):
120
120
 
121
121
  - When `engine.autoConsolidateMemory: true`, every tick the engine consults `shouldAutoSweep()` from [`engine/kb-sweep.js`](../engine/kb-sweep.js) and, when the 4-hour cadence has elapsed since the last completion, calls `spawnSweepRunnerDetached()` to fire-and-forget a fresh `engine/kb-sweep-runner.js` process (source: [`engine.js`](../engine.js) tick step 2.1).
122
122
  - The inbox→`notes.md` consolidation runs every tick *regardless* of this flag via `consolidateInbox()`; `autoConsolidateMemory` controls **only** the heavier `knowledge/` sweep.
@@ -203,7 +203,7 @@ Killing a spec from outside Minions (raw `Stop-Process`) leaves a stale row in `
203
203
 
204
204
  ## Configuration
205
205
 
206
- All knobs live under `engine.managedSpawn` in `engine/shared.js:1500` (`ENGINE_DEFAULTS.managedSpawn`). Override per install via `config.json`:
206
+ All knobs live under `engine.managedSpawn` in `engine/shared.js:2494` (`ENGINE_DEFAULTS.managedSpawn`). Override per install via `config.json`:
207
207
 
208
208
  | Key | Default | Notes |
209
209
  |---|---|---|
package/docs/watches.md CHANGED
@@ -24,7 +24,7 @@ A watch is a small JSON record persisted to `engine/watches.json`. It binds:
24
24
 
25
25
  ## Lifecycle (`WATCH_STATUS`)
26
26
 
27
- Defined in `engine/shared.js:2523`:
27
+ Defined in `engine/shared.js:3163` (`WATCH_STATUS`):
28
28
 
29
29
  | Status | Meaning |
30
30
  |-------------|-------------------------------------------------------------------------|
@@ -37,10 +37,10 @@ Pause/resume flips the `status` field via `POST /api/watches/update` *(source: `
37
37
 
38
38
  ## Conditions (`WATCH_CONDITION`)
39
39
 
40
- Defined in `engine/shared.js:2539-2577`. Conditions split into two families:
40
+ Defined in `engine/shared.js:3179-3220` (`WATCH_CONDITION`). Conditions split into two families:
41
41
 
42
42
  ### Absolute conditions (`WATCH_ABSOLUTE_CONDITIONS`)
43
- *(source: `engine/shared.js:2586-2602`)*
43
+ *(source: `engine/shared.js:3226-3245`)*
44
44
 
45
45
  `merged`, `build-fail`, `build-pass`, `completed`, `failed`, `concluded`, `approved`, `rejected`, `ready-for-merge`, `retry-limit-reached`, `all-items-done`, `item-failed-n-times`.
46
46
 
@@ -49,12 +49,12 @@ When `stopAfter === 0`, these are **fire-once** — the engine flips the watch t
49
49
  > **Per-target override (W-mp7hg58e000b5212):** the global `WATCH_ABSOLUTE_CONDITIONS` set is the legacy fallback. Each target type now declares its own `absoluteConditions: [...]` array in its spec; `registerTargetType` normalizes that into a `Set` that takes precedence at evaluation time. The plugin contract (see below) uses this to keep absolute-vs-change semantics local to each target type. Plugins that omit `absoluteConditions` get an empty set (all change-based).
50
50
 
51
51
  ### Change-based conditions
52
- `status-change`, `any`, `new-comments`, `vote-change`, `stage-complete`, `ran`, `enabled`, `disabled`, `activity-change`, plus the predicate conditions added under P-w4e2f6a1 / P-w5b8d2c9 for the `pr`, `work-item`, `plan`, and `pipeline` target types (`head-commit-change`, `mergeable-flipped`, `behind-master`, `draft-flipped`, `stalled`, `dependency-met`, `stage-advanced`, `stuck-in-stage`). See `engine/shared.js:2539-2577` for the canonical enum.
52
+ `status-change`, `any`, `new-comments`, `vote-change`, `stage-complete`, `ran`, `enabled`, `disabled`, `activity-change`, plus the predicate conditions added under P-w4e2f6a1 / P-w5b8d2c9 for the `pr`, `work-item`, `plan`, and `pipeline` target types (`head-commit-change`, `mergeable-flipped`, `behind-master`, `draft-flipped`, `stalled`, `dependency-met`, `stage-advanced`, `stuck-in-stage`). See `engine/shared.js:3179-3220` for the canonical enum.
53
53
 
54
54
  These compare the live entity against the watch's `_lastState` snapshot and run forever when `stopAfter === 0`. Baseline `_lastState` is captured on the first check so the very next change triggers the watch *(source: `engine/watches.js:434, 520`)*.
55
55
 
56
56
  ### Tick-counted conditions
57
- `stalled`, `stuck-in-stage` — require N consecutive unchanged captures (default `WATCH_STALLED_DEFAULT_TICKS = 12`, `WATCH_STUCK_STAGE_DEFAULT_TICKS = 12`, both in `engine/shared.js:2582-2583`). Counters (`_unchangedTicks`, `_stuckStageTicks`) are recomputed inside `_captureState` by comparing the fresh snapshot against `prevState`.
57
+ `stalled`, `stuck-in-stage` — require N consecutive unchanged captures (default `WATCH_STALLED_DEFAULT_TICKS = 12`, `WATCH_STUCK_STAGE_DEFAULT_TICKS = 12`, both in `engine/shared.js:3222-3223`). Counters (`_unchangedTicks`, `_stuckStageTicks`) are recomputed inside `_captureState` by comparing the fresh snapshot against `prevState`.
58
58
 
59
59
  ### Predicate conditions
60
60
 
@@ -65,7 +65,7 @@ Several condition keys evaluate a derived predicate on the captured entity/state
65
65
  - **plan** — `all-items-done` (`items_done === items_total > 0`), `item-failed-n-times` (any `missing_features[*]._retryCount >= ENGINE_DEFAULTS.maxRetries`).
66
66
  - **pipeline** — `stage-advanced` (`current_stage_id` changed within the same `runId`), `stuck-in-stage` (current stage unchanged for `WATCH_STUCK_STAGE_DEFAULT_TICKS` checks, default 12).
67
67
 
68
- Compound state-assertion predicates (`ready-for-merge`, `retry-limit-reached`, `all-items-done`, `item-failed-n-times`) live in `WATCH_ABSOLUTE_CONDITIONS` so they fire-once when `stopAfter === 0` — without that they would re-fire every tick while the assertion holds *(source: `engine/shared.js:2586` `WATCH_ABSOLUTE_CONDITIONS`)*.
68
+ Compound state-assertion predicates (`ready-for-merge`, `retry-limit-reached`, `all-items-done`, `item-failed-n-times`) live in `WATCH_ABSOLUTE_CONDITIONS` so they fire-once when `stopAfter === 0` — without that they would re-fire every tick while the assertion holds *(source: `engine/shared.js:3226` `WATCH_ABSOLUTE_CONDITIONS`)*.
69
69
 
70
70
  ## Target Types — `TARGET_TYPES` Registry
71
71
 
@@ -89,7 +89,7 @@ Canonical example: `watches.d/http.js` (W-mp7i22mu00191b07) — a generic HTTP p
89
89
 
90
90
  ### Built-in target types
91
91
 
92
- The eight built-ins are registered at module load *(source: `engine/watches.js:672-1313`)*. Constants live at `engine/shared.js:2529-2538` (`WATCH_TARGET_TYPE`).
92
+ The eight built-ins are registered at module load *(source: `engine/watches.js:669-1268`)*. Constants live at `engine/shared.js:3169-3177` (`WATCH_TARGET_TYPE`).
93
93
 
94
94
  | `targetType` | Target value | Conditions | Notes |
95
95
  |---------------|--------------------------------------|----------------------------------------------------------------------------|-------|
@@ -133,7 +133,7 @@ Resolution is `path.join(shared.MINIONS_DIR, 'watches.d')` so it works in both d
133
133
 
134
134
  ## Tick Integration
135
135
 
136
- `engine.js` calls `checkWatches(config, state)` every `ENGINE_DEFAULTS.watchPollEvery` ticks (default 18 ⇒ ~3 min at the default 10s tick) inside its own `safe('checkWatches', ...)` block *(source: `engine.js:6386-6440`)*. The engine builds the state object from cached project files + module reads:
136
+ `engine.js` calls `checkWatches(config, state)` every `ENGINE_DEFAULTS.watchPollEvery` ticks (default 18 ⇒ ~3 min at the default 10s tick) inside its own `safe('checkWatches', ...)` block *(source: `engine.js:7439-7470`)*. The engine builds the state object from cached project files + module reads:
137
137
 
138
138
  ```
139
139
  {
@@ -175,7 +175,7 @@ I/O happens **outside the lock**: notifications via `writeToInbox`, follow-up ac
175
175
  | `resume-plan` | Set PRD `status=PLAN_STATUS.ACTIVE` and clear `planStale` |
176
176
  | `cc-triage` | Invoke Command Center headlessly via the loopback `POST /api/command-center/triage` endpoint with the trigger context (and optional completion-report / live-output artifacts). Wraps the prompt in `<UNTRUSTED-INPUT>`, uses a default 10-min timeout (capped at 1 h), and is isolated from the user CC session |
177
177
 
178
- Constants live in `WATCH_ACTION_TYPE` (`engine/shared.js:2608`); handlers in `engine/watch-actions.js`.
178
+ Constants live in `WATCH_ACTION_TYPE` (`engine/shared.js:3248`); handlers in `engine/watch-actions.js`.
179
179
 
180
180
  ### Templating
181
181
 
@@ -242,11 +242,11 @@ Absolute conditions firing under `stopAfter === 0` flip `status` to `expired`; `
242
242
  | Webhook action returns `"only http/https allowed"` | URLs must use `http://` or `https://` schemes; other protocols are rejected by design *(source: `engine/watch-actions.js` `WEBHOOK` handler)* |
243
243
  | Trigger fires but follow-up `dispatch-work-item` is missing | Check the engine log for `Watch <id> action <type>: <summary>`. Common reasons: missing `title`, the project's `work-items.json` couldn't be written, or the WI landed in central `work-items.json` because no project was specified |
244
244
  | Watch `_lastActionResult` shows `"timeout"` for webhook | Webhooks have a 10s safety timeout to keep the watches tick fast *(source: `engine/watch-actions.js:482-484`)* |
245
- | `checkWatches` block crashes silently | Wrapped in `safe('checkWatches', ...)` so one failure doesn't abort the tick *(source: `engine.js:6386`)*. Inspect `engine/log.json` for `Watch check error (<id>)` lines. Regression #1088: the block must use `getProjects(config)`, never the long-removed `PROJECTS` constant |
245
+ | `checkWatches` block crashes silently | Wrapped in `safe('checkWatches', ...)` so one failure doesn't abort the tick *(source: `engine.js:7443`)*. Inspect `engine/log.json` for `Watch check error (<id>)` lines. Regression #1088: the block must use `getProjects(config)`, never the long-removed `PROJECTS` constant |
246
246
 
247
247
  ## See Also
248
248
 
249
- - `engine/shared.js:2523-2618` — `WATCH_STATUS`, `WATCH_TARGET_TYPE`, `WATCH_CONDITION`, `WATCH_ABSOLUTE_CONDITIONS`, `WATCH_ACTION_TYPE` constants
249
+ - `engine/shared.js:3163-3275` — `WATCH_STATUS`, `WATCH_TARGET_TYPE`, `WATCH_CONDITION`, `WATCH_ABSOLUTE_CONDITIONS`, `WATCH_ACTION_TYPE` constants
250
250
  - `engine/watches.js` — registry, lifecycle, tick integration, `watches.d/` plugin loader
251
251
  - `engine/watch-actions.js` — action registry and built-in handlers (including `minions-api`)
252
252
  - `watches.d/http.js` — canonical user-extensible target type plugin
package/engine/cli.js CHANGED
@@ -159,17 +159,20 @@ function handleCommand(cmd, args) {
159
159
  if (!cmd) {
160
160
  return commands.start();
161
161
  } else if (commands[cmd]) {
162
- // W-mq07mjzi000s1cc9: Centralized help-flag interception.
162
+ // W-mq07mjzi000s1cc9 + W-mq0770160001ee50: Help-flag interception.
163
163
  //
164
- // `minions work --help` was creating ghost work items with title='--help'
164
+ // `minions work --help` used to create ghost work items with title='--help'
165
165
  // because the bare-string `title` was truthy and bypassed the `!title`
166
- // usage check. Same class of bug exists in `spawn`/`plan`/`complete`
167
- // every command that takes a positional arg and tests it with `if (!arg)`.
166
+ // usage check. The fix lives in per-command guards (`_isHelpArg` /
167
+ // `looksLikeFlagOrHelp`) on `work`/`spawn`/`plan`/`complete`, which print
168
+ // command-specific `Usage:` output. `pr` and `bridge` handle help inline.
168
169
  //
169
- // Intercept here so a single guard covers the whole command set. `pr` and
170
- // `bridge` already handle `help`/`--help`/`-h` inline (see their own
171
- // first-arg branches), so let them route through unchanged.
172
- if (cmd !== 'pr' && cmd !== 'bridge' && isHelpToken(args && args[0])) {
170
+ // For other commands (start/stop/status/etc.), fall back to printing the
171
+ // global command list they take no positional args so `--help` is
172
+ // otherwise harmless. Commands with per-command help handling are excluded
173
+ // so their `Usage:` text is what the user sees.
174
+ const COMMANDS_WITH_OWN_HELP = new Set(['pr', 'bridge', 'work', 'spawn', 'plan', 'complete']);
175
+ if (!COMMANDS_WITH_OWN_HELP.has(cmd) && isHelpToken(args && args[0])) {
173
176
  console.log('Commands:');
174
177
  for (const line of formatCliCommandHelpLines()) console.log(line);
175
178
  return;
@@ -245,6 +248,15 @@ function formatCliCommandHelpLines() {
245
248
  return entries.map(([, { summary }], i) => lefts[i].padEnd(padTo) + summary);
246
249
  }
247
250
 
251
+ // W-mq0770160001ee50 — `minions work --help` used to swallow `--help` as the
252
+ // work-item title and queue a literal junk item titled "--help"; `minions plan
253
+ // --help` dispatched a plan-to-prd agent against the inline text "--help".
254
+ // Every handler that takes a free-form positional arg must call this BEFORE
255
+ // using it as data so users get usage text instead of a state mutation.
256
+ function _isHelpArg(arg) {
257
+ return arg === '--help' || arg === '-h' || arg === 'help';
258
+ }
259
+
248
260
  // ─── Runtime fleet flags (--cli / --model / --effort) ────────────────────────
249
261
  //
250
262
  // Shared by `start`, `restart`, and `config set-cli`. Single source of truth
@@ -522,6 +534,39 @@ const commands = {
522
534
  try { shared.applyLegacyCcModelMigration(config, { logger: e.log }); }
523
535
  catch (err) { e.log('warn', `legacy ccModel migration failed: ${err.message}`); }
524
536
 
537
+ // W-mq5s5ttx000j7ab8-a — One-shot canonical-gate migration. Project the
538
+ // legacy `_contextOnly` / `_autoObserve` / `_manual` keys onto the
539
+ // canonical `contextOnly` field on every `projects/<name>/pull-
540
+ // requests.json` record BEFORE the first tick fires. `isAutoManagedPrRecord`
541
+ // now reads `contextOnly` only, so any record the migration doesn't reach
542
+ // before tick #1 would flip its auto-managed verdict. Idempotent.
543
+ try {
544
+ const projectsRoot = path.join(shared.MINIONS_DIR, 'projects');
545
+ shared.migratePrGateFlags(projectsRoot);
546
+ } catch (err) { e.log('warn', `pr-gate-migration failed: ${err.message}`); }
547
+
548
+ // One-time force-on of the CC worker pool. The pool has been the resolved
549
+ // default for copilot CC since PR #2492, but configs still carrying an
550
+ // explicit `ccUseWorkerPool: false` (set before the default flipped) stay
551
+ // opted out forever. Flip those to ON once, persisting the
552
+ // `engine._ccPoolForcedOnV1` marker so a deliberate later opt-out sticks.
553
+ // Disk-side re-derives from the on-disk copy so a concurrent dashboard
554
+ // write isn't clobbered; skipWriteIfUnchanged makes it a no-op once marked.
555
+ try {
556
+ const forced = shared.applyCcWorkerPoolForceOnMigration(config);
557
+ if (forced.changed) {
558
+ const configPath = path.join(shared.MINIONS_DIR, 'config.json');
559
+ shared.mutateJsonFileLocked(configPath, (onDisk) => {
560
+ shared.applyCcWorkerPoolForceOnMigration(onDisk);
561
+ return onDisk;
562
+ }, { defaultValue: {}, skipWriteIfUnchanged: true });
563
+ if (forced.flipped.length) {
564
+ e.log('info', `Forced CC worker pool ON — flipped explicit opt-out in: ${forced.flipped.join(', ')}`);
565
+ console.log(` Forced CC worker pool ON (was explicitly off in: ${forced.flipped.join(', ')}).`);
566
+ }
567
+ }
568
+ } catch (err) { e.log('warn', `cc worker pool force-on migration failed: ${err.message}`); }
569
+
525
570
  // Auto-heal projects missing workSources (cloned-repo / hand-rolled-config
526
571
  // footgun): without this block, discoverFromWorkItems / discoverFromPrs
527
572
  // bail silently and the engine looks healthy but never dispatches. The
@@ -1355,7 +1400,7 @@ const commands = {
1355
1400
  },
1356
1401
 
1357
1402
  complete(id) {
1358
- if (!id) {
1403
+ if (!id || _isHelpArg(id)) {
1359
1404
  console.log('Usage: minions complete <dispatch-id>');
1360
1405
  return;
1361
1406
  }
@@ -1398,7 +1443,7 @@ const commands = {
1398
1443
  spawn(agentId, ...promptParts) {
1399
1444
  const e = engine();
1400
1445
  const prompt = promptParts.join(' ');
1401
- if (!agentId || !prompt) {
1446
+ if (!agentId || _isHelpArg(agentId) || !prompt) {
1402
1447
  console.log('Usage: node .minions/engine.js spawn <agent-id> "<prompt>"');
1403
1448
  return;
1404
1449
  }
@@ -1433,7 +1478,7 @@ const commands = {
1433
1478
 
1434
1479
  work(title, ...rest) {
1435
1480
  const e = engine();
1436
- if (!title) {
1481
+ if (!title || _isHelpArg(title)) {
1437
1482
  console.log('Usage: node .minions/engine.js work "<title>" [options-json]');
1438
1483
  console.log('Options: {"id":"W-customid","type":"implement","priority":"high","agent":"dallas","description":"...","branch":"feature/...","project":"minions"}');
1439
1484
  console.log(' id Optional caller-supplied work item ID. Defaults to a cuid-style W-<id>.');
@@ -1523,7 +1568,7 @@ const commands = {
1523
1568
 
1524
1569
  plan(source, projectName) {
1525
1570
  const e = engine();
1526
- if (!source) {
1571
+ if (!source || _isHelpArg(source)) {
1527
1572
  console.log('Usage: node .minions/engine.js plan <source> [project]');
1528
1573
  console.log('');
1529
1574
  console.log('Source can be:');
@@ -32,6 +32,17 @@ const FEATURES = {
32
32
  addedIn: '0.1.1757',
33
33
  expires: '2026-11-01',
34
34
  },
35
+ // slim-ux-promo — gates the Slim UX *entry point*, not Slim UX itself. When
36
+ // off (default) the "Try new Slim UX" button is hidden on the classic
37
+ // dashboard header AND the first-visit welcome popup is suppressed inside
38
+ // Slim UX. Slim UX stays reachable via the `slim-ux` flag / Settings; this
39
+ // only controls whether it's advertised to users.
40
+ 'slim-ux-promo': {
41
+ description: 'Show the "Try new Slim UX" entry-point button on the classic dashboard and the first-visit welcome popup inside Slim UX. Off by default — Slim UX itself is still reachable via the slim-ux flag.',
42
+ default: false,
43
+ addedIn: '0.1.2057',
44
+ expires: '2026-11-01',
45
+ },
35
46
  // ccUseWorkerPool — sub-tasks B/C/D of W-mp2w003600196c51 (CC perf).
36
47
  // Routes Command Center / doc-chat through engine/cc-worker-pool.js
37
48
  // (persistent `copilot --acp` per CC tab) instead of spawning a fresh CLI