@yemi33/minions 0.1.2379 → 0.1.2381
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 +3 -4
- package/dashboard/js/render-other.js +43 -23
- package/dashboard/js/render-prd.js +1 -1
- package/dashboard/js/render-work-items.js +13 -6
- package/dashboard/js/settings.js +40 -17
- package/dashboard.js +435 -768
- 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/live-checkout-mode.md +45 -26
- 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 +39 -44
- package/engine/github.js +20 -27
- package/engine/lifecycle.js +279 -354
- package/engine/live-checkout.js +193 -149
- 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 +13 -16
- 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 +4 -6
- package/engine/scheduler.js +0 -4
- package/engine/shared-branch-pr-reconcile.js +2 -3
- package/engine/shared.js +268 -699
- 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 +197 -358
- 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 +9 -9
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
|
@@ -402,6 +402,7 @@ const PLAYBOOK_REQUIRED_VARS = {
|
|
|
402
402
|
'implement': ['item_id', 'item_name', 'branch_name', 'project_path'],
|
|
403
403
|
'implement-shared': ['item_id', 'item_name', 'branch_name', 'worktree_path'],
|
|
404
404
|
'fix': ['pr_id', 'pr_branch'],
|
|
405
|
+
// M005 — SHERLOC two-phase build-fix. Same PR context as fix.
|
|
405
406
|
'build-fix-complex': ['pr_id', 'pr_branch'],
|
|
406
407
|
'review': ['pr_id', 'pr_branch'],
|
|
407
408
|
'build-and-test': ['pr_id', 'pr_branch', 'project_path'],
|
|
@@ -434,11 +435,14 @@ const PLAYBOOK_REQUIRED_VARS = {
|
|
|
434
435
|
'meeting-investigate': ['meeting_title', 'agenda'],
|
|
435
436
|
'meeting-debate': ['meeting_title', 'agenda'],
|
|
436
437
|
'meeting-conclude': ['meeting_title', 'agenda'],
|
|
437
|
-
// M005 — SHERLOC two-phase build-fix. Same required vars as fix (PR-context
|
|
438
|
-
// dispatch), but routed to build-fix-complex.md for multi-file failures.
|
|
439
|
-
'build-fix-complex': ['pr_id', 'pr_branch'],
|
|
440
438
|
};
|
|
441
439
|
|
|
440
|
+
const LIVE_VALIDATION_DEFERRED_PLAYBOOKS = new Set([
|
|
441
|
+
'implement',
|
|
442
|
+
'fix',
|
|
443
|
+
'build-fix-complex',
|
|
444
|
+
]);
|
|
445
|
+
|
|
442
446
|
/**
|
|
443
447
|
* Validate that all required template variables for a playbook type are present
|
|
444
448
|
* and non-empty in the provided vars object.
|
|
@@ -848,18 +852,11 @@ function renderPlaybook(type, vars) {
|
|
|
848
852
|
} catch (e) { log('warn', `qa-validate context render failed: ${e.message}`); }
|
|
849
853
|
}
|
|
850
854
|
|
|
851
|
-
//
|
|
852
|
-
//
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
// the PR lands. When autoDispatch is false the agent is responsible for its own
|
|
857
|
-
// inline validation (may fail in isolated worktrees — documented limitation).
|
|
858
|
-
const LIVE_VALIDATION_PLAYBOOKS = new Set(['implement', 'fix', 'build-fix-complex', 'docs', 'decompose']);
|
|
859
|
-
if (LIVE_VALIDATION_PLAYBOOKS.has(type)) {
|
|
860
|
-
const lv = matchedProject && matchedProject.liveValidation;
|
|
861
|
-
if (lv && lv.autoDispatch === true) {
|
|
862
|
-
const lvType = lv.type || 'live-validation';
|
|
855
|
+
// Only playbooks whose completion creates the lifecycle follow-up may defer
|
|
856
|
+
// builds. The shared resolver also requires effective live `test` routing.
|
|
857
|
+
if (LIVE_VALIDATION_DEFERRED_PLAYBOOKS.has(type)) {
|
|
858
|
+
const lvType = shared.resolveLiveValidationAutoDispatchType(matchedProject);
|
|
859
|
+
if (lvType) {
|
|
863
860
|
inertAppendices.push(
|
|
864
861
|
`\n\n---\n\n## Live Validation — Validation Deferred\n\n` +
|
|
865
862
|
`**Live validation is deferred for this project.** ` +
|
|
@@ -1077,7 +1074,7 @@ function buildSystemPrompt(agentId, config, project) {
|
|
|
1077
1074
|
prompt += `2. ${getRepoHostToolRule(project)}\n`;
|
|
1078
1075
|
prompt += `3. Follow the project conventions in CLAUDE.md if present\n`;
|
|
1079
1076
|
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
|
|
1077
|
+
prompt += `5. Agent status is managed by the engine — agents do not need to track their own status\n\n`;
|
|
1081
1078
|
|
|
1082
1079
|
// The lean system prompt (identity + rules) does not contain task-dispatch
|
|
1083
1080
|
// 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) {
|