@yemi33/minions 0.1.2378 → 0.1.2380
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/bin/minions.js +19 -9
- package/dashboard/js/refresh.js +2 -3
- package/dashboard/js/render-prd.js +1 -1
- package/dashboard/js/render-work-items.js +13 -6
- package/dashboard.js +393 -721
- package/docs/README.md +1 -0
- package/docs/architecture-review-2026-07-09.md +2 -4
- package/docs/auto-discovery.md +36 -49
- package/docs/blog-first-successful-dispatch.md +1 -1
- package/docs/branch-derivation.md +2 -2
- package/docs/command-center.md +1 -1
- package/docs/completion-reports.md +14 -4
- package/docs/constellation-bridge.md +59 -10
- package/docs/constellation-style-telemetry.md +6 -6
- package/docs/cooldown-merge-semantics.md +4 -0
- package/docs/copilot-cli-schema.md +3 -3
- package/docs/cross-repo-plans.md +17 -17
- package/docs/deprecated.json +2 -2
- package/docs/design-inbox-entries-schema.md +31 -39
- package/docs/design-state-storage.md +1 -1
- package/docs/documentation-audit-2026-07-09.md +2 -2
- package/docs/engine-restart.md +20 -8
- package/docs/harness-mode.md +1 -1
- package/docs/managed-spawn.md +4 -4
- package/docs/onboarding.md +1 -2
- package/docs/pr-comment-followup.md +3 -3
- package/docs/pr-review-fix-loop.md +1 -1
- package/docs/proposals/repo-pool-for-live-checkout.md +1 -2
- package/docs/qa-runbook-lifecycle.md +4 -4
- package/docs/qa-runbooks.md +2 -2
- package/docs/rfc-completion-json.md +4 -1
- package/docs/runtime-adapters.md +1 -1
- package/docs/self-improvement.md +4 -5
- package/docs/shared-lifecycle-module-map.md +3 -1
- package/docs/slim-ux/architecture-suggestions.md +5 -6
- package/docs/slim-ux/concepts.md +23 -25
- package/docs/watches.md +7 -7
- package/docs/workspace-manifests.md +1 -1
- package/docs/worktree-lifecycle.md +1 -1
- package/engine/abandoned-pr-reconciliation.js +4 -5
- package/engine/acp-transport.js +49 -8
- package/engine/ado-status.js +5 -5
- package/engine/ado.js +20 -25
- package/engine/agent-worker-pool.js +124 -15
- package/engine/bridge.js +260 -5
- package/engine/cleanup.js +48 -131
- package/engine/cli.js +125 -83
- package/engine/cooldown.js +9 -16
- package/engine/db/index.js +22 -9
- package/engine/db/migrations/009-qa.js +1 -1
- package/engine/db/migrations/020-qa-session-scopes.js +23 -0
- package/engine/db/migrations/021-archived-work-items.js +72 -0
- package/engine/db/migrations/022-global-cc-session.js +31 -0
- package/engine/db/migrations/023-engine-state.js +30 -0
- package/engine/db/migrations/024-prd-ghost-collisions.js +53 -0
- package/engine/db/migrations/025-malformed-work-item-phantoms.js +33 -0
- package/engine/dispatch-store.js +2 -7
- package/engine/dispatch.js +34 -41
- package/engine/github.js +20 -27
- package/engine/lifecycle.js +221 -283
- package/engine/llm.js +1 -1
- package/engine/logs-store.js +2 -2
- package/engine/managed-spawn.js +2 -23
- package/engine/meeting.js +6 -6
- package/engine/metrics-store.js +2 -2
- package/engine/note-link-backfill.js +6 -11
- package/engine/pipeline.js +18 -36
- package/engine/playbook.js +1 -1
- package/engine/pooled-agent-process.js +46 -26
- package/engine/prd-store.js +73 -54
- package/engine/preflight.js +2 -5
- package/engine/projects.js +15 -62
- package/engine/pull-requests-store.js +0 -17
- package/engine/qa-runbooks.js +2 -2
- package/engine/qa-runs.js +1 -8
- package/engine/qa-sessions.js +41 -64
- package/engine/queries.js +120 -219
- package/engine/routing.js +3 -5
- package/engine/scheduler.js +0 -4
- package/engine/shared-branch-pr-reconcile.js +2 -3
- package/engine/shared.js +132 -637
- package/engine/small-state-store.js +89 -10
- package/engine/state-operations.js +16 -4
- package/engine/stdio-timestamps.js +1 -1
- package/engine/timeout.js +5 -12
- package/engine/watch-actions.js +20 -22
- package/engine/watches-store.js +1 -1
- package/engine/watches.js +6 -10
- package/engine/work-item-validation.js +52 -0
- package/engine/work-items-store.js +127 -29
- package/engine/worktree-gc.js +2 -2
- package/engine/worktree-pool.js +8 -18
- package/engine.js +167 -349
- package/minions.js +2 -2
- package/package.json +1 -1
- package/playbooks/plan-to-prd.md +2 -2
- package/playbooks/shared-rules.md +3 -3
- package/playbooks/templates/followup-dispatch.md +1 -1
- package/playbooks/verify.md +1 -1
- package/prompts/cc-system.md +2 -2
package/docs/slim-ux/concepts.md
CHANGED
|
@@ -33,7 +33,7 @@ Watches, etc. It is the primary human-to-fleet interface.
|
|
|
33
33
|
|
|
34
34
|
**Where it lives.**
|
|
35
35
|
- System prompt: `prompts/cc-system.md`
|
|
36
|
-
- Per-tab session state: `engine/
|
|
36
|
+
- Per-tab and global session state: `engine/state.db` (`cc_sessions`).
|
|
37
37
|
- Direct CLI invocation: `engine/llm.js` `callLLM({ direct: true })` — bypasses
|
|
38
38
|
`spawn-agent.js` and runs the runtime CLI itself.
|
|
39
39
|
|
|
@@ -70,10 +70,8 @@ Plan, Note, Schedule, Watch, Knowledge entry, Pinned note). Per-call cap is
|
|
|
70
70
|
the home page eventually reduce to a Work Item.
|
|
71
71
|
|
|
72
72
|
**Where it lives.**
|
|
73
|
-
-
|
|
74
|
-
-
|
|
75
|
-
`engine/shared.js` line ~1530).
|
|
76
|
-
- In-flight copy: `engine/dispatch.json` (`pending[] / active[] / completed[]`).
|
|
73
|
+
- Durable records: `engine/state.db` `work_items`, keyed by central/project scope.
|
|
74
|
+
- In-flight dispatch rows: `engine/state.db` `dispatches`.
|
|
77
75
|
|
|
78
76
|
**Backing classes / functions.**
|
|
79
77
|
- Status constants: `engine/shared.js` line 1348 (`WI_STATUS`).
|
|
@@ -327,7 +325,7 @@ engine drops a Work Item into the queue.
|
|
|
327
325
|
|
|
328
326
|
**Where it lives.**
|
|
329
327
|
- Definitions: `config.json` `schedules: []` (3-field cron `min hour dow`).
|
|
330
|
-
- Last-run history: `engine/
|
|
328
|
+
- Last-run history: `engine/state.db` `schedule_runs`.
|
|
331
329
|
|
|
332
330
|
**Backing classes / functions.**
|
|
333
331
|
- Cron parser: `engine/scheduler.js`
|
|
@@ -361,7 +359,7 @@ analogous to **Watch** (event-driven instead of time-driven).
|
|
|
361
359
|
(merged, build passes, build fails, status changes, new comments, vote changes). Watches are
|
|
362
360
|
event-driven, schedules are time-driven.
|
|
363
361
|
|
|
364
|
-
**Where it lives.** `engine/
|
|
362
|
+
**Where it lives.** `engine/state.db` `watches`.
|
|
365
363
|
|
|
366
364
|
**Backing classes / functions.**
|
|
367
365
|
- Status constants: `engine/shared.js` line 1380 (`WATCH_STATUS`).
|
|
@@ -389,11 +387,11 @@ A merged-PR watch is the canonical "tell me when X ships" primitive.
|
|
|
389
387
|
|
|
390
388
|
**What it is.** A multi-stage workflow. Stages can be a Work Item (`task`), a `meeting`, or a
|
|
391
389
|
`plan`. Stages can declare dependencies on previous stages. Pipelines run via triggers (manual,
|
|
392
|
-
schedule, watch) and persist
|
|
390
|
+
schedule, watch) and persist run state in SQLite.
|
|
393
391
|
|
|
394
392
|
**Where it lives.**
|
|
395
393
|
- Definitions: `pipelines/*.json` (one file per pipeline).
|
|
396
|
-
- Run state: `engine/
|
|
394
|
+
- Run state: `engine/state.db` `pipeline_runs`.
|
|
397
395
|
|
|
398
396
|
**Backing classes / functions.**
|
|
399
397
|
- Dirs: `engine/pipeline.js` lines 19–24 (`PIPELINES_DIR`, `PIPELINE_RUNS_PATH`, `PLANS_DIR`,
|
|
@@ -427,7 +425,7 @@ unrelated to ADO/GitHub Actions pipelines, even though the word collides.
|
|
|
427
425
|
dispatch holds the agent's PID, prompt sidecar path, worktree path, and current status until the
|
|
428
426
|
process exits.
|
|
429
427
|
|
|
430
|
-
**Where it lives.**
|
|
428
|
+
**Where it lives.** Active rows in `engine/state.db` `dispatches`.
|
|
431
429
|
|
|
432
430
|
**Backing classes / functions.**
|
|
433
431
|
- `mutateDispatch`: `engine/dispatch.js` line 60 (the only safe RMW path).
|
|
@@ -450,7 +448,7 @@ tick. Read shape via `/api/status` (line 6838).
|
|
|
450
448
|
|
|
451
449
|
## 13. Dispatch Queue
|
|
452
450
|
|
|
453
|
-
**What it is.** The
|
|
451
|
+
**What it is.** The sectioned view reconstructed from SQL dispatch rows:
|
|
454
452
|
|
|
455
453
|
- `pending[]` — admitted Work Items waiting on agent / dependency / cooldown / budget.
|
|
456
454
|
- `active[]` — currently spawned.
|
|
@@ -460,7 +458,7 @@ The queue's invariants are: max concurrent (default 5), per-PR / per-branch lock
|
|
|
460
458
|
agents fighting on the same branch, dependency-aware spawning (`depends_on` IDs must be done
|
|
461
459
|
first), retry on failure up to `ENGINE_DEFAULTS.maxRetries`.
|
|
462
460
|
|
|
463
|
-
**Where it lives.** `engine/
|
|
461
|
+
**Where it lives.** `engine/state.db` `dispatches`.
|
|
464
462
|
|
|
465
463
|
**Backing classes / functions.** All in `engine/dispatch.js`. Mutation only via `mutateDispatch`
|
|
466
464
|
line 60. Queue dedupe keys: `getDispatchProjectKey` line 80, `getPrDispatchTargetKey` line 85,
|
|
@@ -502,12 +500,11 @@ record). It also feeds **Pull Request** sync (`pr` field).
|
|
|
502
500
|
|
|
503
501
|
## 15. Pull Request (as tracked in minions)
|
|
504
502
|
|
|
505
|
-
**What it is.** A row in
|
|
503
|
+
**What it is.** A project-scoped row in the SQL PR store. Tracks status, vote tally, build
|
|
506
504
|
state, review verdict, and the work item that created it. *Not* the GitHub/ADO PR itself —
|
|
507
505
|
minions polls the host and writes a sanitized snapshot here.
|
|
508
506
|
|
|
509
|
-
**Where it lives.** `
|
|
510
|
-
line 1546). Cross-project links also tracked in `engine/pr-links.json`.
|
|
507
|
+
**Where it lives.** `engine/state.db` `pull_requests`; cross-project links use `pr_links`.
|
|
511
508
|
|
|
512
509
|
**Backing classes / functions.**
|
|
513
510
|
- Status / pollable constants: `engine/shared.js` line 1372 (`PR_STATUS`,
|
|
@@ -541,7 +538,7 @@ Examples: Ripley (architect / explorer), Dallas (engineer), Lambert (analyst), R
|
|
|
541
538
|
- Default roster: `engine/shared.js` line 1486 (`DEFAULT_AGENTS`).
|
|
542
539
|
- Charter: `agents/<id>/charter.md` — read by `getAgentCharter` (`engine/queries.js` line 505).
|
|
543
540
|
- Per-agent config (cli, model, budget, skills): `config.json` `agents.<id>`.
|
|
544
|
-
- Metrics: `engine/
|
|
541
|
+
- Metrics: `engine/state.db` `metrics` (per-agent token / cost / quality / runtime).
|
|
545
542
|
|
|
546
543
|
**Backing classes / functions.**
|
|
547
544
|
- Charter loader: `engine/queries.js` line 505.
|
|
@@ -569,14 +566,14 @@ gets which work type.
|
|
|
569
566
|
|
|
570
567
|
**Where it lives.**
|
|
571
568
|
- Definitions: `config.json` `projects: []`.
|
|
572
|
-
-
|
|
569
|
+
- Runtime records: project-scoped rows in `engine/state.db`.
|
|
573
570
|
- Removal artifact: `projects/.archived/<name>-YYYYMMDD/`.
|
|
574
571
|
|
|
575
572
|
**Backing classes / functions.**
|
|
576
573
|
- Helpers: `engine/shared.js`
|
|
577
|
-
- `getProjects`
|
|
578
|
-
- `
|
|
579
|
-
- `
|
|
574
|
+
- `getProjects`
|
|
575
|
+
- `stateScope`, `readWorkItems`, `readPullRequests`
|
|
576
|
+
- `projectStateDir` for file-backed project definitions/artifacts.
|
|
580
577
|
- Removal: `engine/projects.js` `removeProject` (canonical teardown — never edit `config.json`
|
|
581
578
|
directly).
|
|
582
579
|
|
|
@@ -658,13 +655,14 @@ consolidates the **Inbox**, and so on. It's also the gatekeeper for control stat
|
|
|
658
655
|
|
|
659
656
|
**Where it lives.**
|
|
660
657
|
- Process: `engine.js` (root).
|
|
661
|
-
- State: `engine/
|
|
662
|
-
|
|
658
|
+
- State: `engine/state.db` (logs, metrics, queues, watches, schedules, PRDs) plus
|
|
659
|
+
`engine/control.json` for process coordination.
|
|
663
660
|
|
|
664
661
|
**Backing classes / functions.**
|
|
665
662
|
- Tick: `engine.js` (top-level `tick()`).
|
|
666
663
|
- Defaults: `engine/shared.js` line 860 (`ENGINE_DEFAULTS`).
|
|
667
|
-
- Concurrency
|
|
664
|
+
- Concurrency: SQLite transactions for runtime records; file locks only for intentionally
|
|
665
|
+
file-backed definitions and coordination sidecars.
|
|
668
666
|
- Restart re-attach: PID files in `engine/tmp/pid-<id>.pid` + `live-output.log` mtimes,
|
|
669
667
|
20-min grace.
|
|
670
668
|
|
|
@@ -839,6 +837,6 @@ Pipeline (stages: task / meeting / plan) ──► spawns Work Items / Meetings
|
|
|
839
837
|
Schedule (cron) ──► spawns Work Items
|
|
840
838
|
Watch (event) ──► spawns notifications / Work Items / pipeline-continuations
|
|
841
839
|
|
|
842
|
-
Engine reads: routing.md, config.json,
|
|
843
|
-
Engine writes: control.json,
|
|
840
|
+
Engine reads: routing.md, config.json, definitions, engine/state.db
|
|
841
|
+
Engine writes: control.json, artifacts, engine/state.db
|
|
844
842
|
```
|
package/docs/watches.md
CHANGED
|
@@ -4,7 +4,7 @@ Persistent monitoring jobs that fire inbox notifications and follow-up actions w
|
|
|
4
4
|
|
|
5
5
|
## What a Watch Is
|
|
6
6
|
|
|
7
|
-
A watch is a record persisted in `engine/state.db
|
|
7
|
+
A watch is a record persisted in `engine/state.db`. It binds:
|
|
8
8
|
|
|
9
9
|
| Field | Purpose |
|
|
10
10
|
|--------------|-------------------------------------------------------------------------|
|
|
@@ -130,7 +130,7 @@ module.exports = {
|
|
|
130
130
|
|
|
131
131
|
Resolution is `path.join(shared.MINIONS_DIR, 'watches.d')` so it works in both dev checkouts and installed `~/.minions/` layouts. Missing folder is silent; per-file failures are isolated (load errors log a `WARN` line, don't abort boot). Tests can re-scan via the exported `_loadPluginTargetTypes()`.
|
|
132
132
|
|
|
133
|
-
**Canonical plugin example:** [`watches.d/http.js`](../watches.d/http.js) (W-mp7i22mu00191b07) ships a generic `http` target type — point a watch at any URL or `{ url, method, headers, body, extract, expected, regex, timeoutMs }` config and the plugin exposes `status-change`, `value-change`, `value-equals`, `value-matches`, and `http-error` conditions. Auth headers support `{{env.GH_TOKEN}}`-style substitution at fetch time (secrets are never
|
|
133
|
+
**Canonical plugin example:** [`watches.d/http.js`](../watches.d/http.js) (W-mp7i22mu00191b07) ships a generic `http` target type — point a watch at any URL or `{ url, method, headers, body, extract, expected, regex, timeoutMs }` config and the plugin exposes `status-change`, `value-change`, `value-equals`, `value-matches`, and `http-error` conditions. Auth headers support `{{env.GH_TOKEN}}`-style substitution at fetch time (secrets are never persisted in watch records). All conditions are change-based (`absoluteConditions: []`).
|
|
134
134
|
|
|
135
135
|
## Tick Integration
|
|
136
136
|
|
|
@@ -222,10 +222,10 @@ CC will discover the endpoints from `GET /api/routes` and `POST /api/watches` wi
|
|
|
222
222
|
|
|
223
223
|
When a watch fires:
|
|
224
224
|
|
|
225
|
-
-
|
|
225
|
+
- The SQL watch record gets `triggerCount++`, `last_triggered=<ISO>`, `_lastTriggerMessage=<message>`, and a refreshed `_lastState`.
|
|
226
226
|
- An inbox file lands at `notes/inbox/<owner>/watch-<watchId>-<n>.md` (when `notify === 'inbox'`).
|
|
227
227
|
- If `action` is set, a follow-up runs (work item, webhook POST, plan archive, ...) and writes `_lastActionResult` (`{ type, ok, summary, dispatchedItemId?, at }`) back onto the watch.
|
|
228
|
-
-
|
|
228
|
+
- The SQL engine log gets `Watch triggered: <id> — <message>` and `Watch <id> action <type>: <summary>`.
|
|
229
229
|
|
|
230
230
|
Absolute conditions firing under `stopAfter === 0` flip `status` to `expired`; `stopAfter > 0` flips it to `expired` once `triggerCount >= stopAfter`.
|
|
231
231
|
|
|
@@ -236,14 +236,14 @@ Absolute conditions firing under `stopAfter === 0` flip `status` to `expired`; `
|
|
|
236
236
|
| 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 |
|
|
237
237
|
| `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 |
|
|
238
238
|
| `"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[]` |
|
|
239
|
-
| 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
|
|
239
|
+
| 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`)*. If you see this, inspect the persisted watch through the API |
|
|
240
240
|
| Absolute watch fires forever instead of once | `stopAfter` is set to a non-zero value; only `stopAfter === 0` triggers fire-once expiration |
|
|
241
241
|
| 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 |
|
|
242
242
|
| `_lastActionResult.ok === false` with `"unknown action type"` | The `action.type` isn't registered. List with `listActionTypes()` / `GET /api/watches/action-types` |
|
|
243
243
|
| 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)* |
|
|
244
|
-
| Trigger fires but follow-up `dispatch-work-item` is missing | Check the engine log for `Watch <id> action <type>: <summary>`. Common reasons: missing `title`,
|
|
244
|
+
| Trigger fires but follow-up `dispatch-work-item` is missing | Check the engine log for `Watch <id> action <type>: <summary>`. Common reasons: missing `title`, a project-scope write failed, or the WI landed in the central scope because no project was specified |
|
|
245
245
|
| 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`)* |
|
|
246
|
-
| `checkWatches` block crashes silently | Wrapped in `safe('checkWatches', ...)` so one failure doesn't abort the tick
|
|
246
|
+
| `checkWatches` block crashes silently | Wrapped in `safe('checkWatches', ...)` so one failure doesn't abort the tick. Inspect the engine log for `Watch check error (<id>)` lines. Regression #1088: the block must use `getProjects(config)`, never the long-removed `PROJECTS` constant |
|
|
247
247
|
|
|
248
248
|
## See Also
|
|
249
249
|
|
|
@@ -84,7 +84,7 @@ const { MEMORY_SCOPES, WORKSPACE_MANIFEST_DEFAULTS,
|
|
|
84
84
|
|
|
85
85
|
## Audit trail
|
|
86
86
|
|
|
87
|
-
Because rejections route through the standard `completeDispatch(... ERROR ...)` path, the existing dispatch + inbox + dashboard timeline machinery already surfaces every manifest rejection. No new
|
|
87
|
+
Because rejections route through the standard `completeDispatch(... ERROR ...)` path, the existing dispatch + inbox + dashboard timeline machinery already surfaces every manifest rejection. No new table or event topic was added. Per-agent audit can be derived from SQL dispatch records filtered by `failure_class === 'workspace-manifest-repo-forbidden'`.
|
|
88
88
|
|
|
89
89
|
## Rollout guidance
|
|
90
90
|
|
|
@@ -45,7 +45,7 @@ recycle worktree dirs across branches.
|
|
|
45
45
|
- **Return** in `onAgentClose` BEFORE `completeDispatch`:
|
|
46
46
|
`git reset --hard HEAD` → `git clean -fd` → `git fetch origin <main>`
|
|
47
47
|
→ `git checkout --detach origin/<main>` → mark IDLE.
|
|
48
|
-
- **State**
|
|
48
|
+
- **State** in SQLite (`worktree_pool`); git ops outside the transaction.
|
|
49
49
|
|
|
50
50
|
## Ownership marker is never "dirty" (#284)
|
|
51
51
|
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* The version-gated one-shot model gives us exactly what we need: re-probe
|
|
21
21
|
* once per release that bumps `ENGINE_DEFAULTS.abandonedReconciliationVersion`,
|
|
22
22
|
* then never again until the next bump. First boot after this lands cleans up
|
|
23
|
-
* the historical false-flips; subsequent boots are a single state
|
|
23
|
+
* the historical false-flips; subsequent boots are a single state-marker read +
|
|
24
24
|
* version compare → no-op.
|
|
25
25
|
*
|
|
26
26
|
* ## Why a separate WI from W-mp5trwh60008386d?
|
|
@@ -37,14 +37,13 @@
|
|
|
37
37
|
* Errors here must NOT block boot — the catch in cli.js logs and continues.
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
|
-
const path = require('path');
|
|
41
40
|
const shared = require('./shared');
|
|
42
41
|
|
|
43
42
|
const { ENGINE_DEFAULTS, log, mutateEngineState, readEngineState } = shared;
|
|
44
43
|
|
|
45
44
|
/**
|
|
46
45
|
* Run the version-gated startup reconciliation pass. Idempotent across reruns
|
|
47
|
-
* at the same code version: state
|
|
46
|
+
* at the same code version: persisted engine state gates re-execution.
|
|
48
47
|
*
|
|
49
48
|
* @param {object} config — engine config (used to enumerate projects)
|
|
50
49
|
* @param {object} [options]
|
|
@@ -82,7 +81,7 @@ async function runStartupReconciliation(config, options = {}) {
|
|
|
82
81
|
const projects = shared.getProjects(config);
|
|
83
82
|
projectCount = projects.length;
|
|
84
83
|
for (const p of projects) {
|
|
85
|
-
const prs = shared.
|
|
84
|
+
const prs = shared.readPullRequests(p);
|
|
86
85
|
preCount += prs.filter(pr =>
|
|
87
86
|
pr.status === shared.PR_STATUS.ABANDONED && !pr._reconciliation404Confirmed
|
|
88
87
|
).length;
|
|
@@ -128,7 +127,7 @@ async function runStartupReconciliation(config, options = {}) {
|
|
|
128
127
|
return state;
|
|
129
128
|
});
|
|
130
129
|
} catch (err) {
|
|
131
|
-
//
|
|
130
|
+
// Marker persistence failure is non-fatal: next boot will
|
|
132
131
|
// re-run the pass, which is idempotent (already-flipped PRs aren't
|
|
133
132
|
// abandoned anymore, confirmed-404s carry the marker). Log and continue.
|
|
134
133
|
log('warn', `Abandoned PR reconciliation: failed to persist version marker: ${err.message}`);
|
package/engine/acp-transport.js
CHANGED
|
@@ -97,9 +97,10 @@ const WARM_MAX_CONCURRENT = 3;
|
|
|
97
97
|
// W-mqid8uev). resolveBinary() resolves the real JS entry
|
|
98
98
|
// (node_modules/@github/copilot/index.js) and reports whether to spawn it
|
|
99
99
|
// directly (native) or under the current Node process (shim).
|
|
100
|
-
function spawnAcp({ cwd } = {}) {
|
|
100
|
+
function spawnAcp({ cwd, env } = {}) {
|
|
101
101
|
const copilot = require('./runtimes/copilot');
|
|
102
|
-
const
|
|
102
|
+
const workerEnv = env || process.env;
|
|
103
|
+
const resolved = copilot.resolveBinary({ env: workerEnv });
|
|
103
104
|
if (!resolved || !resolved.bin) {
|
|
104
105
|
throw new Error(
|
|
105
106
|
`copilot binary not resolvable -- ${copilot.installHint || 'install the GitHub Copilot CLI'}`
|
|
@@ -114,6 +115,7 @@ function spawnAcp({ cwd } = {}) {
|
|
|
114
115
|
return spawn(execBin, execArgs, {
|
|
115
116
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
116
117
|
cwd: cwd || process.cwd(),
|
|
118
|
+
env: workerEnv,
|
|
117
119
|
windowsHide: true,
|
|
118
120
|
// Don't pass shell:true — the native/leadingArgs handling above already
|
|
119
121
|
// produces a directly-spawnable argv, and a shell wrapper would swallow
|
|
@@ -139,6 +141,33 @@ function hashMcpServers(mcpServers) {
|
|
|
139
141
|
return crypto.createHash('sha256').update(json).digest('hex');
|
|
140
142
|
}
|
|
141
143
|
|
|
144
|
+
function _stableObject(value) {
|
|
145
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
146
|
+
return Object.fromEntries(
|
|
147
|
+
Object.entries(value)
|
|
148
|
+
.filter(([, entry]) => entry !== undefined)
|
|
149
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function hashProcessContext(processEnv) {
|
|
154
|
+
return crypto.createHash('sha256')
|
|
155
|
+
.update(JSON.stringify(_stableObject(processEnv)))
|
|
156
|
+
.digest('hex');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function buildTrustedSessionContext(sessionContext) {
|
|
160
|
+
const context = _stableObject(sessionContext);
|
|
161
|
+
if (Object.keys(context).length === 0) return '';
|
|
162
|
+
const json = JSON.stringify(context, null, 2).replace(/<\/system/gi, '<\\/system');
|
|
163
|
+
return [
|
|
164
|
+
'Trusted pooled-dispatch context (session-scoped):',
|
|
165
|
+
'These values are the authoritative equivalents of the same-named environment variables for this ACP session.',
|
|
166
|
+
'Use them directly; do not try to recover them from the reusable worker process environment.',
|
|
167
|
+
json,
|
|
168
|
+
].join('\n');
|
|
169
|
+
}
|
|
170
|
+
|
|
142
171
|
// Bug B (issue #2479): `model` and `effort` were stored on the Worker but
|
|
143
172
|
// never forwarded to ACP `session/new`, so any model override was silently
|
|
144
173
|
// dropped. Build the params object here and only include fields that are
|
|
@@ -216,12 +245,17 @@ function mapAcpToolCallToToolUse(update) {
|
|
|
216
245
|
* logger (default no-op); consumers wire their own env-gated trace prefix.
|
|
217
246
|
*/
|
|
218
247
|
class Worker {
|
|
219
|
-
constructor({
|
|
248
|
+
constructor({
|
|
249
|
+
id, model, effort, mcpServers, mcpServersHash, processEnv, processContextHash,
|
|
250
|
+
systemPromptHash, cwd, internals, trace,
|
|
251
|
+
}) {
|
|
220
252
|
this.id = id;
|
|
221
253
|
this.model = model;
|
|
222
254
|
this.effort = effort;
|
|
223
255
|
this.mcpServers = mcpServers || [];
|
|
224
256
|
this.mcpServersHash = mcpServersHash;
|
|
257
|
+
this.processEnv = processEnv;
|
|
258
|
+
this.processContextHash = processContextHash;
|
|
225
259
|
this.systemPromptHash = systemPromptHash;
|
|
226
260
|
this.cwd = cwd || process.cwd();
|
|
227
261
|
this._internals = internals;
|
|
@@ -249,7 +283,7 @@ class Worker {
|
|
|
249
283
|
async _spawnAndInit() {
|
|
250
284
|
let proc;
|
|
251
285
|
try {
|
|
252
|
-
proc = this._internals.spawnAcp({ cwd: this.cwd });
|
|
286
|
+
proc = this._internals.spawnAcp({ cwd: this.cwd, env: this.processEnv });
|
|
253
287
|
} catch (err) {
|
|
254
288
|
// spawn() threw synchronously — typically ENOENT (copilot binary not
|
|
255
289
|
// on PATH) or EACCES. Surface as worker-spawn-failed so the consumer
|
|
@@ -440,7 +474,10 @@ class Worker {
|
|
|
440
474
|
|
|
441
475
|
// ── Stream a single turn ───────────────────────────────────────────────
|
|
442
476
|
stream(promptText, opts = {}) {
|
|
443
|
-
const {
|
|
477
|
+
const {
|
|
478
|
+
onChunk, onToolUse, onToolUpdate, onDone, onError, signal,
|
|
479
|
+
systemPromptText, trustedSessionContext, deferSystemPrompt,
|
|
480
|
+
} = opts;
|
|
444
481
|
if (this.killed) {
|
|
445
482
|
const err = new Error('acp-transport: worker is closed');
|
|
446
483
|
if (onError) try { onError(err); } catch { /* swallow */ }
|
|
@@ -458,10 +495,12 @@ class Worker {
|
|
|
458
495
|
// adapter pattern of <system>...</system> prepended to the first user
|
|
459
496
|
// message remains correct.
|
|
460
497
|
let prompt = promptText;
|
|
461
|
-
|
|
462
|
-
|
|
498
|
+
const trustedContextText = buildTrustedSessionContext(trustedSessionContext);
|
|
499
|
+
const trustedSystemText = [systemPromptText, trustedContextText].filter(Boolean).join('\n\n');
|
|
500
|
+
if (!this.firstSystemPromptSent && trustedSystemText) {
|
|
501
|
+
prompt = `<system>\n${trustedSystemText}\n</system>\n\n${promptText}`;
|
|
463
502
|
}
|
|
464
|
-
this.firstSystemPromptSent = true;
|
|
503
|
+
if (!deferSystemPrompt) this.firstSystemPromptSent = true;
|
|
465
504
|
|
|
466
505
|
const id = this.nextReqId++;
|
|
467
506
|
const inflight = {
|
|
@@ -586,6 +625,8 @@ module.exports = {
|
|
|
586
625
|
spawnAcp,
|
|
587
626
|
killImmediate,
|
|
588
627
|
hashMcpServers,
|
|
628
|
+
hashProcessContext,
|
|
629
|
+
buildTrustedSessionContext,
|
|
589
630
|
buildSessionNewParams,
|
|
590
631
|
extractChunkText,
|
|
591
632
|
mapAcpToolCallToToolUse,
|
package/engine/ado-status.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Output: JSON to stdout. Exit 0 on success, 1 on error/not-found.
|
|
16
16
|
*
|
|
17
|
-
* Cached (default): reads
|
|
17
|
+
* Cached (default): reads engine-maintained SQL state (~3 min stale).
|
|
18
18
|
* Live (--live): makes fresh ADO API calls — use when engine isn't running or you
|
|
19
19
|
* just pushed and need up-to-the-moment build status.
|
|
20
20
|
*/
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
const path = require('path');
|
|
25
25
|
const shared = require('./shared');
|
|
26
|
-
const { safeJson,
|
|
26
|
+
const { safeJson, readPullRequests, getProjects, MINIONS_DIR } = shared;
|
|
27
27
|
|
|
28
28
|
// ── Arg parsing ──────────────────────────────────────────────────────────────
|
|
29
29
|
|
|
@@ -56,7 +56,7 @@ const prNumber = parseInt(prNumberArg, 10);
|
|
|
56
56
|
|
|
57
57
|
function findInCache(projects) {
|
|
58
58
|
for (const project of projects) {
|
|
59
|
-
const prs =
|
|
59
|
+
const prs = readPullRequests(project);
|
|
60
60
|
shared.normalizePrRecords(prs, project);
|
|
61
61
|
const pr = prs.find(p => p.prNumber === prNumber || p.id === shared.getCanonicalPrId(project, prNumber) || shared.getPrDisplayId(p) === `PR-${prNumber}`);
|
|
62
62
|
if (!pr) continue;
|
|
@@ -104,7 +104,7 @@ async function main() {
|
|
|
104
104
|
if (!live) {
|
|
105
105
|
const result = findInCache(projects);
|
|
106
106
|
if (result) { console.log(JSON.stringify(result, null, 2)); return; }
|
|
107
|
-
console.error(`PR #${prNumber} not found in cached
|
|
107
|
+
console.error(`PR #${prNumber} not found in the cached PR store`);
|
|
108
108
|
console.error('Tip: try --live for a fresh ADO API call, or check the PR number is correct.');
|
|
109
109
|
process.exit(1);
|
|
110
110
|
}
|
|
@@ -133,7 +133,7 @@ async function main() {
|
|
|
133
133
|
return;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
console.error(`PR #${prNumber} not found (tried live ADO API and cached
|
|
136
|
+
console.error(`PR #${prNumber} not found (tried live ADO API and the cached PR store).`);
|
|
137
137
|
process.exit(1);
|
|
138
138
|
}
|
|
139
139
|
|
package/engine/ado.js
CHANGED
|
@@ -279,7 +279,7 @@ function repairAdoProjectConfig(project, purpose, prs = null) {
|
|
|
279
279
|
const missingBefore = getMissingAdoProjectConfigFields(project);
|
|
280
280
|
if (missingBefore.length === 0) return false;
|
|
281
281
|
|
|
282
|
-
const trackedPrs = prs || shared.
|
|
282
|
+
const trackedPrs = prs || shared.readPullRequests(project);
|
|
283
283
|
for (const candidate of collectAdoRepairCandidates(project, trackedPrs)) {
|
|
284
284
|
const repairs = {};
|
|
285
285
|
if (!project.adoOrg && candidate.adoOrg) repairs.adoOrg = candidate.adoOrg;
|
|
@@ -1199,7 +1199,7 @@ async function forEachActivePr(config, token, callback) {
|
|
|
1199
1199
|
}
|
|
1200
1200
|
|
|
1201
1201
|
if (projectUpdated > 0) {
|
|
1202
|
-
|
|
1202
|
+
shared.mutatePullRequests(project, (currentPrs) => {
|
|
1203
1203
|
// Merge back only fields changed by callbacks; preserve concurrent disk updates.
|
|
1204
1204
|
for (const { before, after } of updatedRecords) {
|
|
1205
1205
|
const updatedPrNumber = shared.getPrNumber(after);
|
|
@@ -1244,9 +1244,9 @@ async function forEachActivePr(config, token, callback) {
|
|
|
1244
1244
|
}
|
|
1245
1245
|
}
|
|
1246
1246
|
|
|
1247
|
-
// BUG-C2 — Also poll manually
|
|
1247
|
+
// BUG-C2 — Also poll manually linked ADO PRs from the central PR store.
|
|
1248
1248
|
// Mirror of engine/github.js:540-633. Without this block, any ADO PR enrolled
|
|
1249
|
-
// into central
|
|
1249
|
+
// into the central PR store via the dashboard attach UI or
|
|
1250
1250
|
// lifecycle.enrollPrFromCanonicalId is never polled (status, build, review,
|
|
1251
1251
|
// comments, abandoned-reconciliation all freeze permanently).
|
|
1252
1252
|
//
|
|
@@ -1272,8 +1272,7 @@ async function forEachActivePr(config, token, callback) {
|
|
|
1272
1272
|
// in that project's per-project file, so the project loop will really see
|
|
1273
1273
|
// them. Blindly skipping all project-compatible IDs caused a permanent
|
|
1274
1274
|
// orphan when the record lived in the central file (#328).
|
|
1275
|
-
const
|
|
1276
|
-
const centralPrs = shared.safeJsonArr(centralPath);
|
|
1275
|
+
const centralPrs = shared.readPullRequests('central');
|
|
1277
1276
|
const configuredAdoProjects = projects.filter(p => !isGitHubProject(p) && p.adoOrg && p.adoProject);
|
|
1278
1277
|
|
|
1279
1278
|
const activeCentral = centralPrs.filter(pr => {
|
|
@@ -1287,7 +1286,7 @@ async function forEachActivePr(config, token, callback) {
|
|
|
1287
1286
|
// polled (fix for #328).
|
|
1288
1287
|
const owningProject = configuredAdoProjects.find(p => shared.isPrCompatibleWithProject(p, { id: pr.id }, ''));
|
|
1289
1288
|
if (owningProject) {
|
|
1290
|
-
const projectPrs = shared.
|
|
1289
|
+
const projectPrs = shared.readPullRequests(owningProject);
|
|
1291
1290
|
if (projectPrs.some(p => p.id === pr.id)) return false; // present in per-project file → project loop handles it
|
|
1292
1291
|
}
|
|
1293
1292
|
return true;
|
|
@@ -1353,7 +1352,7 @@ async function forEachActivePr(config, token, callback) {
|
|
|
1353
1352
|
}
|
|
1354
1353
|
|
|
1355
1354
|
if (centralUpdated > 0) {
|
|
1356
|
-
|
|
1355
|
+
shared.mutatePullRequests('central', (currentPrs) => {
|
|
1357
1356
|
for (const { before, after } of updatedCentralRecords) {
|
|
1358
1357
|
const idx = currentPrs.findIndex(p => p.id === after.id);
|
|
1359
1358
|
if (idx >= 0) {
|
|
@@ -1457,7 +1456,7 @@ async function pollPrStatus(config) {
|
|
|
1457
1456
|
}
|
|
1458
1457
|
// F6 (P-f6commentedit): drop the comment-edit dedup map on merge/abandon
|
|
1459
1458
|
// — no further re-dispatch path can fire from this PR, so keeping it
|
|
1460
|
-
// around would only bloat
|
|
1459
|
+
// around would only bloat the PR store.
|
|
1461
1460
|
if (pr.humanFeedback && pr.humanFeedback.editsSeen) {
|
|
1462
1461
|
delete pr.humanFeedback.editsSeen;
|
|
1463
1462
|
}
|
|
@@ -1911,7 +1910,7 @@ async function pollPrStatus(config) {
|
|
|
1911
1910
|
// review status flipped, etc.) is dropped via clearPrNoOpFixAttempt. The
|
|
1912
1911
|
// dispatch gate already ignores these stale records (isPrNoOpFixCausePaused
|
|
1913
1912
|
// re-validates the fingerprint), but without this sweep they linger in
|
|
1914
|
-
//
|
|
1913
|
+
// PR store forever — bloating the registry and confusing the
|
|
1915
1914
|
// dashboard `pr-paused-chip`. Runs AFTER all live-field updates so the
|
|
1916
1915
|
// fingerprint comparison uses post-poll PR state.
|
|
1917
1916
|
try {
|
|
@@ -2160,7 +2159,7 @@ async function pollPrHumanComments(config) {
|
|
|
2160
2159
|
|
|
2161
2160
|
/**
|
|
2162
2161
|
* Reconcile PRs: find active ADO PRs created by the minions that aren't tracked
|
|
2163
|
-
* in
|
|
2162
|
+
* in the PR store, and add them. Matches PRs to work items by branch name.
|
|
2164
2163
|
*/
|
|
2165
2164
|
async function reconcilePrs(config) {
|
|
2166
2165
|
// #3758 — inert when no project actually uses ADO.
|
|
@@ -2207,18 +2206,15 @@ async function reconcilePrs(config) {
|
|
|
2207
2206
|
|
|
2208
2207
|
if (adoPrs.length === 0) continue;
|
|
2209
2208
|
|
|
2210
|
-
const
|
|
2211
|
-
const existingPrs = shared.safeJsonArr(prPath);
|
|
2209
|
+
const existingPrs = shared.readPullRequests(project);
|
|
2212
2210
|
shared.normalizePrRecords(existingPrs, project);
|
|
2213
2211
|
const existingIds = new Set(existingPrs.map(p => p.id));
|
|
2214
2212
|
let projectAdded = 0;
|
|
2215
2213
|
let metadataUpdated = 0;
|
|
2216
2214
|
|
|
2217
2215
|
// Load work items to match branches to work item IDs
|
|
2218
|
-
const
|
|
2219
|
-
const
|
|
2220
|
-
const centralWiPath = path.join(shared.MINIONS_DIR, 'work-items.json');
|
|
2221
|
-
const centralItems = shared.safeJsonArr(centralWiPath);
|
|
2216
|
+
const workItems = shared.readWorkItems(project);
|
|
2217
|
+
const centralItems = shared.readWorkItems('central');
|
|
2222
2218
|
const allItems = [...workItems, ...centralItems];
|
|
2223
2219
|
|
|
2224
2220
|
let projectUpdated = 0;
|
|
@@ -2251,9 +2247,9 @@ async function reconcilePrs(config) {
|
|
|
2251
2247
|
log('warn', `ADO: invalid sourceRefName "${branch.slice(0, 64)}" for PR ${prId}: ${refErr.message}`);
|
|
2252
2248
|
}
|
|
2253
2249
|
}
|
|
2254
|
-
// PR already tracked — write
|
|
2250
|
+
// PR already tracked — write a PR link if we can extract an ID
|
|
2255
2251
|
if (confirmedItemId) {
|
|
2256
|
-
shared.upsertPullRequestRecord(
|
|
2252
|
+
shared.upsertPullRequestRecord(project, existing || {
|
|
2257
2253
|
id: prId,
|
|
2258
2254
|
prNumber: adoPr.pullRequestId,
|
|
2259
2255
|
title: (adoPr.title || `PR #${adoPr.pullRequestId}`).slice(0, 120),
|
|
@@ -2312,7 +2308,7 @@ async function reconcilePrs(config) {
|
|
|
2312
2308
|
sourcePlan: linkedItem?.sourcePlan || '',
|
|
2313
2309
|
itemType: linkedItem?.itemType || '',
|
|
2314
2310
|
};
|
|
2315
|
-
const upserted = shared.upsertPullRequestRecord(
|
|
2311
|
+
const upserted = shared.upsertPullRequestRecord(project, entry, { project, itemId: confirmedItemId });
|
|
2316
2312
|
existingPrs.push(upserted.record || entry);
|
|
2317
2313
|
existingIds.add(prId);
|
|
2318
2314
|
projectAdded++;
|
|
@@ -2331,7 +2327,7 @@ async function reconcilePrs(config) {
|
|
|
2331
2327
|
const backfilled = shared.backfillPrPrdItems(existingPrs, shared.getPrLinks());
|
|
2332
2328
|
|
|
2333
2329
|
if (projectAdded > 0 || projectUpdated > 0 || backfilled > 0 || metadataUpdated > 0) {
|
|
2334
|
-
|
|
2330
|
+
shared.mutatePullRequests(project, (currentPrs) => {
|
|
2335
2331
|
// Merge reconciled PRs into the locked copy by ID
|
|
2336
2332
|
for (const pr of existingPrs) {
|
|
2337
2333
|
const idx = currentPrs.findIndex(p => p.id === pr.id);
|
|
@@ -2905,7 +2901,7 @@ async function reconcileAbandonedPrs(config) {
|
|
|
2905
2901
|
// log line is honest about what we left behind.
|
|
2906
2902
|
let pending = 0;
|
|
2907
2903
|
for (const project of projects) {
|
|
2908
|
-
const prs = shared.
|
|
2904
|
+
const prs = shared.readPullRequests(project);
|
|
2909
2905
|
pending += prs.filter(pr =>
|
|
2910
2906
|
pr.status === shared.PR_STATUS.ABANDONED && !pr._reconciliation404Confirmed
|
|
2911
2907
|
).length;
|
|
@@ -2926,8 +2922,7 @@ async function reconcileAbandonedPrs(config) {
|
|
|
2926
2922
|
continue;
|
|
2927
2923
|
}
|
|
2928
2924
|
|
|
2929
|
-
const
|
|
2930
|
-
const prs = shared.safeJsonArr(prPath);
|
|
2925
|
+
const prs = shared.readPullRequests(project);
|
|
2931
2926
|
const abandonedPrs = prs.filter(pr =>
|
|
2932
2927
|
pr.status === shared.PR_STATUS.ABANDONED
|
|
2933
2928
|
&& !pr._reconciliation404Confirmed
|
|
@@ -3010,7 +3005,7 @@ async function reconcileAbandonedPrs(config) {
|
|
|
3010
3005
|
|
|
3011
3006
|
if (updates.length > 0) {
|
|
3012
3007
|
const reconciledAt = ts();
|
|
3013
|
-
shared.mutatePullRequests(
|
|
3008
|
+
shared.mutatePullRequests(project, (currentPrs) => {
|
|
3014
3009
|
for (const upd of updates) {
|
|
3015
3010
|
const pr = currentPrs.find(p => shared.getPrNumber(p) === upd.prNumber);
|
|
3016
3011
|
if (!pr) continue;
|