@yemi33/minions 0.1.2379 → 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/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-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/ado-status.js +5 -5
- package/engine/ado.js +20 -25
- package/engine/agent-worker-pool.js +58 -1
- 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/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 +147 -343
- 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
|
package/engine/prd-store.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
// engine/prd-store.js
|
|
2
2
|
//
|
|
3
|
-
// SQL-authoritative PRD state.
|
|
4
|
-
// existing callers, but no runtime PRD JSON is read or written here.
|
|
5
|
-
|
|
6
|
-
const path = require('path');
|
|
3
|
+
// SQL-authoritative PRD state.
|
|
7
4
|
|
|
8
5
|
function _toMs(v) {
|
|
9
6
|
if (v == null) return null;
|
|
@@ -13,28 +10,12 @@ function _toMs(v) {
|
|
|
13
10
|
}
|
|
14
11
|
function _bool01(v) { return v ? 1 : 0; }
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
function parsePrdPath(filePath) {
|
|
21
|
-
if (!filePath || typeof filePath !== 'string') return null;
|
|
22
|
-
let minionsDir;
|
|
23
|
-
try { minionsDir = require('./shared').MINIONS_DIR; } catch { return null; }
|
|
24
|
-
if (!minionsDir) return null;
|
|
25
|
-
const norm = (p) => path.resolve(p).replace(/\\/g, '/').toLowerCase();
|
|
26
|
-
const full = norm(filePath);
|
|
27
|
-
const prdRoot = norm(path.join(minionsDir, 'prd'));
|
|
28
|
-
const archiveRoot = norm(path.join(minionsDir, 'prd', 'archive'));
|
|
29
|
-
if (!full.endsWith('.json')) return null;
|
|
30
|
-
const base = path.basename(filePath);
|
|
31
|
-
if (full === norm(path.join(archiveRoot, base)) && full.startsWith(archiveRoot + '/')) {
|
|
32
|
-
return { filename: base, archived: 1 };
|
|
33
|
-
}
|
|
34
|
-
if (full === norm(path.join(prdRoot, base)) && full.startsWith(prdRoot + '/')) {
|
|
35
|
-
return { filename: base, archived: 0 };
|
|
13
|
+
function _validatePrdFilename(filename) {
|
|
14
|
+
if (typeof filename !== 'string' || !filename.endsWith('.json')
|
|
15
|
+
|| filename.includes('/') || filename.includes('\\') || filename === '.json') {
|
|
16
|
+
throw new TypeError('PRD filename must be a basename ending in .json');
|
|
36
17
|
}
|
|
37
|
-
return
|
|
18
|
+
return filename;
|
|
38
19
|
}
|
|
39
20
|
|
|
40
21
|
// (filename, archived) → plans.id, preferring the same archived bucket.
|
|
@@ -48,6 +29,22 @@ function _resolvePlanId(db, sourcePlan, archived) {
|
|
|
48
29
|
|
|
49
30
|
// Core upsert: write/replace the prds row + its prd_items + prd_verify_prs for
|
|
50
31
|
// (filename, archived). Returns the prds.id. Runs inside a transaction.
|
|
32
|
+
function _clearWorkItemLinks(db, prdId, predicate = '', params = []) {
|
|
33
|
+
db.prepare(`
|
|
34
|
+
UPDATE work_items SET prd_item_id=NULL
|
|
35
|
+
WHERE prd_item_id IN (
|
|
36
|
+
SELECT id FROM prd_items WHERE prd_id=? ${predicate}
|
|
37
|
+
)
|
|
38
|
+
`).run(prdId, ...params);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function _deletePrdRow(db, prdId) {
|
|
42
|
+
_clearWorkItemLinks(db, prdId);
|
|
43
|
+
db.prepare('DELETE FROM prd_items WHERE prd_id=?').run(prdId);
|
|
44
|
+
db.prepare('DELETE FROM prd_verify_prs WHERE prd_id=?').run(prdId);
|
|
45
|
+
db.prepare('DELETE FROM prds WHERE id=?').run(prdId);
|
|
46
|
+
}
|
|
47
|
+
|
|
51
48
|
function _upsertPrd(db, filename, archived, prd, now) {
|
|
52
49
|
const sourcePlan = prd.source_plan || prd.sourcePlan || null;
|
|
53
50
|
const planId = _resolvePlanId(db, sourcePlan, archived);
|
|
@@ -98,10 +95,13 @@ function _upsertPrd(db, filename, archived, prd, now) {
|
|
|
98
95
|
for (const ft of features) { if (ft && typeof ft === 'object' && ft.id) keepIds.push(String(ft.id)); }
|
|
99
96
|
// Drop id-bearing items no longer on the PRD, plus all NULL-feature_id rows.
|
|
100
97
|
if (keepIds.length) {
|
|
98
|
+
const predicate = `AND feature_id IS NOT NULL AND feature_id NOT IN (${keepIds.map(() => '?').join(',')})`;
|
|
99
|
+
_clearWorkItemLinks(db, prdId, predicate, keepIds);
|
|
101
100
|
db.prepare(
|
|
102
101
|
`DELETE FROM prd_items WHERE prd_id=? AND feature_id IS NOT NULL AND feature_id NOT IN (${keepIds.map(() => '?').join(',')})`
|
|
103
102
|
).run(prdId, ...keepIds);
|
|
104
103
|
} else {
|
|
104
|
+
_clearWorkItemLinks(db, prdId, 'AND feature_id IS NOT NULL');
|
|
105
105
|
db.prepare('DELETE FROM prd_items WHERE prd_id=? AND feature_id IS NOT NULL').run(prdId);
|
|
106
106
|
}
|
|
107
107
|
db.prepare('DELETE FROM prd_items WHERE prd_id=? AND feature_id IS NULL').run(prdId);
|
|
@@ -181,27 +181,51 @@ function _upsertPrd(db, filename, archived, prd, now) {
|
|
|
181
181
|
return prdId;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
function writePrd(
|
|
185
|
-
|
|
186
|
-
if (!
|
|
187
|
-
throw new TypeError('prd-store.writePrd requires a
|
|
184
|
+
function writePrd(filename, prd, { archived = false } = {}) {
|
|
185
|
+
_validatePrdFilename(filename);
|
|
186
|
+
if (!prd || typeof prd !== 'object' || Array.isArray(prd)) {
|
|
187
|
+
throw new TypeError('prd-store.writePrd requires a filename and object');
|
|
188
188
|
}
|
|
189
189
|
const { getDb, withTransaction } = require('./db');
|
|
190
190
|
const db = getDb();
|
|
191
|
-
const
|
|
192
|
-
|
|
191
|
+
const archivedValue = archived ? 1 : 0;
|
|
192
|
+
const prdId = withTransaction(db, () => _upsertPrd(db, filename, archivedValue, prd, Date.now()));
|
|
193
|
+
require('./db-events').emitStateEvent('prds', { filename, archived: archivedValue });
|
|
193
194
|
return { ok: true, id: prdId, plan: prd };
|
|
194
195
|
}
|
|
195
196
|
|
|
196
|
-
function
|
|
197
|
-
|
|
198
|
-
if (!
|
|
199
|
-
throw new TypeError('prd-store.
|
|
197
|
+
function createPrdUnique(baseFilename, prd) {
|
|
198
|
+
_validatePrdFilename(baseFilename);
|
|
199
|
+
if (!prd || typeof prd !== 'object' || Array.isArray(prd)) {
|
|
200
|
+
throw new TypeError('prd-store.createPrdUnique requires a PRD object');
|
|
201
|
+
}
|
|
202
|
+
const stem = baseFilename.slice(0, -'.json'.length);
|
|
203
|
+
const { getDb, withTransaction } = require('./db');
|
|
204
|
+
const db = getDb();
|
|
205
|
+
const filename = withTransaction(db, () => {
|
|
206
|
+
const exists = db.prepare('SELECT 1 FROM prds WHERE filename=? LIMIT 1');
|
|
207
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
208
|
+
const candidate = attempt === 0 ? baseFilename : `${stem}-${attempt}.json`;
|
|
209
|
+
if (exists.get(candidate)) continue;
|
|
210
|
+
_upsertPrd(db, candidate, 0, prd, Date.now());
|
|
211
|
+
return candidate;
|
|
212
|
+
}
|
|
213
|
+
throw new Error('Could not reserve unique PRD filename after 100 attempts');
|
|
214
|
+
});
|
|
215
|
+
require('./db-events').emitStateEvent('prds', { filename, archived: 0 });
|
|
216
|
+
return filename;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function mutatePrd(filename, mutator, { archived = false, defaultValue = {} } = {}) {
|
|
220
|
+
_validatePrdFilename(filename);
|
|
221
|
+
if (typeof mutator !== 'function') {
|
|
222
|
+
throw new TypeError('prd-store.mutatePrd requires a filename and mutator');
|
|
200
223
|
}
|
|
201
224
|
const { getDb, withTransaction } = require('./db');
|
|
202
225
|
const db = getDb();
|
|
203
226
|
const result = withTransaction(db, () => {
|
|
204
|
-
const
|
|
227
|
+
const archivedValue = archived ? 1 : 0;
|
|
228
|
+
const row = db.prepare('SELECT data FROM prds WHERE filename=? AND archived=?').get(filename, archivedValue);
|
|
205
229
|
let current = row ? JSON.parse(row.data) : structuredClone(defaultValue);
|
|
206
230
|
if (!current || typeof current !== 'object' || Array.isArray(current)) current = structuredClone(defaultValue);
|
|
207
231
|
const next = mutator(current);
|
|
@@ -209,36 +233,33 @@ function mutatePrd(filePath, mutator, { defaultValue = {} } = {}) {
|
|
|
209
233
|
if (!finalData || typeof finalData !== 'object' || Array.isArray(finalData)) {
|
|
210
234
|
throw new TypeError('PRD mutator must return an object or mutate in place');
|
|
211
235
|
}
|
|
212
|
-
_upsertPrd(db,
|
|
236
|
+
_upsertPrd(db, filename, archivedValue, finalData, Date.now());
|
|
213
237
|
return finalData;
|
|
214
238
|
});
|
|
215
|
-
require('./db-events').emitStateEvent('prds', { filename
|
|
239
|
+
require('./db-events').emitStateEvent('prds', { filename, archived: archived ? 1 : 0 });
|
|
216
240
|
return result;
|
|
217
241
|
}
|
|
218
242
|
|
|
219
|
-
function deletePrd(
|
|
220
|
-
|
|
221
|
-
if (!parsed) throw new TypeError('prd-store.deletePrd requires a PRD path');
|
|
243
|
+
function deletePrd(filename, { archived = false } = {}) {
|
|
244
|
+
_validatePrdFilename(filename);
|
|
222
245
|
const { getDb, withTransaction } = require('./db');
|
|
223
246
|
const db = getDb();
|
|
224
247
|
const removed = withTransaction(db, () => {
|
|
225
|
-
const
|
|
248
|
+
const archivedValue = archived ? 1 : 0;
|
|
249
|
+
const row = db.prepare('SELECT id FROM prds WHERE filename=? AND archived=?').get(filename, archivedValue);
|
|
226
250
|
if (!row) return false;
|
|
227
|
-
db
|
|
228
|
-
db.prepare('DELETE FROM prd_verify_prs WHERE prd_id=?').run(row.id);
|
|
229
|
-
db.prepare('DELETE FROM prds WHERE id=?').run(row.id);
|
|
251
|
+
_deletePrdRow(db, row.id);
|
|
230
252
|
return true;
|
|
231
253
|
});
|
|
232
|
-
if (removed) require('./db-events').emitStateEvent('prds', { filename
|
|
254
|
+
if (removed) require('./db-events').emitStateEvent('prds', { filename, archived: archived ? 1 : 0, removed: true });
|
|
233
255
|
return removed;
|
|
234
256
|
}
|
|
235
257
|
|
|
236
|
-
function readPrd(
|
|
237
|
-
|
|
238
|
-
if (!parsed) return null;
|
|
258
|
+
function readPrd(filename, { archived = false } = {}) {
|
|
259
|
+
_validatePrdFilename(filename);
|
|
239
260
|
const row = require('./db').getDb()
|
|
240
261
|
.prepare('SELECT data FROM prds WHERE filename=? AND archived=?')
|
|
241
|
-
.get(
|
|
262
|
+
.get(filename, archived ? 1 : 0);
|
|
242
263
|
return row ? JSON.parse(row.data) : null;
|
|
243
264
|
}
|
|
244
265
|
|
|
@@ -262,9 +283,7 @@ function restorePrdFromArchive(filename) {
|
|
|
262
283
|
if (!row) return null;
|
|
263
284
|
const prd = JSON.parse(row.data);
|
|
264
285
|
_upsertPrd(db, filename, 0, prd, Date.now());
|
|
265
|
-
db
|
|
266
|
-
db.prepare('DELETE FROM prd_verify_prs WHERE prd_id=?').run(row.id);
|
|
267
|
-
db.prepare('DELETE FROM prds WHERE id=?').run(row.id);
|
|
286
|
+
_deletePrdRow(db, row.id);
|
|
268
287
|
return prd;
|
|
269
288
|
});
|
|
270
289
|
if (restored) {
|
|
@@ -332,8 +351,8 @@ function listPrdRows() {
|
|
|
332
351
|
}
|
|
333
352
|
|
|
334
353
|
module.exports = {
|
|
335
|
-
parsePrdPath,
|
|
336
354
|
writePrd,
|
|
355
|
+
createPrdUnique,
|
|
337
356
|
mutatePrd,
|
|
338
357
|
deletePrd,
|
|
339
358
|
readPrd,
|
package/engine/preflight.js
CHANGED
|
@@ -266,11 +266,8 @@ function runPreflight(opts = {}) {
|
|
|
266
266
|
// discoverFromWorkItems / discoverFromPrs return [] without logging.
|
|
267
267
|
try {
|
|
268
268
|
const projectWarns = shared.projectWorkSourceWarnings(opts.config, (project) => {
|
|
269
|
-
const
|
|
270
|
-
const
|
|
271
|
-
const safeJson = shared.safeJson;
|
|
272
|
-
const wi = (() => { try { const a = safeJson(wiPath); return Array.isArray(a) ? a.length : 0; } catch { return 0; } })();
|
|
273
|
-
const pr = (() => { try { const a = safeJson(prPath); return Array.isArray(a) ? a.length : 0; } catch { return 0; } })();
|
|
269
|
+
const wi = (() => { try { return shared.readWorkItems(project).length; } catch { return 0; } })();
|
|
270
|
+
const pr = (() => { try { return shared.readPullRequests(project).length; } catch { return 0; } })();
|
|
274
271
|
return { workItems: wi, pullRequests: pr };
|
|
275
272
|
});
|
|
276
273
|
for (const w of projectWarns) {
|