@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/engine/llm.js
CHANGED
|
@@ -33,7 +33,7 @@ const EXIT_DRAIN_FALLBACK_MS = 100;
|
|
|
33
33
|
//
|
|
34
34
|
// Updates accumulate in an in-memory buffer and flush every
|
|
35
35
|
// metricsFlushIntervalMs (default 10s). Replaces the per-call mutateJsonFileLocked
|
|
36
|
-
// that was
|
|
36
|
+
// that was serializing the LLM hot path
|
|
37
37
|
// on every call — defeating the dashboard fast-state mtime-based early-exit.
|
|
38
38
|
|
|
39
39
|
let _pendingMetrics = { engine: Object.create(null), daily: Object.create(null) };
|
package/engine/logs-store.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// engine/logs-store.js — SQL-backed
|
|
1
|
+
// engine/logs-store.js — SQL-backed engine logs.
|
|
2
2
|
//
|
|
3
3
|
// Logs differ from work_items / pull_requests / dispatches: they are
|
|
4
4
|
// pure append-only with a tail-cap, so the store exposes a different
|
|
@@ -168,7 +168,7 @@ function readRecentLogs(n, opts) {
|
|
|
168
168
|
return out;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
// Convenience:
|
|
171
|
+
// Convenience: return the latest N entries in chronological order.
|
|
172
172
|
// — chronological order (oldest first). Inverts the readRecentLogs DESC
|
|
173
173
|
// output so existing callers can drop in without re-sorting.
|
|
174
174
|
function readRecentLogsChronological(n, opts) {
|
package/engine/managed-spawn.js
CHANGED
|
@@ -28,7 +28,6 @@ const queries = require('./queries');
|
|
|
28
28
|
const { log, ENGINE_DEFAULTS } = shared;
|
|
29
29
|
|
|
30
30
|
const MANAGED_SPAWN_FILENAME = 'managed-spawn.json';
|
|
31
|
-
const MANAGED_PROCESSES_STATE_FILE = 'managed-processes.json';
|
|
32
31
|
const MANAGED_LOGS_DIR = 'managed-logs';
|
|
33
32
|
const INVALID_WORKDIR_REASON_PREFIX = 'invalid-workdir: ';
|
|
34
33
|
|
|
@@ -760,7 +759,7 @@ function buildManagedSpawnHint(opts) {
|
|
|
760
759
|
// spawnDashboard detached-spawn pattern.
|
|
761
760
|
// Returns {pid, started_at, log_path}.
|
|
762
761
|
// 3. recordManagedSpec(spec, → writes one entry to
|
|
763
|
-
// runtime, ctx)
|
|
762
|
+
// runtime, ctx) SQL managed-process state via
|
|
764
763
|
// mutateJsonFileLocked. Replaces any
|
|
765
764
|
// existing entry with the same name
|
|
766
765
|
// (idempotent under retry).
|
|
@@ -777,16 +776,6 @@ function buildManagedSpawnHint(opts) {
|
|
|
777
776
|
// (playbook injection) + items 3/7
|
|
778
777
|
// (sweep + boot reconcile).
|
|
779
778
|
//
|
|
780
|
-
// All state writes go through mutateJsonFileLocked per the repo convention
|
|
781
|
-
// ('Key conventions' in copilot-instructions.md). Callbacks stay synchronous
|
|
782
|
-
// and fast — no kill / no spawn inside the lock callback. Healthcheck loops,
|
|
783
|
-
// dispatch ERROR gating on healthcheck failure, per-tick sweep, boot
|
|
784
|
-
// reconcile, and dashboard endpoints are deferred to items 3/4/5/7.
|
|
785
|
-
|
|
786
|
-
function _getStatePath() {
|
|
787
|
-
return path.join(shared.MINIONS_DIR, 'engine', MANAGED_PROCESSES_STATE_FILE);
|
|
788
|
-
}
|
|
789
|
-
|
|
790
779
|
function _getLogsDir() {
|
|
791
780
|
return path.join(shared.MINIONS_DIR, 'engine', MANAGED_LOGS_DIR);
|
|
792
781
|
}
|
|
@@ -906,7 +895,6 @@ function _toStateRecord(spec, runtime, ctx) {
|
|
|
906
895
|
|
|
907
896
|
function recordManagedSpec(spec, runtime, ctx) {
|
|
908
897
|
if (!spec || !spec.name) throw new Error('recordManagedSpec: spec.name required');
|
|
909
|
-
const statePath = _getStatePath();
|
|
910
898
|
shared.mutateManagedProcesses((data) => {
|
|
911
899
|
if (!data || typeof data !== 'object' || Array.isArray(data) || !Array.isArray(data.specs)) {
|
|
912
900
|
data = _initialStateShape();
|
|
@@ -921,7 +909,6 @@ function recordManagedSpec(spec, runtime, ctx) {
|
|
|
921
909
|
|
|
922
910
|
function recordManagedBatch(items, ctx) {
|
|
923
911
|
if (!Array.isArray(items) || items.length === 0) return;
|
|
924
|
-
const statePath = _getStatePath();
|
|
925
912
|
shared.mutateManagedProcesses((data) => {
|
|
926
913
|
if (!data || typeof data !== 'object' || Array.isArray(data) || !Array.isArray(data.specs)) {
|
|
927
914
|
data = _initialStateShape();
|
|
@@ -942,7 +929,6 @@ function recordManagedBatch(items, ctx) {
|
|
|
942
929
|
function removeManagedSpec(name) {
|
|
943
930
|
if (typeof name !== 'string' || name.length === 0) return;
|
|
944
931
|
let killPid = null;
|
|
945
|
-
const statePath = _getStatePath();
|
|
946
932
|
shared.mutateManagedProcesses((data) => {
|
|
947
933
|
if (!data || !Array.isArray(data.specs)) return data;
|
|
948
934
|
const idx = data.specs.findIndex(s => s && s.name === name);
|
|
@@ -968,8 +954,7 @@ function removeManagedSpec(name) {
|
|
|
968
954
|
|
|
969
955
|
function listManagedSpecs(opts) {
|
|
970
956
|
opts = opts || {};
|
|
971
|
-
const
|
|
972
|
-
const parsed = shared.safeJsonObj(statePath);
|
|
957
|
+
const parsed = require('./small-state-store').readManagedProcesses();
|
|
973
958
|
const specs = (parsed && Array.isArray(parsed.specs)) ? parsed.specs : [];
|
|
974
959
|
if (opts.project) return specs.filter(s => s && s.owner_project === opts.project);
|
|
975
960
|
return specs.slice();
|
|
@@ -1090,7 +1075,6 @@ async function runHealthcheck(spec) {
|
|
|
1090
1075
|
// waitForFirstHealth on the first pass. Callers MUST be outside any other
|
|
1091
1076
|
// lock — this acquires its own.
|
|
1092
1077
|
function _markHealthy(name, now) {
|
|
1093
|
-
const statePath = _getStatePath();
|
|
1094
1078
|
shared.mutateManagedProcesses((data) => {
|
|
1095
1079
|
if (!data || !Array.isArray(data.specs)) return data;
|
|
1096
1080
|
const rec = data.specs.find(s => s && s.name === name);
|
|
@@ -1404,7 +1388,6 @@ function _runManagedReconcile(opts) {
|
|
|
1404
1388
|
rotatedLogs: 0,
|
|
1405
1389
|
malformed: 0,
|
|
1406
1390
|
};
|
|
1407
|
-
const statePath = _getStatePath();
|
|
1408
1391
|
const ttlPidsToKill = [];
|
|
1409
1392
|
const survivors = []; // [{name, log_path}] post-mutation, used for log rotation + bootReconcile probes
|
|
1410
1393
|
shared.mutateManagedProcesses((data) => {
|
|
@@ -1459,7 +1442,6 @@ function sweepManagedSpawn(opts) {
|
|
|
1459
1442
|
// rely on the per-spec loop to refresh post-success, or item 7 boot reconcile
|
|
1460
1443
|
// to (re)establish initial truth on engine restart).
|
|
1461
1444
|
function _markBootProbe(name, result) {
|
|
1462
|
-
const statePath = _getStatePath();
|
|
1463
1445
|
shared.mutateManagedProcesses((data) => {
|
|
1464
1446
|
if (!data || !Array.isArray(data.specs)) return data;
|
|
1465
1447
|
const rec = data.specs.find(s => s && s.name === name);
|
|
@@ -1510,7 +1492,6 @@ function removeManagedSpecsForProject(projectName) {
|
|
|
1510
1492
|
if (typeof projectName !== 'string' || projectName.length === 0) {
|
|
1511
1493
|
return { killed: 0, unlinked: 0, scanned: 0 };
|
|
1512
1494
|
}
|
|
1513
|
-
const statePath = _getStatePath();
|
|
1514
1495
|
let toKill = [];
|
|
1515
1496
|
let logPaths = [];
|
|
1516
1497
|
shared.mutateManagedProcesses((data) => {
|
|
@@ -1546,7 +1527,6 @@ function removeManagedSpecsForProject(projectName) {
|
|
|
1546
1527
|
|
|
1547
1528
|
module.exports = {
|
|
1548
1529
|
MANAGED_SPAWN_FILENAME: MANAGED_SPAWN_FILENAME,
|
|
1549
|
-
MANAGED_PROCESSES_STATE_FILE: MANAGED_PROCESSES_STATE_FILE,
|
|
1550
1530
|
MANAGED_LOGS_DIR: MANAGED_LOGS_DIR,
|
|
1551
1531
|
INVALID_WORKDIR_REASON_PREFIX: INVALID_WORKDIR_REASON_PREFIX,
|
|
1552
1532
|
HEALTHCHECK_TYPES: HEALTHCHECK_TYPES,
|
|
@@ -1561,7 +1541,6 @@ module.exports = {
|
|
|
1561
1541
|
recordManagedBatch: recordManagedBatch,
|
|
1562
1542
|
removeManagedSpec: removeManagedSpec,
|
|
1563
1543
|
listManagedSpecs: listManagedSpecs,
|
|
1564
|
-
getStatePath: _getStatePath,
|
|
1565
1544
|
// Item 3 (P-9c1f47a6): healthcheck implementations + first-pass waiter.
|
|
1566
1545
|
runHealthcheck: runHealthcheck,
|
|
1567
1546
|
waitForFirstHealth: waitForFirstHealth,
|
package/engine/meeting.js
CHANGED
|
@@ -10,6 +10,7 @@ const { safeJson, uid, log, ts, ENGINE_DEFAULTS, WORK_TYPE, DISPATCH_RESULT, MEE
|
|
|
10
10
|
const queries = require('./queries');
|
|
11
11
|
const { getDispatch, getConfig } = queries;
|
|
12
12
|
const { renderPlaybook } = require('./playbook');
|
|
13
|
+
const dispatchModule = require('./dispatch');
|
|
13
14
|
|
|
14
15
|
/** Patterns that indicate an agent returned no meaningful output */
|
|
15
16
|
const EMPTY_OUTPUT_PATTERNS = ['(no output)', '(no findings)', '(no response)'];
|
|
@@ -514,7 +515,7 @@ function discoverMeetingWork(config) {
|
|
|
514
515
|
|
|
515
516
|
// Pre-flight validation: meetings missing required template vars (agenda)
|
|
516
517
|
// would otherwise fail playbook rendering on every tick (~1/min), spamming
|
|
517
|
-
// log
|
|
518
|
+
// the engine log with the same "missing required template variables: agenda"
|
|
518
519
|
// error. Skip them silently here; emit one structured warning per meeting
|
|
519
520
|
// ID per process so the operator still has signal without the spam.
|
|
520
521
|
if (!meeting.agenda || !String(meeting.agenda).trim()) {
|
|
@@ -757,12 +758,11 @@ function addMeetingNote(meetingId, note) {
|
|
|
757
758
|
|
|
758
759
|
function _killMeetingDispatches(meetingId) {
|
|
759
760
|
try {
|
|
760
|
-
const DISPATCH_PATH = path.join(shared.MINIONS_DIR, 'engine', 'dispatch.json');
|
|
761
761
|
const tmpDir = path.join(shared.MINIONS_DIR, 'engine', 'tmp');
|
|
762
762
|
const entriesToStop = [];
|
|
763
763
|
const filesToDelete = [];
|
|
764
764
|
const dispatchDirsToRemove = [];
|
|
765
|
-
|
|
765
|
+
dispatchModule.mutateDispatch((dp) => {
|
|
766
766
|
dp.pending = Array.isArray(dp.pending) ? dp.pending : [];
|
|
767
767
|
dp.active = Array.isArray(dp.active) ? dp.active : [];
|
|
768
768
|
dp.completed = Array.isArray(dp.completed) ? dp.completed : [];
|
|
@@ -828,7 +828,7 @@ function advanceMeetingRound(meetingId) {
|
|
|
828
828
|
if (!existing || existing.status === MEETING_STATUS.COMPLETED || existing.status === MEETING_STATUS.ARCHIVED) return null;
|
|
829
829
|
|
|
830
830
|
// CRITICAL: kill BEFORE acquiring the meeting lock. _killMeetingDispatches
|
|
831
|
-
//
|
|
831
|
+
// mutates dispatch state and shells out to kill processes — never
|
|
832
832
|
// run that under the meeting lock (per CLAUDE.md, lock callbacks must
|
|
833
833
|
// stay fast and never spawn / kill / await).
|
|
834
834
|
_killMeetingDispatches(meetingId);
|
|
@@ -923,13 +923,13 @@ function checkMeetingTimeouts(config) {
|
|
|
923
923
|
// BUG-H11: when a hard timeout will fire, kill in-flight meeting
|
|
924
924
|
// dispatches BEFORE entering the meeting lock so they stop consuming
|
|
925
925
|
// worker slots / LLM tokens immediately rather than draining over the
|
|
926
|
-
// next tens of minutes. _killMeetingDispatches takes the dispatch
|
|
926
|
+
// next tens of minutes. _killMeetingDispatches takes the dispatch
|
|
927
927
|
// lock and shells out to kill processes — per CLAUDE.md and the
|
|
928
928
|
// advanceMeetingRound/endMeeting/deleteMeeting pattern at L834, that
|
|
929
929
|
// MUST run outside the meeting lock. We pre-evaluate eligibility from
|
|
930
930
|
// the snapshot using the same predicate the mutate callback below
|
|
931
931
|
// re-checks under lock; if the timeout has already advanced/cleared,
|
|
932
|
-
// the kill is a no-op
|
|
932
|
+
// the kill is a no-op in dispatch state (no matching meetingId entries).
|
|
933
933
|
const snapshotRoundName = snapshot.status === 'investigating'
|
|
934
934
|
? 'investigate'
|
|
935
935
|
: snapshot.status === 'debating'
|
package/engine/metrics-store.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// engine/metrics-store.js — SQL-backed
|
|
1
|
+
// engine/metrics-store.js — SQL-backed engine metrics.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// Metrics expose a flat object whose top-level keys mix
|
|
4
4
|
// per-agent counters with reserved keys (`_engine`, `_daily`,
|
|
5
5
|
// `_contextPressure`). The store reconstructs that legacy shape for
|
|
6
6
|
// reads, and reverses it for writes — each (kind, key) row holds one
|
|
@@ -148,12 +148,7 @@ function repointItemNoteLinks(item, resolve) {
|
|
|
148
148
|
return fixed;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
// Repair broken note links across every
|
|
152
|
-
// the central one), routed through shared.mutateWorkItems so the canonical SQL
|
|
153
|
-
// store AND its JSON mirror stay consistent — the same write path the live
|
|
154
|
-
// rewriter uses. Idempotent: only touches links that are provably broken AND
|
|
155
|
-
// resolvable, so once healed a re-run writes nothing (skipWriteIfUnchanged).
|
|
156
|
-
// Safe to call on every engine boot. Per-file try/catch isolates a bad scope.
|
|
151
|
+
// Repair broken note links across every project scope plus central.
|
|
157
152
|
function runNoteLinkBackfill(opts = {}) {
|
|
158
153
|
const shared = require('./shared');
|
|
159
154
|
const minionsDir = opts.minionsDir || shared.MINIONS_DIR;
|
|
@@ -172,14 +167,14 @@ function runNoteLinkBackfill(opts = {}) {
|
|
|
172
167
|
const projects = Array.isArray(config.projects) ? config.projects : [];
|
|
173
168
|
const candidates = [];
|
|
174
169
|
for (const p of projects) {
|
|
175
|
-
if (p && p.name) candidates.push(
|
|
170
|
+
if (p && p.name) candidates.push(p);
|
|
176
171
|
}
|
|
177
|
-
candidates.push(
|
|
172
|
+
candidates.push('central');
|
|
178
173
|
|
|
179
174
|
let scanned = 0, fixedItems = 0, fixedLinks = 0;
|
|
180
|
-
for (const
|
|
175
|
+
for (const scope of candidates) {
|
|
181
176
|
try {
|
|
182
|
-
shared.mutateWorkItems(
|
|
177
|
+
shared.mutateWorkItems(scope, (items) => {
|
|
183
178
|
if (!Array.isArray(items)) return items;
|
|
184
179
|
for (const item of items) {
|
|
185
180
|
if (!item) continue;
|
|
@@ -191,7 +186,7 @@ function runNoteLinkBackfill(opts = {}) {
|
|
|
191
186
|
}
|
|
192
187
|
return items;
|
|
193
188
|
});
|
|
194
|
-
} catch { /*
|
|
189
|
+
} catch { /* one bad scope cannot block the others */ }
|
|
195
190
|
}
|
|
196
191
|
|
|
197
192
|
return { scanned, fixedItems, fixedLinks };
|
package/engine/pipeline.js
CHANGED
|
@@ -8,20 +8,18 @@ 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,
|
|
11
|
+
const { safeJson, safeJsonArr, safeJsonNoRestore, safeWrite, safeRead, safeReadDir, uid, log, ts, dateStamp, mutateJsonFileLocked, mutateWorkItems, mutatePipelineRuns, 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, isPrdArchived } = shared;
|
|
12
12
|
const routing = require('./routing');
|
|
13
13
|
const http = require('http');
|
|
14
14
|
const { shouldRunNow } = require('./scheduler');
|
|
15
|
+
const smallStateStore = require('./small-state-store');
|
|
15
16
|
|
|
16
17
|
// All module-relative paths flow through MINIONS_DIR so MINIONS_TEST_DIR
|
|
17
18
|
// (set by test/unit.test.js createTestMinionsDir) consistently redirects
|
|
18
19
|
// pipeline writes into the temp root instead of the live runtime root.
|
|
19
20
|
const PIPELINES_DIR = path.join(MINIONS_DIR, 'pipelines');
|
|
20
|
-
const PIPELINE_RUNS_PATH = path.join(MINIONS_DIR, 'engine', 'pipeline-runs.json');
|
|
21
|
-
const CENTRAL_WI_PATH = path.join(MINIONS_DIR, 'work-items.json');
|
|
22
21
|
const MEETINGS_DIR = path.join(MINIONS_DIR, 'meetings');
|
|
23
22
|
const PLANS_DIR = path.join(MINIONS_DIR, 'plans');
|
|
24
|
-
const PRD_DIR = path.join(MINIONS_DIR, 'prd');
|
|
25
23
|
const NOTES_INBOX_DIR = path.join(MINIONS_DIR, 'notes', 'inbox');
|
|
26
24
|
const NOTES_ARCHIVE_DIR = path.join(MINIONS_DIR, 'notes', 'archive');
|
|
27
25
|
const CONFIG_PATH = path.join(MINIONS_DIR, 'config.json');
|
|
@@ -74,22 +72,11 @@ function deletePipeline(id) {
|
|
|
74
72
|
// ── Run State ────────────────────────────────────────────────────────────────
|
|
75
73
|
|
|
76
74
|
function getPipelineRuns() {
|
|
77
|
-
return
|
|
75
|
+
return smallStateStore.readPipelineRuns();
|
|
78
76
|
}
|
|
79
77
|
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
// projects/<name>/work-items.json) are passive, best-effort mirrors that can
|
|
83
|
-
// legitimately lag behind a just-completed write (mirror failures are
|
|
84
|
-
// swallowed by design so they never block the SQL write; see
|
|
85
|
-
// work-items-store.js's `_mirrorJsonFromSql` comments). Reading those JSON
|
|
86
|
-
// files directly (via safeJson/safeJsonArr) can therefore observe a work
|
|
87
|
-
// item whose `status` mirrored to DONE but whose `resultSummary` write
|
|
88
|
-
// hasn't landed in the mirror yet — which silently degraded stage-output
|
|
89
|
-
// template substitution ({{stages.<id>.output}}) to the stage's own work
|
|
90
|
-
// item id instead of its real completion text. Route every work-item read
|
|
91
|
-
// in this module through the SQL-backed store so pipeline state always sees
|
|
92
|
-
// the authoritative record.
|
|
78
|
+
// Always read work items from the authoritative SQL store so stage-output
|
|
79
|
+
// template substitution sees the latest completion metadata.
|
|
93
80
|
function _allWorkItems() {
|
|
94
81
|
return require('./work-items-store').readAllWorkItems();
|
|
95
82
|
}
|
|
@@ -319,8 +306,8 @@ function _pipelineProjectSlug(project) {
|
|
|
319
306
|
return project ? shared.safeSlugComponent(project.name || 'project', 32).toLowerCase() : 'central';
|
|
320
307
|
}
|
|
321
308
|
|
|
322
|
-
function
|
|
323
|
-
return project
|
|
309
|
+
function _pipelineWorkItemScope(project) {
|
|
310
|
+
return project || 'central';
|
|
324
311
|
}
|
|
325
312
|
|
|
326
313
|
// ── Condition Evaluation ─────────────────────────────────────────────────────
|
|
@@ -435,7 +422,7 @@ function executeTaskStage(stage, stageState, run, config, pipeline = {}) {
|
|
|
435
422
|
for (const project of projectResolution.projects) {
|
|
436
423
|
const projectSlug = _pipelineProjectSlug(project);
|
|
437
424
|
const id = `PL-${run.runId.slice(4, 12)}-${stage.id}-${i}${projectResolution.projects.length > 1 || project ? '-' + projectSlug : ''}`;
|
|
438
|
-
const
|
|
425
|
+
const wiScope = _pipelineWorkItemScope(project);
|
|
439
426
|
const wiType = routing.normalizeWorkType(item.type || stage.taskType, WORK_TYPE.EXPLORE);
|
|
440
427
|
// W-mp8ho6w500034a58: read-only stages don't commit, so a pipeline
|
|
441
428
|
// branch label is meaningless to them — omit it entirely so the
|
|
@@ -443,7 +430,7 @@ function executeTaskStage(stage, stageState, run, config, pipeline = {}) {
|
|
|
443
430
|
const wiBranch = READ_ONLY_ROOT_TASK_TYPES.has(wiType)
|
|
444
431
|
? null
|
|
445
432
|
: `pipeline/${run.pipelineId}/${stage.id}`;
|
|
446
|
-
mutateWorkItems(
|
|
433
|
+
mutateWorkItems(wiScope, workItems => {
|
|
447
434
|
if (workItems.some(w => w.id === id)) { createdIds.push(id); return workItems; }
|
|
448
435
|
workItems.push({
|
|
449
436
|
id,
|
|
@@ -574,12 +561,10 @@ const _VALID_EXISTING_PRD_STATUSES = new Set(Object.values(PLAN_STATUS));
|
|
|
574
561
|
// permanently skip re-running plan-to-prd for a plan that never actually got
|
|
575
562
|
// a usable PRD. Require both a recognized top-level status AND an array
|
|
576
563
|
// `missing_features` field before counting the PRD as an existing conversion.
|
|
577
|
-
function _findExistingPrdForPlan(planFile
|
|
564
|
+
function _findExistingPrdForPlan(planFile) {
|
|
578
565
|
const prdFiles = require('./prd-store').listPrdRows().filter(row => !row.archived);
|
|
579
566
|
const planKey = _canonicalPlanName(planFile);
|
|
580
567
|
for (const { filename: pf, plan: prd } of prdFiles) {
|
|
581
|
-
// safeJsonNoRestore: PRDs are terminal artifacts — never restore archived
|
|
582
|
-
// PRDs from a stale .backup sidecar (W-mouptdh1000h9f39).
|
|
583
568
|
if (!prd?.source_plan || _canonicalPlanName(path.basename(String(prd.source_plan))) !== planKey) continue;
|
|
584
569
|
if (!(_VALID_EXISTING_PRD_STATUSES.has(prd.status) || isPrdArchived(prd)) || !Array.isArray(prd.missing_features)) {
|
|
585
570
|
log('warn', `Pipeline plan-to-prd: found PRD "${pf}" for plan "${planFile}" but it lacks a valid status/missing_features (garbage/incomplete conversion) — treating plan-to-prd as not yet done`);
|
|
@@ -623,8 +608,7 @@ async function executePlanStage(stage, stageState, run, config, pipeline = {}) {
|
|
|
623
608
|
log('info', `Pipeline ${run.pipelineId}: reconciling plan stage — adopting existing plan "${existingPlanFile}"`);
|
|
624
609
|
|
|
625
610
|
// Check if a PRD already exists for this plan (skip plan-to-prd entirely)
|
|
626
|
-
const
|
|
627
|
-
const existingPrdFile = _findExistingPrdForPlan(existingPlanFile, prdDir);
|
|
611
|
+
const existingPrdFile = _findExistingPrdForPlan(existingPlanFile);
|
|
628
612
|
if (existingPrdFile) {
|
|
629
613
|
log('info', `Pipeline ${run.pipelineId}: PRD "${existingPrdFile}" already exists for plan "${existingPlanFile}" — skipping plan-to-prd`);
|
|
630
614
|
return {
|
|
@@ -636,10 +620,10 @@ async function executePlanStage(stage, stageState, run, config, pipeline = {}) {
|
|
|
636
620
|
// Adopt or create plan-to-prd WIs atomically under lock for each target project.
|
|
637
621
|
const adoptedWiIds = [];
|
|
638
622
|
for (const project of targetProjects) {
|
|
639
|
-
const
|
|
623
|
+
const wiScope = _pipelineWorkItemScope(project);
|
|
640
624
|
const wiId = wiIdForProject(project);
|
|
641
625
|
let adoptedWiId = wiId;
|
|
642
|
-
mutateWorkItems(
|
|
626
|
+
mutateWorkItems(wiScope, workItems => {
|
|
643
627
|
const existing = workItems.find(w => w.type === WORK_TYPE.PLAN_TO_PRD && w.planFile === existingPlanFile);
|
|
644
628
|
if (existing) {
|
|
645
629
|
existing._pipelineRun = run.runId;
|
|
@@ -744,9 +728,9 @@ async function executePlanStage(stage, stageState, run, config, pipeline = {}) {
|
|
|
744
728
|
|
|
745
729
|
const createdWiIds = [];
|
|
746
730
|
for (const project of targetProjects) {
|
|
747
|
-
const
|
|
731
|
+
const wiScope = _pipelineWorkItemScope(project);
|
|
748
732
|
const wiId = wiIdForProject(project);
|
|
749
|
-
mutateWorkItems(
|
|
733
|
+
mutateWorkItems(wiScope, workItems => {
|
|
750
734
|
if (!workItems.some(w => w.id === wiId)) {
|
|
751
735
|
workItems.push({
|
|
752
736
|
id: wiId,
|
|
@@ -986,7 +970,6 @@ function isStageComplete(stage, stageState, run, config) {
|
|
|
986
970
|
if (!prdDone) return false;
|
|
987
971
|
|
|
988
972
|
// Discover PRDs and their work items — collect into local arrays, then merge into artifacts
|
|
989
|
-
const prdDir = PRD_DIR;
|
|
990
973
|
const plans = artifacts.plans || [];
|
|
991
974
|
const discoveredPrds = [];
|
|
992
975
|
const discoveredWiIds = [];
|
|
@@ -1019,9 +1002,8 @@ function isStageComplete(stage, stageState, run, config) {
|
|
|
1019
1002
|
// Auto-approve if configured
|
|
1020
1003
|
if (stage.autoApprove && artifacts.prds?.length > 0) {
|
|
1021
1004
|
for (const prdFile of artifacts.prds) {
|
|
1022
|
-
const prdPath = path.join(prdDir, prdFile);
|
|
1023
1005
|
let approved = false;
|
|
1024
|
-
|
|
1006
|
+
require('./prd-store').mutatePrd(prdFile, (prd) => {
|
|
1025
1007
|
if (prd && prd.status === PLAN_STATUS.AWAITING_APPROVAL) {
|
|
1026
1008
|
prd.status = PLAN_STATUS.APPROVED;
|
|
1027
1009
|
prd.approvedAt = ts();
|
|
@@ -1029,7 +1011,7 @@ function isStageComplete(stage, stageState, run, config) {
|
|
|
1029
1011
|
approved = true;
|
|
1030
1012
|
}
|
|
1031
1013
|
return prd;
|
|
1032
|
-
}
|
|
1014
|
+
});
|
|
1033
1015
|
if (approved) log('info', `Pipeline ${run.pipelineId}: auto-approved PRD ${prdFile}`);
|
|
1034
1016
|
}
|
|
1035
1017
|
}
|
|
@@ -1047,7 +1029,7 @@ function isStageComplete(stage, stageState, run, config) {
|
|
|
1047
1029
|
if (prIds.length === 0) return true; // nothing to merge
|
|
1048
1030
|
const projects = shared.getProjects(config);
|
|
1049
1031
|
for (const project of projects) {
|
|
1050
|
-
const prs =
|
|
1032
|
+
const prs = shared.readPullRequests(project);
|
|
1051
1033
|
for (const prId of prIds) {
|
|
1052
1034
|
const pr = shared.findPrRecord(prs, prId, project);
|
|
1053
1035
|
if (pr && pr.status !== PR_STATUS.MERGED && pr.status !== PR_STATUS.ABANDONED) return false;
|
package/engine/playbook.js
CHANGED
|
@@ -1077,7 +1077,7 @@ function buildSystemPrompt(agentId, config, project) {
|
|
|
1077
1077
|
prompt += `2. ${getRepoHostToolRule(project)}\n`;
|
|
1078
1078
|
prompt += `3. Follow the project conventions in CLAUDE.md if present\n`;
|
|
1079
1079
|
prompt += `4. Write learnings to the path specified in the task prompt (format: \`notes/inbox/{agent}-{work-item-id}-{date}-{time}.md\`)\n`;
|
|
1080
|
-
prompt += `5. Agent status is managed by the engine
|
|
1080
|
+
prompt += `5. Agent status is managed by the engine — agents do not need to track their own status\n\n`;
|
|
1081
1081
|
|
|
1082
1082
|
// The lean system prompt (identity + rules) does not contain task-dispatch
|
|
1083
1083
|
// sections (## Your Task, ## Tools, ## Constraints) by design — those live
|
|
@@ -45,9 +45,15 @@ class PooledAgentProcess extends EventEmitter {
|
|
|
45
45
|
* @param {string} [opts.effort]
|
|
46
46
|
* @param {Array} [opts.mcpServers]
|
|
47
47
|
* @param {Array<string>} [opts.hermeticDirs]
|
|
48
|
-
* @param {
|
|
48
|
+
* @param {object} [opts.processEnv] - worker-process environment
|
|
49
|
+
* @param {object} [opts.sessionContext] - trusted values scoped to this lease
|
|
50
|
+
* @param {string} [opts.systemPromptText] - trusted system instructions
|
|
51
|
+
* @param {string} opts.promptText - task/user prompt only
|
|
49
52
|
*/
|
|
50
|
-
constructor({
|
|
53
|
+
constructor({
|
|
54
|
+
pool, dispatchId, cwd, model, effort, mcpServers, hermeticDirs,
|
|
55
|
+
processEnv, sessionContext, systemPromptText, promptText,
|
|
56
|
+
}) {
|
|
51
57
|
super();
|
|
52
58
|
this.pid = null;
|
|
53
59
|
this.killed = false;
|
|
@@ -63,14 +69,21 @@ class PooledAgentProcess extends EventEmitter {
|
|
|
63
69
|
// Fire-and-forget, mirroring child_process.spawn()'s async nature — the
|
|
64
70
|
// facade is usable (event listeners can be attached) immediately, before
|
|
65
71
|
// the underlying ACP worker has actually been acquired.
|
|
66
|
-
this._start({
|
|
72
|
+
this._start({
|
|
73
|
+
cwd, model, effort, mcpServers, hermeticDirs,
|
|
74
|
+
processEnv, sessionContext, systemPromptText, promptText,
|
|
75
|
+
});
|
|
67
76
|
}
|
|
68
77
|
|
|
69
|
-
async _start({
|
|
78
|
+
async _start({
|
|
79
|
+
cwd, model, effort, mcpServers, hermeticDirs,
|
|
80
|
+
processEnv, sessionContext, systemPromptText, promptText,
|
|
81
|
+
}) {
|
|
70
82
|
let handle;
|
|
71
83
|
try {
|
|
72
84
|
handle = await this._pool.acquireWorker({
|
|
73
85
|
dispatchId: this._dispatchId, cwd, model, effort, mcpServers, hermeticDirs,
|
|
86
|
+
processEnv, sessionContext,
|
|
74
87
|
});
|
|
75
88
|
} catch (err) {
|
|
76
89
|
// Mirrors child_process's 'error' event (spawn failed) — engine.js's
|
|
@@ -95,28 +108,35 @@ class PooledAgentProcess extends EventEmitter {
|
|
|
95
108
|
this.emit('acquired');
|
|
96
109
|
|
|
97
110
|
let sawError = false;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
111
|
+
try {
|
|
112
|
+
await handle.stream(promptText, {
|
|
113
|
+
systemPromptText,
|
|
114
|
+
onChunk: (text) => {
|
|
115
|
+
if (!text) return;
|
|
116
|
+
this._writeStdoutEvent({ type: 'assistant.message_delta', data: { deltaContent: text } });
|
|
117
|
+
},
|
|
118
|
+
onDone: (result) => {
|
|
119
|
+
this._writeStdoutEvent({
|
|
120
|
+
type: 'result',
|
|
121
|
+
sessionId: handle.sessionId || null,
|
|
122
|
+
usage: {
|
|
123
|
+
totalApiDurationMs: Date.now() - this._startedAt,
|
|
124
|
+
stopReason: (result && result.stopReason) || null,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
onError: (err) => {
|
|
129
|
+
sawError = true;
|
|
130
|
+
try { this.stderr.write(String((err && err.message) || err) + '\n'); } catch { /* best-effort */ }
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
} catch (err) {
|
|
134
|
+
sawError = true;
|
|
135
|
+
try { this.stderr.write(String((err && err.message) || err) + '\n'); } catch { /* best-effort */ }
|
|
136
|
+
} finally {
|
|
137
|
+
try { handle.release(); } catch { /* best-effort */ }
|
|
138
|
+
this._handle = null;
|
|
139
|
+
}
|
|
120
140
|
this._finish(sawError ? 1 : 0);
|
|
121
141
|
}
|
|
122
142
|
|