@yemi33/minions 0.1.1961 → 0.1.1963
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/docs/README.md +7 -0
- package/docs/auto-discovery.md +5 -2
- package/docs/engine-restart.md +1 -1
- package/docs/watches.md +46 -36
- package/engine/pipeline.js +22 -7
- package/engine/shared.js +6 -4
- package/engine.js +13 -34
- package/package.json +1 -1
package/docs/README.md
CHANGED
|
@@ -14,12 +14,18 @@ Hands-on stories and distribution guides for people running or evaluating Minion
|
|
|
14
14
|
Architecture, design proposals, and lifecycle references for people working on the engine, dashboard, or playbooks.
|
|
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
|
+
- [completion-reports.md](completion-reports.md) — Canonical schema for the per-dispatch completion JSON every agent writes (status, verdict, retryability, nonce trust boundary, artifacts).
|
|
17
18
|
- [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).
|
|
18
19
|
- [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.
|
|
20
|
+
- [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`.
|
|
19
21
|
- [plan-lifecycle.md](plan-lifecycle.md) — Full plan pipeline from `/plan` through PRD materialization, dispatch with dependency gating, verify task, and human archive.
|
|
20
22
|
- [pr-review-fix-loop.md](pr-review-fix-loop.md) — How the engine moves a PR from creation through review, fix dispatch, and re-review, including stale-status guards.
|
|
21
23
|
- [rfc-completion-json.md](rfc-completion-json.md) — RFC for replacing stdout regex-scraping with a structured `completion.json` control-plane protocol.
|
|
24
|
+
- [runtime-adapters.md](runtime-adapters.md) — Adapter contract (`engine/runtimes/<name>.js`), capability flags, runtime/model resolution helpers, and the "no `runtime.name === …` branching" rule.
|
|
22
25
|
- [self-improvement.md](self-improvement.md) — The six self-improvement mechanisms (learnings inbox, per-agent history, review feedback, quality metrics, etc.) that form Minions' continuous feedback loop.
|
|
26
|
+
- [skills.md](skills.md) — `.claude/skills/<name>/SKILL.md` format, `scope: minions` vs `scope: project`, auto-extraction from agent output, and per-runtime skill roots.
|
|
27
|
+
- [team-memory.md](team-memory.md) — How `pinned.md`, `notes.md`, `knowledge/`, and per-agent memory files in `knowledge/agents/` flow into every agent prompt.
|
|
28
|
+
- [watches.md](watches.md) — Persistent watch jobs: registry-driven target types, condition catalog, follow-up action registry, and the user-extensible `watches.d/` plugin folder.
|
|
23
29
|
|
|
24
30
|
## Operations
|
|
25
31
|
|
|
@@ -28,6 +34,7 @@ Operational runbooks for engine operators and fleet maintainers.
|
|
|
28
34
|
- [auto-discovery.md](auto-discovery.md) — Auto-discovery and execution pipeline: the per-tick orchestration loop and the four work-discovery sources.
|
|
29
35
|
- [engine-restart.md](engine-restart.md) — How agents survive an engine restart: state persistence, the 20-minute startup grace period, and orphan reattachment via PID files and `live-output.log`.
|
|
30
36
|
- [human-vs-automated.md](human-vs-automated.md) — Quick reference table of which features humans start, run, decide, and recover, and the two human approval gates.
|
|
37
|
+
- [onboarding.md](onboarding.md) — First-run setup: installing the CLI, linking a project, the doctor preflight, and the first dispatch.
|
|
31
38
|
|
|
32
39
|
---
|
|
33
40
|
|
package/docs/auto-discovery.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Auto-Discovery & Execution Pipeline
|
|
2
2
|
|
|
3
|
-
> Last verified: 2026-05-
|
|
3
|
+
> Last verified: 2026-05-16 against `engine.js` `tickInner()` (lines 5381-5717).
|
|
4
4
|
|
|
5
5
|
How the minions engine finds work and dispatches agents automatically.
|
|
6
6
|
|
|
@@ -16,14 +16,16 @@ 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.5 runCleanup() Periodic cleanup (every 10 ticks ≈ 10min)
|
|
19
|
+
2.52 sweepKeepProcesses() keep_processes TTL/dead-PID sweep (every 30 ticks)
|
|
19
20
|
2.55 checkWatches() Persistent watch jobs (every 3 tick-equivalents)
|
|
20
21
|
2.6 pollPrStatus() Poll ADO + GitHub for build, review, merge status (wall-clock cadence from prPollStatusEvery × tickInterval, default ≈ 12min)
|
|
21
22
|
processPendingRebases() Run any rebase work queued from the previous tick
|
|
22
23
|
syncPrdFromPrs() Backfill PRD item status from active PRs
|
|
23
24
|
checkPlanCompletion() Mark plans completed when all features done/in-pr
|
|
24
|
-
2.7 pollPrHumanComments() Poll PR threads for human
|
|
25
|
+
2.7 pollPrHumanComments() Poll PR threads for human comments (wall-clock cadence from prPollCommentsEvery × tickInterval, default ≈ 12min)
|
|
25
26
|
reconcilePrs() (ADO+GH) Reconciliation sweep (runs regardless of poll flags)
|
|
26
27
|
2.9 stallRecovery() Auto-retry failed items blocking pending deps (every 20 ticks)
|
|
28
|
+
3a. pruneStalePrDispatches() Clear pending PR dispatches whose underlying PRs no longer warrant action
|
|
27
29
|
3. discoverWork() Scan ALL linked projects for new tasks
|
|
28
30
|
4. updateSnapshot() Write identity/now.md
|
|
29
31
|
5. dispatch Spawn agents for pending items (up to maxConcurrent)
|
|
@@ -195,6 +197,7 @@ routing.md table (see the file for the authoritative list):
|
|
|
195
197
|
verify → dallas (fallback: ralph)
|
|
196
198
|
decompose → ripley (fallback: rebecca)
|
|
197
199
|
meeting → ripley (fallback: lambert)
|
|
200
|
+
docs → lambert (fallback: _any_)
|
|
198
201
|
```
|
|
199
202
|
|
|
200
203
|
Resolution order:
|
package/docs/engine-restart.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Engine Restart & Agent Survival
|
|
2
2
|
|
|
3
|
-
> Last verified: 2026-05-
|
|
3
|
+
> Last verified: 2026-05-16 against `engine.js` (`engineRestartGraceUntil`, line 168) and `engine/shared.js` `ENGINE_DEFAULTS` (`restartGracePeriod: 1200000`, `heartbeatTimeout: 300000`, `agentTimeout: 18000000`).
|
|
4
4
|
|
|
5
5
|
## The Problem
|
|
6
6
|
|
package/docs/watches.md
CHANGED
|
@@ -19,40 +19,43 @@ A watch is a small JSON record persisted to `engine/watches.json`. It binds:
|
|
|
19
19
|
| `action` | Optional follow-up action (see "Follow-Up Actions" below) |
|
|
20
20
|
| `status` | `WATCH_STATUS.ACTIVE` \| `PAUSED` \| `TRIGGERED` \| `EXPIRED` |
|
|
21
21
|
|
|
22
|
-
`createWatch()` allocates a `watch-<uid>` id, defaults the fields above, and persists atomically via `mutateJsonFileLocked` *(source: `engine/watches.js:
|
|
22
|
+
`createWatch()` allocates a `watch-<uid>` id, defaults the fields above, and persists atomically via `mutateJsonFileLocked` *(source: `engine/watches.js:184`)*.
|
|
23
23
|
|
|
24
24
|
## Lifecycle (`WATCH_STATUS`)
|
|
25
25
|
|
|
26
|
-
Defined in `engine/shared.js:
|
|
26
|
+
Defined in `engine/shared.js:1875`:
|
|
27
27
|
|
|
28
28
|
| Status | Meaning |
|
|
29
29
|
|-------------|-------------------------------------------------------------------------|
|
|
30
30
|
| `active` | Eligible for evaluation each tick |
|
|
31
31
|
| `paused` | Skipped by `checkWatches`; persists indefinitely until resumed/deleted |
|
|
32
32
|
| `triggered` | Reserved status (set on demand by callers; not auto-applied) |
|
|
33
|
-
| `expired` | Auto-set when `stopAfter` is reached, or on first trigger for absolute conditions when `stopAfter === 0`. The watch is left on disk for audit but no longer evaluated *(source: `engine/watches.js:
|
|
33
|
+
| `expired` | Auto-set when `stopAfter` is reached, or on first trigger for absolute conditions when `stopAfter === 0`. The watch is left on disk for audit but no longer evaluated *(source: `engine/watches.js:507-508`)* |
|
|
34
34
|
|
|
35
|
-
Pause/resume flips the `status` field via `POST /api/watches/update` *(source: `engine/watches.js:153-178`, `dashboard.js
|
|
35
|
+
Pause/resume flips the `status` field via `POST /api/watches/update` *(source: `engine/watches.js:153-178`, `dashboard.js` `handleWatchesUpdate`)*.
|
|
36
36
|
|
|
37
37
|
## Conditions (`WATCH_CONDITION`)
|
|
38
38
|
|
|
39
|
-
Defined in `engine/shared.js:
|
|
39
|
+
Defined in `engine/shared.js:1891-1929`. Conditions split into two families:
|
|
40
40
|
|
|
41
41
|
### Absolute conditions (`WATCH_ABSOLUTE_CONDITIONS`)
|
|
42
|
-
*(source: `engine/shared.js:
|
|
42
|
+
*(source: `engine/shared.js:1938-1954`)*
|
|
43
43
|
|
|
44
|
-
`merged`, `build-fail`, `build-pass`, `completed`, `failed`, `concluded`, `approved`, `rejected`.
|
|
44
|
+
`merged`, `build-fail`, `build-pass`, `completed`, `failed`, `concluded`, `approved`, `rejected`, `ready-for-merge`, `retry-limit-reached`, `all-items-done`, `item-failed-n-times`.
|
|
45
45
|
|
|
46
|
-
When `stopAfter === 0`, these are **fire-once** — the engine flips the watch to `expired` after the first trigger so a permanently-merged PR doesn't keep notifying *(source: `engine/watches.js:
|
|
46
|
+
When `stopAfter === 0`, these are **fire-once** — the engine flips the watch to `expired` after the first trigger so a permanently-merged PR (or a permanently-true compound state assertion like `ready-for-merge`) doesn't keep notifying *(source: `engine/watches.js:507-508`)*.
|
|
47
47
|
|
|
48
48
|
### Change-based conditions
|
|
49
|
-
`status-change`, `any`, `new-comments`, `vote-change`, `stage-complete`, `ran`, `enabled`, `disabled`, `activity-change`.
|
|
49
|
+
`status-change`, `any`, `new-comments`, `vote-change`, `stage-complete`, `ran`, `enabled`, `disabled`, `activity-change`, `head-commit-change`, `mergeable-flipped`, `behind-master`, `draft-flipped`, `dependency-met`, `stage-advanced`.
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
### Tick-counted conditions
|
|
52
|
+
`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:1934-1935`). Counters (`_unchangedTicks`, `_stuckStageTicks`) are recomputed inside `_captureState` by comparing the fresh snapshot against `prevState`.
|
|
53
|
+
|
|
54
|
+
Change-based and tick-counted conditions 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`)*.
|
|
52
55
|
|
|
53
56
|
## Target Types — `TARGET_TYPES` Registry
|
|
54
57
|
|
|
55
|
-
Target-type behavior in `engine/watches.js` is **data-driven via a registry** *(source: `engine/watches.js:
|
|
58
|
+
Target-type behavior in `engine/watches.js` is **data-driven via a registry** *(source: `engine/watches.js:101, 124-160`)*. Each spec must provide:
|
|
56
59
|
|
|
57
60
|
- `label` — human name shown in dashboard pickers
|
|
58
61
|
- `description` — short help text
|
|
@@ -61,28 +64,34 @@ Target-type behavior in `engine/watches.js` is **data-driven via a registry** *(
|
|
|
61
64
|
- `captureState(entity)` — snapshot used for change-detection diffs
|
|
62
65
|
- `evaluate(condition, entity, prevState, target)` — returns `{ triggered, message }`
|
|
63
66
|
|
|
64
|
-
The registry IS the allowlist for `createWatch` and `/api/watches/target-types`; the old hardcoded "pr or work-item" check is gone. Add a new target type at runtime with `registerTargetType(type, spec)` and look one up with `getTargetType(type)`. `listTargetTypes()` returns the serializable form used by the dashboard *(source: `engine/watches.js:
|
|
67
|
+
The registry IS the allowlist for `createWatch` and `/api/watches/target-types`; the old hardcoded "pr or work-item" check is gone. Add a new target type at runtime with `registerTargetType(type, spec)` and look one up with `getTargetType(type)`. `listTargetTypes()` returns the serializable form used by the dashboard *(source: `engine/watches.js:124-184`)*.
|
|
68
|
+
|
|
69
|
+
### User-extensible via `watches.d/` (W-mp7hg58e000b5212)
|
|
70
|
+
|
|
71
|
+
At engine boot, every `*.js` file in `<MINIONS_DIR>/watches.d/` is auto-loaded **after** the built-in registrations *(source: `engine/watches.js:1314-1340`)*, so plugins can both add new target types and override built-ins. A plugin file exports either `{ name, spec }` or an array of such objects. Failures are logged-and-skipped — one bad plugin must not break boot or block other plugins. Reloads require an engine restart.
|
|
72
|
+
|
|
73
|
+
Canonical example: `watches.d/http.js` (W-mp7i22mu00191b07) — a generic HTTP poller covering the full plugin contract including `extractState` (custom snapshot fields not on the entity itself) and `extendTemplateVars` (custom action-template vars like `{{httpStatus}}`, `{{prevExtracted}}`).
|
|
65
74
|
|
|
66
75
|
### Built-in target types
|
|
67
76
|
|
|
68
|
-
The eight built-ins are registered at module load *(source: `engine/watches.js:
|
|
77
|
+
The eight built-ins are registered at module load *(source: `engine/watches.js:672-1271`)*. Constants live at `engine/shared.js:1881-1890` (`WATCH_TARGET_TYPE`).
|
|
69
78
|
|
|
70
79
|
| `targetType` | Target value | Conditions | Notes |
|
|
71
80
|
|---------------|--------------------------------------|----------------------------------------------------------------------------|-------|
|
|
72
|
-
| `pr` | PR number, canonical id, or display id | `merged`, `build-fail`, `build-pass`, `status-change`, `any`, `new-comments`, `vote-change` | Reads from `pull-requests.json` for any project; `new-comments` watches `humanFeedback.lastProcessedCommentDate` |
|
|
73
|
-
| `work-item` | Work item id | `completed`, `failed`, `status-change`, `any`
|
|
81
|
+
| `pr` | PR number, canonical id, or display id | `merged`, `build-fail`, `build-pass`, `status-change`, `any`, `new-comments`, `vote-change`, `head-commit-change`, `mergeable-flipped`, `ready-for-merge`, `behind-master`, `draft-flipped` | Reads from `pull-requests.json` for any project; `new-comments` watches `humanFeedback.lastProcessedCommentDate`; `behind-master` requires `engine.watchesIncludeBehindBy: true` so the GH poller populates `pr.behindBy` |
|
|
82
|
+
| `work-item` | Work item id | `completed`, `failed`, `status-change`, `any`, `stalled`, `retry-limit-reached`, `dependency-met` | `completed` matches `DONE_STATUSES`; `failed` matches `WI_STATUS.FAILED`; `stalled` fires after N unchanged captures (default 12 ≈ 60 min); `retry-limit-reached` fires when `_retryCount >= ENGINE_DEFAULTS.maxRetries` |
|
|
74
83
|
| `meeting` | Meeting id | `concluded`, `status-change`, `any` | `concluded` fires on terminal status (`completed`, `archived`) |
|
|
75
|
-
| `plan` | PRD JSON filename or plan id | `approved`, `rejected`, `completed`, `status-change`, `any`
|
|
84
|
+
| `plan` | PRD JSON filename or plan id | `approved`, `rejected`, `completed`, `status-change`, `any`, `all-items-done`, `item-failed-n-times` | Looked up by `_source` (PRD file), `_sourcePlan` (.md), or `id`; uses `PLAN_STATUS` |
|
|
76
85
|
| `schedule` | Schedule id | `ran`, `enabled`, `disabled`, `status-change`, `any` | `ran` fires when `lastRun` advances; `enabled`/`disabled` fire on the flip |
|
|
77
|
-
| `pipeline` | Pipeline id (latest run is tracked) | `completed`, `failed`, `stage-complete`, `status-change`, `any`
|
|
86
|
+
| `pipeline` | Pipeline id (latest run is tracked) | `completed`, `failed`, `stage-complete`, `status-change`, `any`, `stage-advanced`, `stuck-in-stage` | `failed` covers `PIPELINE_STATUS.FAILED` and `STOPPED`; `stage-complete`/`stage-advanced` only count within the same `runId`; `stuck-in-stage` fires after N unchanged captures (default 12) |
|
|
78
87
|
| `dispatch` | Dispatch entry id | `completed`, `failed`, `status-change`, `any` | Looks across `pending` / `active` / `completed` lists |
|
|
79
88
|
| `agent` | Agent id | `activity-change`, `status-change`, `any` | `activity-change` fires only on transitions in/out of `'working'` |
|
|
80
89
|
|
|
81
|
-
`evaluateWatch` dispatches to `tt.evaluate(...)`; unknown target types return `"Unknown target type: ..."` and unknown conditions return `"Unknown condition: ..."` — both are non-triggering *(source: `engine/watches.js:
|
|
90
|
+
`evaluateWatch` dispatches to `tt.evaluate(...)`; unknown target types return `"Unknown target type: ..."` and unknown conditions return `"Unknown condition: ..."` — both are non-triggering *(source: `engine/watches.js:318+`)*.
|
|
82
91
|
|
|
83
92
|
## Tick Integration
|
|
84
93
|
|
|
85
|
-
`engine.js` calls `checkWatches(config, state)` every 3 ticks (~3 min at the default 60s tick) inside its own `safe('checkWatches', ...)` block *(source: `engine.js:
|
|
94
|
+
`engine.js` calls `checkWatches(config, state)` every 3 ticks (~3 min at the default 60s tick) inside its own `safe('checkWatches', ...)` block *(source: `engine.js:5449-5507`)*. The engine builds the state object from cached project files + module reads:
|
|
86
95
|
|
|
87
96
|
```
|
|
88
97
|
{
|
|
@@ -93,7 +102,7 @@ The eight built-ins are registered at module load *(source: `engine/watches.js:3
|
|
|
93
102
|
}
|
|
94
103
|
```
|
|
95
104
|
|
|
96
|
-
`checkWatches` walks every active watch and, inside a single `mutateJsonFileLocked` callback *(source: `engine/watches.js:
|
|
105
|
+
`checkWatches` walks every active watch and, inside a single `mutateJsonFileLocked` callback *(source: `engine/watches.js:410+`)*:
|
|
97
106
|
|
|
98
107
|
1. Skips paused/expired watches and any watch checked within its `interval`.
|
|
99
108
|
2. Captures a baseline `_lastState` on first check (so change conditions have something to diff).
|
|
@@ -103,11 +112,11 @@ The eight built-ins are registered at module load *(source: `engine/watches.js:3
|
|
|
103
112
|
6. On non-trigger: writes a per-poll inbox note when `onNotMet === 'notify'`.
|
|
104
113
|
7. Refreshes `_lastState` for the next check.
|
|
105
114
|
|
|
106
|
-
I/O happens **outside the lock**: notifications via `writeToInbox`, follow-up actions via `_runActionTask` (`Promise` per action, failures isolated). Each action's result is persisted back onto the watch as `_lastActionResult` in a follow-up locked write *(source: `engine/watches.js:
|
|
115
|
+
I/O happens **outside the lock**: notifications via `writeToInbox`, follow-up actions via `_runActionTask` (`Promise` per action, failures isolated). Each action's result is persisted back onto the watch as `_lastActionResult` in a follow-up locked write *(source: `engine/watches.js:410+`)*.
|
|
107
116
|
|
|
108
117
|
## Follow-Up Actions on Trigger
|
|
109
118
|
|
|
110
|
-
`watch.action` is an optional structured action that runs after the inbox notification fires. Action types live in a sibling registry in `engine/watch-actions.js` and are validated at create/update time *(source: `engine/watches.js:112-115`, `engine/watch-actions.js:
|
|
119
|
+
`watch.action` is an optional structured action that runs after the inbox notification fires. Action types live in a sibling registry in `engine/watch-actions.js` and are validated at create/update time *(source: `engine/watches.js:112-115`, `engine/watch-actions.js:223-330`)*. `GET /api/watches/action-types` returns the live list for dashboard pickers.
|
|
111
120
|
|
|
112
121
|
### Built-in actions
|
|
113
122
|
|
|
@@ -117,16 +126,17 @@ I/O happens **outside the lock**: notifications via `writeToInbox`, follow-up ac
|
|
|
117
126
|
| `dispatch-work-item` | Append a new WI to the project (or central) `work-items.json` with `createdBy: "watch:<id>"` |
|
|
118
127
|
| `run-skill` | Wrapper around `dispatch-work-item` that asks the agent to run a specific `.claude` skill |
|
|
119
128
|
| `webhook` | `http`/`https` request to an arbitrary URL (10s safety timeout, JSON or string body) |
|
|
129
|
+
| `minions-api` | Loopback call to the in-process dashboard at `http://127.0.0.1:${MINIONS_PORT \|\| 7331}` — `endpoint` must start with `/api/`; sets `X-Minions-Internal: 1`; returns `{ok, status, summary, response}` for chain-step templating |
|
|
120
130
|
| `cancel-work-item` | Flip a WI to `WI_STATUS.CANCELLED` across all known work-items files |
|
|
121
131
|
| `trigger-pipeline` | Start a new pipeline run (skipped if the pipeline already has an active run) |
|
|
122
132
|
| `archive-plan` | Set PRD `status="archived"` + `archivedAt` |
|
|
123
133
|
| `resume-plan` | Set PRD `status=PLAN_STATUS.ACTIVE` and clear `planStale` |
|
|
124
134
|
|
|
125
|
-
Constants live in `WATCH_ACTION_TYPE` (`engine/shared.js:
|
|
135
|
+
Constants live in `WATCH_ACTION_TYPE` (`engine/shared.js:1960+`); handlers in `engine/watch-actions.js:332-700`.
|
|
126
136
|
|
|
127
137
|
### Templating
|
|
128
138
|
|
|
129
|
-
Action params support `{{var}}` substitution from the trigger context *(source: `engine/watch-actions.js:
|
|
139
|
+
Action params support `{{var}}` substitution from the trigger context *(source: `engine/watch-actions.js:145-220`)*. Built-in vars from `buildTriggerContext`:
|
|
130
140
|
|
|
131
141
|
- Always: `target`, `targetType`, `condition`, `watchId`, `triggerCount`, `message`
|
|
132
142
|
- All scalar fields from `newState` (e.g. `status`, `buildStatus`, `reviewStatus`, `lastRun`)
|
|
@@ -136,7 +146,7 @@ Unknown vars are left as `{{var}}` so callers can detect them downstream.
|
|
|
136
146
|
|
|
137
147
|
### Failure isolation
|
|
138
148
|
|
|
139
|
-
Action handlers must return `{ ok: false, summary }` rather than throw — `runWatchAction` wraps them in try/catch as a backstop *(source: `engine/watch-actions.js:
|
|
149
|
+
Action handlers must return `{ ok: false, summary }` rather than throw — `runWatchAction` wraps them in try/catch as a backstop *(source: `engine/watch-actions.js:223-270`)*. A bad action never blocks other watches in the same tick, and the failed result is persisted onto the watch as `_lastActionResult` for debugging. Multi-step action chains run via `runWatchActionChain` *(source: `engine/watch-actions.js:271+`)* — each step's `{ok, summary, result}` is templated into the next step's params as `{{steps.N.result.<field>}}`.
|
|
140
150
|
|
|
141
151
|
## Dashboard Surface
|
|
142
152
|
|
|
@@ -150,11 +160,11 @@ Action handlers must return `{ ok: false, summary }` rather than throw — `runW
|
|
|
150
160
|
| `/api/watches/update` | POST | `handleWatchesUpdate` | Pause/resume/modify (body: `id, status?, interval?, description?, notify?, stopAfter?, onNotMet?, condition?, action?`) |
|
|
151
161
|
| `/api/watches/delete` | POST | `handleWatchesDelete` | Delete (body: `id`) |
|
|
152
162
|
|
|
153
|
-
*(source: route table in `dashboard.js
|
|
163
|
+
*(source: route table in `dashboard.js`; handlers `handleWatchesList` / `handleWatchesTargetTypes` / `handleWatchesActionTypes` / `handleWatchesCreate` / `handleWatchesUpdate` / `handleWatchesDelete`)*
|
|
154
164
|
|
|
155
|
-
The watches page is rendered by `dashboard/js/render-watches.js`; the **+ New** button calls `openCreateWatchModal()` which fetches `/api/watches/target-types` and `/api/watches/action-types` to populate the picker dynamically *(source: `dashboard/pages/watches.html
|
|
165
|
+
The watches page is rendered by `dashboard/js/render-watches.js`; the **+ New** button calls `openCreateWatchModal()` which fetches `/api/watches/target-types` and `/api/watches/action-types` to populate the picker dynamically *(source: `dashboard/pages/watches.html`, `dashboard/js/render-watches.js`)*.
|
|
156
166
|
|
|
157
|
-
The CC state preamble injects a `Watches: <count>` line via `getWatches()` so the chat brain knows how many watches exist *(source: `dashboard.js
|
|
167
|
+
The CC state preamble injects a `Watches: <count>` line via `getWatches()` so the chat brain knows how many watches exist *(source: `dashboard.js` `buildCCStatePreamble`)*.
|
|
158
168
|
|
|
159
169
|
## Command Center Integration
|
|
160
170
|
|
|
@@ -182,20 +192,20 @@ Absolute conditions firing under `stopAfter === 0` flip `status` to `expired`; `
|
|
|
182
192
|
| Watch never fires | Check `status === 'active'`; check `last_checked` advancing each cycle; confirm engine tick is running and `interval` isn't longer than your test window |
|
|
183
193
|
| `evaluateWatch` returns `"<label> <target> not found"` | `fetchEntity` got nothing back — wrong `target` (e.g. PR display id vs canonical id), the target type isn't loaded, or the underlying file (PR cache, plan PRD) doesn't exist |
|
|
184
194
|
| `"Unknown target type"` / `"Unknown condition"` | The registry doesn't recognise the value. Check `GET /api/watches/target-types` to see what's registered server-side; condition must be in that target type's `conditions[]` |
|
|
185
|
-
| Change condition fires immediately on first tick | Won't happen — baseline `_lastState` is captured on the first check before `evaluate` runs *(source: `engine/watches.js:
|
|
195
|
+
| Change condition fires immediately on first tick | Won't happen — baseline `_lastState` is captured on the first check before `evaluate` runs *(source: `engine/watches.js:434, 520`)*. If you see this, suspect manual edits to `watches.json` |
|
|
186
196
|
| Absolute watch fires forever instead of once | `stopAfter` is set to a non-zero value; only `stopAfter === 0` triggers fire-once expiration |
|
|
187
197
|
| Action runs but inbox notification doesn't | `notify` field isn't `'inbox'`, or `owner` is empty. `notify` and `action` are independent — both can fire, or only one |
|
|
188
198
|
| `_lastActionResult.ok === false` with `"unknown action type"` | The `action.type` isn't registered. List with `listActionTypes()` / `GET /api/watches/action-types` |
|
|
189
|
-
| 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
|
|
199
|
+
| 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)* |
|
|
190
200
|
| 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 |
|
|
191
|
-
| Watch `_lastActionResult` shows `"timeout"` for webhook | Webhooks have a 10s safety timeout to keep the watches tick fast *(source: `engine/watch-actions.js:
|
|
192
|
-
| `checkWatches` block crashes silently | Wrapped in `safe('checkWatches', ...)` so one failure doesn't abort the tick *(source: `engine.js:
|
|
201
|
+
| Watch `_lastActionResult` shows `"timeout"` for webhook | Webhooks have a 10s safety timeout to keep the watches tick fast *(source: `engine/watch-actions.js:482`)* |
|
|
202
|
+
| `checkWatches` block crashes silently | Wrapped in `safe('checkWatches', ...)` so one failure doesn't abort the tick *(source: `engine.js:5453`)*. Inspect `engine/log.json` for `Watch check error (<id>)` lines. Regression #1088: the block must use `getProjects(config)`, never the long-removed `PROJECTS` constant |
|
|
193
203
|
|
|
194
204
|
## See Also
|
|
195
205
|
|
|
196
|
-
- `engine/shared.js:
|
|
197
|
-
- `engine/watches.js` — registry, lifecycle, tick integration
|
|
198
|
-
- `engine/watch-actions.js` — action registry and built-in handlers
|
|
206
|
+
- `engine/shared.js:1875-1960` — `WATCH_STATUS`, `WATCH_TARGET_TYPE`, `WATCH_CONDITION`, `WATCH_ABSOLUTE_CONDITIONS`, `WATCH_ACTION_TYPE` constants
|
|
207
|
+
- `engine/watches.js` — registry, lifecycle, tick integration, `watches.d/` plugin loader
|
|
208
|
+
- `engine/watch-actions.js` — action registry and built-in handlers (including `minions-api`)
|
|
199
209
|
- `dashboard/pages/watches.html`, `dashboard/js/render-watches.js` — dashboard UI
|
|
200
|
-
- `test/unit/watches-module.test.js`, `test/unit/watch-actions.test.js` — module-level tests
|
|
210
|
+
- `test/unit/watches-module.test.js`, `test/unit/watch-actions.test.js`, `test/unit/watches-plugin-loader.test.js` — module-level tests
|
|
201
211
|
- [`auto-discovery.md`](auto-discovery.md) — overall tick cycle context
|
package/engine/pipeline.js
CHANGED
|
@@ -8,7 +8,7 @@ const fs = require('fs');
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const shared = require('./shared');
|
|
10
10
|
const queries = require('./queries');
|
|
11
|
-
const { safeJson, safeJsonNoRestore, safeWrite, safeRead, safeReadDir, uid, log, ts, dateStamp, mutateJsonFileLocked, mutateWorkItems, slugify, formatTranscriptEntry, WI_STATUS, WORK_TYPE, PLAN_STATUS, PR_STATUS, PIPELINE_STATUS, STAGE_TYPE, MEETING_STATUS, ENGINE_DEFAULTS, MINIONS_DIR } = shared;
|
|
11
|
+
const { safeJson, safeJsonNoRestore, safeWrite, safeRead, safeReadDir, uid, log, ts, dateStamp, mutateJsonFileLocked, mutateWorkItems, slugify, formatTranscriptEntry, WI_STATUS, WORK_TYPE, PLAN_STATUS, PR_STATUS, PIPELINE_STATUS, STAGE_TYPE, MEETING_STATUS, READ_ONLY_ROOT_TASK_TYPES, ENGINE_DEFAULTS, MINIONS_DIR } = shared;
|
|
12
12
|
const routing = require('./routing');
|
|
13
13
|
const http = require('http');
|
|
14
14
|
const { parseCronExpr, shouldRunNow } = require('./scheduler');
|
|
@@ -388,13 +388,20 @@ function executeTaskStage(stage, stageState, run, config, pipeline = {}) {
|
|
|
388
388
|
const projectSlug = _pipelineProjectSlug(project);
|
|
389
389
|
const id = `PL-${run.runId.slice(4, 12)}-${stage.id}-${i}${projectResolution.projects.length > 1 || project ? '-' + projectSlug : ''}`;
|
|
390
390
|
const wiPath = _pipelineWorkItemsPath(project);
|
|
391
|
+
const wiType = routing.normalizeWorkType(item.type || stage.taskType, WORK_TYPE.EXPLORE);
|
|
392
|
+
// W-mp8ho6w500034a58: read-only stages don't commit, so a pipeline
|
|
393
|
+
// branch label is meaningless to them — omit it entirely so the
|
|
394
|
+
// dispatcher's read-only fast-path runs without ceremony.
|
|
395
|
+
const wiBranch = READ_ONLY_ROOT_TASK_TYPES.has(wiType)
|
|
396
|
+
? null
|
|
397
|
+
: `pipeline/${run.pipelineId}/${stage.id}`;
|
|
391
398
|
mutateWorkItems(wiPath, workItems => {
|
|
392
399
|
if (workItems.some(w => w.id === id)) { createdIds.push(id); return workItems; }
|
|
393
400
|
workItems.push({
|
|
394
401
|
id,
|
|
395
402
|
title: item.title || stage.title,
|
|
396
403
|
description: item.description || stage.description || '',
|
|
397
|
-
type:
|
|
404
|
+
type: wiType,
|
|
398
405
|
priority: item.priority || stage.priority || 'medium',
|
|
399
406
|
// Agent is a soft routing hint unless agentLock/hardAgent is set.
|
|
400
407
|
...(item.agent || stage.agent ? { agent: item.agent || stage.agent } : {}),
|
|
@@ -403,7 +410,7 @@ function executeTaskStage(stage, stageState, run, config, pipeline = {}) {
|
|
|
403
410
|
status: WI_STATUS.PENDING,
|
|
404
411
|
created: ts(),
|
|
405
412
|
createdBy: 'pipeline:' + run.pipelineId,
|
|
406
|
-
branch:
|
|
413
|
+
...(wiBranch ? { branch: wiBranch } : {}),
|
|
407
414
|
_pipelineRun: run.runId,
|
|
408
415
|
_pipelineStage: stage.id,
|
|
409
416
|
});
|
|
@@ -430,11 +437,18 @@ function executeTaskStageLegacy(stage, stageState, run, config) {
|
|
|
430
437
|
const item = items[i % items.length];
|
|
431
438
|
const id = `PL-${run.runId.slice(4, 12)}-${stage.id}-${i}`;
|
|
432
439
|
if (workItems.some(w => w.id === id)) { createdIds.push(id); continue; }
|
|
440
|
+
const wiType = routing.normalizeWorkType(item.type || stage.taskType, WORK_TYPE.EXPLORE);
|
|
441
|
+
// W-mp8ho6w500034a58: read-only stages don't commit, so the branch
|
|
442
|
+
// label is meaningless — omit it so dispatch takes the read-only path
|
|
443
|
+
// without recomputing a worktree placement that will never be used.
|
|
444
|
+
const wiBranch = READ_ONLY_ROOT_TASK_TYPES.has(wiType)
|
|
445
|
+
? null
|
|
446
|
+
: `pipeline/${run.pipelineId}/${stage.id}`;
|
|
433
447
|
workItems.push({
|
|
434
448
|
id,
|
|
435
449
|
title: item.title || stage.title,
|
|
436
450
|
description: item.description || stage.description || '',
|
|
437
|
-
type:
|
|
451
|
+
type: wiType,
|
|
438
452
|
priority: item.priority || stage.priority || 'medium',
|
|
439
453
|
// Agent is a soft routing hint unless agentLock/hardAgent is set.
|
|
440
454
|
...(item.agent || stage.agent ? { agent: item.agent || stage.agent } : {}),
|
|
@@ -442,7 +456,7 @@ function executeTaskStageLegacy(stage, stageState, run, config) {
|
|
|
442
456
|
status: WI_STATUS.PENDING,
|
|
443
457
|
created: ts(),
|
|
444
458
|
createdBy: 'pipeline:' + run.pipelineId,
|
|
445
|
-
branch:
|
|
459
|
+
...(wiBranch ? { branch: wiBranch } : {}),
|
|
446
460
|
_pipelineRun: run.runId,
|
|
447
461
|
_pipelineStage: stage.id,
|
|
448
462
|
});
|
|
@@ -571,7 +585,8 @@ async function executePlanStage(stage, stageState, run, config, pipeline = {}) {
|
|
|
571
585
|
id: wiId, title: `Convert plan to PRD: ${existingPlanFile}`,
|
|
572
586
|
type: WORK_TYPE.PLAN_TO_PRD, priority: 'high', status: WI_STATUS.PENDING,
|
|
573
587
|
planFile: existingPlanFile, created: ts(), createdBy: 'pipeline:' + run.pipelineId,
|
|
574
|
-
|
|
588
|
+
// W-mp8ho6w500034a58: PLAN_TO_PRD is read-only — no branch needed.
|
|
589
|
+
_pipelineRun: run.runId, _pipelineStage: stage.id,
|
|
575
590
|
...(project ? { project: project.name } : {}),
|
|
576
591
|
});
|
|
577
592
|
}
|
|
@@ -665,7 +680,7 @@ async function executePlanStage(stage, stageState, run, config, pipeline = {}) {
|
|
|
665
680
|
planFile: path.basename(filePath),
|
|
666
681
|
created: ts(),
|
|
667
682
|
createdBy: 'pipeline:' + run.pipelineId,
|
|
668
|
-
|
|
683
|
+
// W-mp8ho6w500034a58: PLAN_TO_PRD is read-only — no branch needed.
|
|
669
684
|
_pipelineRun: run.runId,
|
|
670
685
|
_pipelineStage: stage.id,
|
|
671
686
|
...(project ? { project: project.name } : {}),
|
package/engine/shared.js
CHANGED
|
@@ -2883,10 +2883,12 @@ const READ_ONLY_ROOT_TASK_TYPES = new Set(['meeting', 'ask', 'explore', 'plan-to
|
|
|
2883
2883
|
* the drive-root preflight that fires when MINIONS_DIR sits one level
|
|
2884
2884
|
* below a filesystem root (resolveProjectRootDir's collapse case).
|
|
2885
2885
|
*
|
|
2886
|
-
* NOTE: Pipeline branches
|
|
2887
|
-
*
|
|
2888
|
-
*
|
|
2889
|
-
* branch
|
|
2886
|
+
* NOTE (W-mp8ho6w500034a58): Pipeline branches no longer override this.
|
|
2887
|
+
* Read-only pipeline stages don't commit, so a `pipeline/...` branch is a
|
|
2888
|
+
* meaningless label for them — the dispatcher short-circuits read-only WIs
|
|
2889
|
+
* regardless of branch name, and `engine/pipeline.js` now omits the branch
|
|
2890
|
+
* field for read-only stages. Only code-mutating pipeline stages need a
|
|
2891
|
+
* worktree, and they take the normal code-mutating path below.
|
|
2890
2892
|
*
|
|
2891
2893
|
* @param {{ localPath?: string|null }|null|undefined} project
|
|
2892
2894
|
* @param {string} type — work type (e.g. 'fix', 'explore', 'meeting')
|
package/engine.js
CHANGED
|
@@ -108,10 +108,6 @@ const CHECKPOINT_CAP_FAIL_REASON = 'Exceeded 3 checkpoint-resumes; manual interv
|
|
|
108
108
|
// re-aliased here for the existing call sites in this file.
|
|
109
109
|
const READ_ONLY_ROOT_TASK_TYPES = shared.READ_ONLY_ROOT_TASK_TYPES;
|
|
110
110
|
|
|
111
|
-
function isPipelineBranchName(branchName) {
|
|
112
|
-
return typeof branchName === 'string' && branchName.startsWith('pipeline/');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
111
|
// ─── Dispatch Management (extracted to engine/dispatch.js) ───────────────────
|
|
116
112
|
|
|
117
113
|
const { mutateDispatch, addToDispatch, addToDispatchWithValidation, isRetryableFailureReason, completeDispatch,
|
|
@@ -777,8 +773,13 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
777
773
|
// (caller defaults cwd to worktreeRootDir; drive-root collapse throws
|
|
778
774
|
// WORKTREE_ROOTDIR_COLLAPSED_TO_DRIVE_ROOT — same fail-fast behavior as
|
|
779
775
|
// the legacy resolveProjectRootDir call this replaced).
|
|
780
|
-
//
|
|
781
|
-
//
|
|
776
|
+
// W-mp8ho6w500034a58: read-only task types (meeting/ask/explore/plan/plan-to-prd)
|
|
777
|
+
// never need a worktree — even when carrying a pipeline branch. Pipeline branches
|
|
778
|
+
// on read-only stages are a no-op label; the stage doesn't commit anything, so
|
|
779
|
+
// the worktree had no functional purpose and was only there to absorb a drive-
|
|
780
|
+
// root preflight that fired against MINIONS_DIR's parent. Read-only pipeline
|
|
781
|
+
// stages now short-circuit alongside any other read-only WI (see the gate at
|
|
782
|
+
// `if (branchName && READ_ONLY_ROOT_TASK_TYPES.has(type))` below).
|
|
782
783
|
const _preBranchName = meta?.branch ? sanitizeBranch(meta.branch) : null;
|
|
783
784
|
let cwd, worktreeRootDir;
|
|
784
785
|
try {
|
|
@@ -799,29 +800,6 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
799
800
|
}
|
|
800
801
|
throw rootErr;
|
|
801
802
|
}
|
|
802
|
-
// Pipeline branches need a worktree even for read-only types (the worktree
|
|
803
|
-
// IS the pipeline's isolated workspace). When we detect a pipeline branch
|
|
804
|
-
// on a read-only type, recompute worktreeRootDir so the worktree creation
|
|
805
|
-
// block has a placement parent — and so the drive-root preflight still fires.
|
|
806
|
-
if (worktreeRootDir === null && isPipelineBranchName(_preBranchName)) {
|
|
807
|
-
try {
|
|
808
|
-
worktreeRootDir = shared.resolveProjectRootDir(project.localPath, MINIONS_DIR);
|
|
809
|
-
} catch (rootErr) {
|
|
810
|
-
if (rootErr?.code === 'WORKTREE_ROOTDIR_COLLAPSED_TO_DRIVE_ROOT' || rootErr?.code === 'WORKTREE_ROOTDIR_MISSING_BASE') {
|
|
811
|
-
log('error', `spawnAgent: pipeline-branch rootDir resolution failed for ${id}: ${rootErr.message}`);
|
|
812
|
-
completeDispatch(
|
|
813
|
-
id,
|
|
814
|
-
DISPATCH_RESULT.ERROR,
|
|
815
|
-
rootErr.message.slice(0, 800),
|
|
816
|
-
'Pre-spawn worktree preflight rejected — see failure_class for the specific cause.',
|
|
817
|
-
{ failureClass: FAILURE_CLASS.WORKTREE_PREFLIGHT, agentRetryable: false },
|
|
818
|
-
);
|
|
819
|
-
cleanupTempAgent(agentId);
|
|
820
|
-
return null;
|
|
821
|
-
}
|
|
822
|
-
throw rootErr;
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
803
|
// Legacy local alias: downstream git ops (worktree add, prune, fetch) and
|
|
826
804
|
// the `cwd === rootDir` safety warn at line ~1387 reference `rootDir`. For
|
|
827
805
|
// read-only rootless tasks (no worktree, no branch) this is null — the
|
|
@@ -910,14 +888,15 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
910
888
|
};
|
|
911
889
|
_phaseT.afterPrompt = Date.now();
|
|
912
890
|
|
|
913
|
-
if (branchName && READ_ONLY_ROOT_TASK_TYPES.has(type)
|
|
891
|
+
if (branchName && READ_ONLY_ROOT_TASK_TYPES.has(type)) {
|
|
914
892
|
// W-mp7havqf0007ce6b: read-only types (meeting/ask/explore/plan/plan-to-prd)
|
|
915
893
|
// short-circuit BEFORE the worktree-creation block. resolveSpawnPaths returns
|
|
916
894
|
// worktreeRootDir=null for read-only types, and path.resolve(null, ...) throws
|
|
917
|
-
// ("paths[0] must be of type string. Received null"). Pipeline branches
|
|
918
|
-
// exempt — they
|
|
919
|
-
//
|
|
920
|
-
//
|
|
895
|
+
// ("paths[0] must be of type string. Received null"). Pipeline branches used
|
|
896
|
+
// to be exempt — they don't need to be (W-mp8ho6w500034a58): read-only stages
|
|
897
|
+
// never commit, so a pipeline-branch label is meaningless for them and the
|
|
898
|
+
// forced worktree only existed to feed the drive-root preflight that this
|
|
899
|
+
// short-circuit now correctly avoids.
|
|
921
900
|
log('info', `${type}: read-only task with branch ${branchName} — skipping worktree, running in cwd ${cwd}`);
|
|
922
901
|
branchName = null;
|
|
923
902
|
worktreePath = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1963",
|
|
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"
|