@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
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
// four Phase 7 small state files:
|
|
3
|
-
//
|
|
4
|
-
// schedule-runs.json <-> schedule_runs (object → row-per-id)
|
|
5
|
-
// pipeline-runs.json <-> pipeline_runs ({id:[runs]} → row-per-run)
|
|
6
|
-
// managed-processes.json <-> managed_processes ({specs:[]} → row-per-name)
|
|
7
|
-
// worktree-pool.json <-> worktree_pool ({entries:[]} → row-per-entry)
|
|
8
|
-
//
|
|
9
|
-
// They share transaction and JSON-shape transformation infrastructure.
|
|
1
|
+
// SQL stores for low-traffic runtime state.
|
|
10
2
|
|
|
11
3
|
function _toMs(v) {
|
|
12
4
|
if (v == null) return null;
|
|
@@ -15,6 +7,47 @@ function _toMs(v) {
|
|
|
15
7
|
return Number.isFinite(parsed) ? parsed : null;
|
|
16
8
|
}
|
|
17
9
|
|
|
10
|
+
const ENGINE_STATE_MARKER = 'engine_state';
|
|
11
|
+
|
|
12
|
+
function _readEngineStateFromSql(db) {
|
|
13
|
+
const row = db.prepare('SELECT value FROM state_markers WHERE key = ?').get(ENGINE_STATE_MARKER);
|
|
14
|
+
if (!row) return {};
|
|
15
|
+
try {
|
|
16
|
+
const state = JSON.parse(row.value);
|
|
17
|
+
return state && typeof state === 'object' && !Array.isArray(state) ? state : {};
|
|
18
|
+
} catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readEngineState() {
|
|
24
|
+
const { getDb } = require('./db');
|
|
25
|
+
return _readEngineStateFromSql(getDb());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function applyEngineStateMutation(mutator) {
|
|
29
|
+
const { getDb, withTransaction } = require('./db');
|
|
30
|
+
let db;
|
|
31
|
+
try { db = getDb(); }
|
|
32
|
+
catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }
|
|
33
|
+
|
|
34
|
+
return withTransaction(db, () => {
|
|
35
|
+
const before = _readEngineStateFromSql(db);
|
|
36
|
+
const next = mutator(JSON.parse(JSON.stringify(before)));
|
|
37
|
+
const after = next === undefined ? before : next;
|
|
38
|
+
if (!after || typeof after !== 'object' || Array.isArray(after)) {
|
|
39
|
+
return { wrote: false, result: before };
|
|
40
|
+
}
|
|
41
|
+
if (JSON.stringify(before) === JSON.stringify(after)) return { wrote: false, result: after };
|
|
42
|
+
db.prepare(`
|
|
43
|
+
INSERT INTO state_markers (key, value, updated_at)
|
|
44
|
+
VALUES (?, ?, ?)
|
|
45
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
|
|
46
|
+
`).run(ENGINE_STATE_MARKER, JSON.stringify(after), Date.now());
|
|
47
|
+
return { wrote: true, result: after };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
18
51
|
// ─── schedule_runs ──────────────────────────────────────────────────────────
|
|
19
52
|
// Shape: { [scheduleId]: { lastRun, lastWorkItemId, lastResult, lastCompletedAt } }
|
|
20
53
|
// SQL: row per scheduleId, data = the value blob.
|
|
@@ -618,8 +651,10 @@ function applyPendingRebasesMutation(mutator) {
|
|
|
618
651
|
// Shape: [ { id, sessionId, title, _promptHash, lastActiveAt, ... }, ... ]
|
|
619
652
|
// SQL: row per id (each entry has a unique `id` tab key).
|
|
620
653
|
|
|
654
|
+
const CC_GLOBAL_SESSION_ID = '__global__';
|
|
655
|
+
|
|
621
656
|
function _readCcSessionsFromSql(db) {
|
|
622
|
-
const rows = db.prepare('SELECT data FROM cc_sessions ORDER BY updated_at, id').all();
|
|
657
|
+
const rows = db.prepare('SELECT data FROM cc_sessions WHERE id <> ? ORDER BY updated_at, id').all(CC_GLOBAL_SESSION_ID);
|
|
623
658
|
const out = [];
|
|
624
659
|
for (const row of rows) {
|
|
625
660
|
try { out.push(JSON.parse(row.data)); }
|
|
@@ -671,6 +706,46 @@ function applyCcSessionsMutation(mutator) {
|
|
|
671
706
|
});
|
|
672
707
|
}
|
|
673
708
|
|
|
709
|
+
function readCcGlobalSession() {
|
|
710
|
+
const { getDb } = require('./db');
|
|
711
|
+
const row = getDb().prepare('SELECT data FROM cc_sessions WHERE id = ?').get(CC_GLOBAL_SESSION_ID);
|
|
712
|
+
if (!row) return null;
|
|
713
|
+
try {
|
|
714
|
+
const session = JSON.parse(row.data);
|
|
715
|
+
return session && typeof session === 'object' && !Array.isArray(session) ? session : null;
|
|
716
|
+
} catch {
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function applyCcGlobalSessionMutation(mutator) {
|
|
722
|
+
const { getDb, withTransaction } = require('./db');
|
|
723
|
+
let db;
|
|
724
|
+
try { db = getDb(); }
|
|
725
|
+
catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }
|
|
726
|
+
|
|
727
|
+
return withTransaction(db, () => {
|
|
728
|
+
const row = db.prepare('SELECT data FROM cc_sessions WHERE id = ?').get(CC_GLOBAL_SESSION_ID);
|
|
729
|
+
let before = {};
|
|
730
|
+
try {
|
|
731
|
+
const parsed = row ? JSON.parse(row.data) : {};
|
|
732
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) before = parsed;
|
|
733
|
+
} catch { /* replace malformed state */ }
|
|
734
|
+
const next = mutator(JSON.parse(JSON.stringify(before)));
|
|
735
|
+
const after = next === undefined ? before : next;
|
|
736
|
+
if (!after || typeof after !== 'object' || Array.isArray(after)) {
|
|
737
|
+
return { wrote: false, result: before };
|
|
738
|
+
}
|
|
739
|
+
if (JSON.stringify(before) === JSON.stringify(after)) return { wrote: false, result: after };
|
|
740
|
+
db.prepare(`
|
|
741
|
+
INSERT INTO cc_sessions (id, data, updated_at)
|
|
742
|
+
VALUES (?, ?, ?)
|
|
743
|
+
ON CONFLICT(id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at
|
|
744
|
+
`).run(CC_GLOBAL_SESSION_ID, JSON.stringify(after), Date.now());
|
|
745
|
+
return { wrote: true, result: after };
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
|
|
674
749
|
// ─── doc_sessions ──────────────────────────────────────────────────────────
|
|
675
750
|
// Shape: { [filePath]: { sessionId, lastActiveAt, turnCount, ... } }
|
|
676
751
|
// SQL: row per filePath key.
|
|
@@ -728,6 +803,8 @@ function applyDocSessionsMutation(mutator) {
|
|
|
728
803
|
}
|
|
729
804
|
|
|
730
805
|
module.exports = {
|
|
806
|
+
readEngineState,
|
|
807
|
+
applyEngineStateMutation,
|
|
731
808
|
// schedule_runs
|
|
732
809
|
readScheduleRuns,
|
|
733
810
|
applyScheduleRunsMutation,
|
|
@@ -758,6 +835,8 @@ module.exports = {
|
|
|
758
835
|
// cc_sessions
|
|
759
836
|
readCcSessions,
|
|
760
837
|
applyCcSessionsMutation,
|
|
838
|
+
readCcGlobalSession,
|
|
839
|
+
applyCcGlobalSessionMutation,
|
|
761
840
|
// doc_sessions
|
|
762
841
|
readDocSessions,
|
|
763
842
|
applyDocSessionsMutation,
|
|
@@ -83,6 +83,11 @@ function importLegacyJson(sourceRoot) {
|
|
|
83
83
|
imported[fn] = (imported[fn] || 0) + 1;
|
|
84
84
|
};
|
|
85
85
|
|
|
86
|
+
replace(
|
|
87
|
+
'./small-state-store',
|
|
88
|
+
'applyEngineStateMutation',
|
|
89
|
+
_readJson(path.join(root, 'engine', 'state.json'), 'object'),
|
|
90
|
+
);
|
|
86
91
|
replace('./dispatch-store', 'applyDispatchMutation', _readJson(path.join(root, 'engine', 'dispatch.json'), 'object'));
|
|
87
92
|
replace('./metrics-store', 'applyMetricsMutation', _readJson(path.join(root, 'engine', 'metrics.json'), 'object'));
|
|
88
93
|
replace('./watches-store', 'applyWatchesMutation', _readJson(path.join(root, 'engine', 'watches.json'), 'array'));
|
|
@@ -103,6 +108,11 @@ function importLegacyJson(sourceRoot) {
|
|
|
103
108
|
for (const [filename, fn, shape] of small) {
|
|
104
109
|
replace('./small-state-store', fn, _readJson(path.join(root, 'engine', filename), shape));
|
|
105
110
|
}
|
|
111
|
+
replace(
|
|
112
|
+
'./small-state-store',
|
|
113
|
+
'applyCcGlobalSessionMutation',
|
|
114
|
+
_readJson(path.join(root, 'engine', 'cc-session.json'), 'object'),
|
|
115
|
+
);
|
|
106
116
|
|
|
107
117
|
const workItemsStore = require('./work-items-store');
|
|
108
118
|
const pullRequestsStore = require('./pull-requests-store');
|
|
@@ -112,6 +122,11 @@ function importLegacyJson(sourceRoot) {
|
|
|
112
122
|
workItemsStore.applyWorkItemsMutation(scope, () => workItems);
|
|
113
123
|
imported.workItemScopes = (imported.workItemScopes || 0) + 1;
|
|
114
124
|
}
|
|
125
|
+
const archivedWorkItems = _readJson(path.join(dir, 'work-items-archive.json'), 'array');
|
|
126
|
+
if (archivedWorkItems) {
|
|
127
|
+
workItemsStore.replaceArchivedWorkItems(scope, archivedWorkItems);
|
|
128
|
+
imported.archivedWorkItemScopes = (imported.archivedWorkItemScopes || 0) + 1;
|
|
129
|
+
}
|
|
115
130
|
const pullRequests = _readJson(path.join(dir, 'pull-requests.json'), 'array');
|
|
116
131
|
if (pullRequests) {
|
|
117
132
|
pullRequestsStore.applyPullRequestsMutation(scope, () => pullRequests);
|
|
@@ -133,10 +148,7 @@ function importLegacyJson(sourceRoot) {
|
|
|
133
148
|
try { files = fs.readdirSync(dir).filter(name => name.endsWith('.json')); } catch { continue; }
|
|
134
149
|
for (const filename of files) {
|
|
135
150
|
const prd = _readJson(path.join(dir, filename), 'object');
|
|
136
|
-
|
|
137
|
-
? path.join(require('./shared').MINIONS_DIR, 'prd', 'archive', filename)
|
|
138
|
-
: path.join(require('./shared').MINIONS_DIR, 'prd', filename);
|
|
139
|
-
prdStore.writePrd(target, prd);
|
|
151
|
+
prdStore.writePrd(filename, prd, { archived });
|
|
140
152
|
imported.prds = (imported.prds || 0) + 1;
|
|
141
153
|
}
|
|
142
154
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Required at the very top of dashboard.js and engine.js (and supervisor.js).
|
|
7
7
|
* Without this, "what was the dashboard doing at 01:01:56?" requires fishing
|
|
8
|
-
* through
|
|
8
|
+
* through adjacent engine-log entries and hoping timing lines up.
|
|
9
9
|
*
|
|
10
10
|
* The wrap is a no-op when MINIONS_NO_STDIO_TIMESTAMPS is truthy (used by
|
|
11
11
|
* tests that compare console output verbatim).
|
package/engine/timeout.js
CHANGED
|
@@ -13,7 +13,7 @@ const shared = require('./shared');
|
|
|
13
13
|
const queries = require('./queries');
|
|
14
14
|
const steering = require('./steering');
|
|
15
15
|
|
|
16
|
-
const { safeRead, safeWrite, safeJson,
|
|
16
|
+
const { safeRead, safeWrite, safeJson, mutateWorkItems, getProjects, log, ts,
|
|
17
17
|
ENGINE_DEFAULTS, ENGINE_DIR, WI_STATUS, WORK_TYPE, DISPATCH_RESULT, AGENT_STATUS, FAILURE_CLASS } = shared;
|
|
18
18
|
const { getDispatch, getAgentStatus } = queries;
|
|
19
19
|
const AGENTS_DIR = queries.AGENTS_DIR;
|
|
@@ -984,21 +984,14 @@ function checkTimeouts(config) {
|
|
|
984
984
|
});
|
|
985
985
|
}
|
|
986
986
|
|
|
987
|
-
// Agent status is
|
|
987
|
+
// Agent status is derived from dispatch state at read time (getAgentStatus).
|
|
988
988
|
// No reconcile sweep needed — dispatch IS the source of truth.
|
|
989
989
|
|
|
990
990
|
// Reconcile: find work items stuck in "dispatched" with no matching active dispatch
|
|
991
991
|
const activeKeys = new Set((dispatchData.active || []).map(d => d.meta?.dispatchKey).filter(Boolean));
|
|
992
|
-
const
|
|
993
|
-
for (const
|
|
994
|
-
|
|
995
|
-
}
|
|
996
|
-
for (const wiPath of allWiPaths) {
|
|
997
|
-
// skipWriteIfUnchanged is critical — checkTimeouts runs every tick, and
|
|
998
|
-
// without this gate the file's mtime updates on every call (no real
|
|
999
|
-
// change), tripping the cli.js work-items.json watcher → triggering tick
|
|
1000
|
-
// → calling checkTimeouts → ... a 5-6s loop of "File change detected".
|
|
1001
|
-
mutateJsonFileLocked(wiPath, (items) => {
|
|
992
|
+
const workItemScopes = ['central', ...getProjects(config)];
|
|
993
|
+
for (const scope of workItemScopes) {
|
|
994
|
+
mutateWorkItems(scope, (items) => {
|
|
1002
995
|
if (!items || !Array.isArray(items)) return items;
|
|
1003
996
|
let changed = false;
|
|
1004
997
|
for (const item of items) {
|
package/engine/watch-actions.js
CHANGED
|
@@ -32,7 +32,7 @@ const { URL } = require('url');
|
|
|
32
32
|
const shared = require('./shared');
|
|
33
33
|
const {
|
|
34
34
|
WATCH_ACTION_TYPE, WI_STATUS, WORK_TYPE, DONE_STATUSES, PLAN_STATUS,
|
|
35
|
-
log, ts, uid, mutateWorkItems,
|
|
35
|
+
log, ts, uid, mutateWorkItems,
|
|
36
36
|
} = shared;
|
|
37
37
|
|
|
38
38
|
// BUG-H10 (P-h10-watch-flood): statuses that mean the dispatched WI is still
|
|
@@ -387,7 +387,7 @@ registerActionType(WATCH_ACTION_TYPE.NOTIFY, {
|
|
|
387
387
|
},
|
|
388
388
|
});
|
|
389
389
|
|
|
390
|
-
// dispatch-work-item — append a new WI to the project
|
|
390
|
+
// dispatch-work-item — append a new WI to the project or central scope.
|
|
391
391
|
registerActionType(WATCH_ACTION_TYPE.DISPATCH_WORK_ITEM, {
|
|
392
392
|
label: 'Dispatch Work Item',
|
|
393
393
|
description: 'Create and queue a new work item when the watch fires. Templated params support {{target}}, {{condition}}, {{prNumber}}, {{workItemId}}, etc.',
|
|
@@ -405,9 +405,7 @@ registerActionType(WATCH_ACTION_TYPE.DISPATCH_WORK_ITEM, {
|
|
|
405
405
|
if (!title) return { ok: false, summary: 'dispatch-work-item: title is required' };
|
|
406
406
|
|
|
407
407
|
const project = p.project || watch.project || null;
|
|
408
|
-
const
|
|
409
|
-
? projectWorkItemsPath({ name: project })
|
|
410
|
-
: path.join(shared.MINIONS_DIR, 'work-items.json');
|
|
408
|
+
const scope = project || 'central';
|
|
411
409
|
|
|
412
410
|
const id = 'W-' + uid();
|
|
413
411
|
const item = {
|
|
@@ -437,7 +435,7 @@ registerActionType(WATCH_ACTION_TYPE.DISPATCH_WORK_ITEM, {
|
|
|
437
435
|
|
|
438
436
|
let appended = false;
|
|
439
437
|
let dedupedAgainstId = null;
|
|
440
|
-
mutateWorkItems(
|
|
438
|
+
mutateWorkItems(scope, (items) => {
|
|
441
439
|
const existing = items.find(it =>
|
|
442
440
|
it
|
|
443
441
|
&& it.meta
|
|
@@ -663,10 +661,10 @@ registerActionType(WATCH_ACTION_TYPE.MINIONS_API, {
|
|
|
663
661
|
},
|
|
664
662
|
});
|
|
665
663
|
|
|
666
|
-
// cancel-work-item — flip a WI to CANCELLED across project + central
|
|
664
|
+
// cancel-work-item — flip a WI to CANCELLED across project + central scopes.
|
|
667
665
|
registerActionType(WATCH_ACTION_TYPE.CANCEL_WORK_ITEM, {
|
|
668
666
|
label: 'Cancel Work Item',
|
|
669
|
-
description: 'Cancel a work item by ID. Searches
|
|
667
|
+
description: 'Cancel a work item by ID. Searches central and project scopes.',
|
|
670
668
|
params: [
|
|
671
669
|
{ key: 'id', required: true, description: 'Work item ID to cancel.' },
|
|
672
670
|
{ key: 'reason', required: false, description: 'Reason recorded on the item (defaults to "watch:<id>").' },
|
|
@@ -677,17 +675,17 @@ registerActionType(WATCH_ACTION_TYPE.CANCEL_WORK_ITEM, {
|
|
|
677
675
|
if (!targetId) return { ok: false, summary: 'cancel-work-item: id is required' };
|
|
678
676
|
|
|
679
677
|
const reason = p.reason || `watch:${watch.id}`;
|
|
680
|
-
const candidates = [
|
|
678
|
+
const candidates = ['central'];
|
|
681
679
|
try {
|
|
682
680
|
const config = shared.safeJson(path.join(shared.MINIONS_DIR, 'config.json')) || {};
|
|
683
681
|
for (const proj of (config.projects || [])) {
|
|
684
|
-
if (proj && proj.name) candidates.push(
|
|
682
|
+
if (proj && proj.name) candidates.push(proj);
|
|
685
683
|
}
|
|
686
684
|
} catch { /* config optional */ }
|
|
687
685
|
|
|
688
686
|
let cancelled = false;
|
|
689
|
-
for (const
|
|
690
|
-
mutateWorkItems(
|
|
687
|
+
for (const scope of candidates) {
|
|
688
|
+
mutateWorkItems(scope, (items) => {
|
|
691
689
|
const it = items.find(i => i && i.id === targetId);
|
|
692
690
|
if (!it) return items;
|
|
693
691
|
if (DONE_STATUSES.has(it.status) || it.status === WI_STATUS.CANCELLED) return items;
|
|
@@ -730,10 +728,10 @@ registerActionType(WATCH_ACTION_TYPE.TRIGGER_PIPELINE, {
|
|
|
730
728
|
},
|
|
731
729
|
});
|
|
732
730
|
|
|
733
|
-
// archive-plan — mark the PRD
|
|
731
|
+
// archive-plan — mark the PRD as archived.
|
|
734
732
|
registerActionType(WATCH_ACTION_TYPE.ARCHIVE_PLAN, {
|
|
735
733
|
label: 'Archive Plan',
|
|
736
|
-
description: 'Mark a PRD/plan as archived. Sets status="archived" and archivedAt
|
|
734
|
+
description: 'Mark a PRD/plan as archived. Sets status="archived" and archivedAt.',
|
|
737
735
|
params: [
|
|
738
736
|
{ key: 'planFile', required: false, description: 'PRD .json filename (defaults to {{planFile}} from trigger context).' },
|
|
739
737
|
],
|
|
@@ -741,16 +739,16 @@ registerActionType(WATCH_ACTION_TYPE.ARCHIVE_PLAN, {
|
|
|
741
739
|
const p = ctx.params || {};
|
|
742
740
|
const planFile = String(p.planFile || ctx.planFile || '').trim();
|
|
743
741
|
if (!planFile) return { ok: false, summary: 'archive-plan: planFile is required' };
|
|
744
|
-
const
|
|
745
|
-
if (!
|
|
742
|
+
const store = require('./prd-store');
|
|
743
|
+
if (!store.readPrd(planFile)) return { ok: false, summary: `archive-plan: ${planFile} not found` };
|
|
746
744
|
let updated = false;
|
|
747
|
-
|
|
745
|
+
store.mutatePrd(planFile, (data) => {
|
|
748
746
|
if (!data || typeof data !== 'object' || Array.isArray(data)) data = {};
|
|
749
747
|
data.status = 'archived';
|
|
750
748
|
data.archivedAt = ts();
|
|
751
749
|
updated = true;
|
|
752
750
|
return data;
|
|
753
|
-
}
|
|
751
|
+
});
|
|
754
752
|
return updated
|
|
755
753
|
? { ok: true, summary: `archived plan ${planFile}` }
|
|
756
754
|
: { ok: false, summary: `archive-plan: ${planFile} write failed` };
|
|
@@ -768,16 +766,16 @@ registerActionType(WATCH_ACTION_TYPE.RESUME_PLAN, {
|
|
|
768
766
|
const p = ctx.params || {};
|
|
769
767
|
const planFile = String(p.planFile || ctx.planFile || '').trim();
|
|
770
768
|
if (!planFile) return { ok: false, summary: 'resume-plan: planFile is required' };
|
|
771
|
-
const
|
|
772
|
-
if (!
|
|
769
|
+
const store = require('./prd-store');
|
|
770
|
+
if (!store.readPrd(planFile)) return { ok: false, summary: `resume-plan: ${planFile} not found` };
|
|
773
771
|
let updated = false;
|
|
774
|
-
|
|
772
|
+
store.mutatePrd(planFile, (data) => {
|
|
775
773
|
if (!data || typeof data !== 'object' || Array.isArray(data)) data = {};
|
|
776
774
|
data.status = PLAN_STATUS.ACTIVE;
|
|
777
775
|
delete data.planStale;
|
|
778
776
|
updated = true;
|
|
779
777
|
return data;
|
|
780
|
-
}
|
|
778
|
+
});
|
|
781
779
|
return updated
|
|
782
780
|
? { ok: true, summary: `resumed plan ${planFile}` }
|
|
783
781
|
: { ok: false, summary: `resume-plan: ${planFile} write failed` };
|
package/engine/watches-store.js
CHANGED
package/engine/watches.js
CHANGED
|
@@ -61,26 +61,23 @@
|
|
|
61
61
|
* `pipelines/`); no sandboxing. Hot-reload is not supported — restart the
|
|
62
62
|
* engine to pick up new plugin files (matches how playbooks already work).
|
|
63
63
|
*
|
|
64
|
-
* State stored in
|
|
64
|
+
* State is stored transactionally in SQLite.
|
|
65
65
|
*/
|
|
66
66
|
|
|
67
67
|
const fs = require('fs');
|
|
68
68
|
const path = require('path');
|
|
69
69
|
const shared = require('./shared');
|
|
70
|
-
const {
|
|
70
|
+
const { mutateWatches, ts, uid, log, writeToInbox,
|
|
71
71
|
WATCH_STATUS, WATCH_TARGET_TYPE, WATCH_CONDITION, WATCH_ABSOLUTE_CONDITIONS } = shared;
|
|
72
72
|
const watchActions = require('./watch-actions');
|
|
73
73
|
|
|
74
|
-
// Dynamic path — respects MINIONS_TEST_DIR for test isolation
|
|
75
|
-
function _watchesPath() { return path.join(shared.MINIONS_DIR, 'engine', 'watches.json'); }
|
|
76
|
-
|
|
77
74
|
// Default check interval: 5 minutes (300000ms). Engine tick runs every 10s
|
|
78
75
|
// (W-mpxpckey000laa4f), so watches are checked every N ticks where
|
|
79
76
|
// N = ceil(interval / tickInterval).
|
|
80
77
|
const DEFAULT_WATCH_INTERVAL = 300000;
|
|
81
78
|
|
|
82
79
|
// P-w12d8f3a — Phase 6.2: per-watch evaluation history is capped at 25
|
|
83
|
-
// entries (oldest-first). Bounded to keep
|
|
80
|
+
// entries (oldest-first). Bounded to keep watch state compact:
|
|
84
81
|
// 25 entries × ~200 bytes × 100 watches ≈ 500 KB ceiling. History entries
|
|
85
82
|
// never contain full state snapshots — only the trigger reason — per the
|
|
86
83
|
// "history is small and bounded" design principle.
|
|
@@ -177,11 +174,11 @@ function listTargetTypes() {
|
|
|
177
174
|
}
|
|
178
175
|
|
|
179
176
|
/**
|
|
180
|
-
* Read all watches from
|
|
177
|
+
* Read all watches from SQLite.
|
|
181
178
|
* @returns {Array<object>}
|
|
182
179
|
*/
|
|
183
180
|
function getWatches() {
|
|
184
|
-
return
|
|
181
|
+
return require('./watches-store').readWatches();
|
|
185
182
|
}
|
|
186
183
|
|
|
187
184
|
/**
|
|
@@ -1366,7 +1363,7 @@ registerTargetType(WATCH_TARGET_TYPE.AGENT, {
|
|
|
1366
1363
|
// P-w2c8d1e7 — Phase 1.2: surface currentDispatchId so predicates can
|
|
1367
1364
|
// distinguish "agent now working on a different dispatch" from a plain
|
|
1368
1365
|
// status flip. Sourced from queries.getAgents() which derives it from
|
|
1369
|
-
// dispatch
|
|
1366
|
+
// active dispatch state via getAgentStatus.dispatch_id.
|
|
1370
1367
|
captureState: (a) => ({
|
|
1371
1368
|
status: a.status || null,
|
|
1372
1369
|
currentDispatchId: a.currentDispatchId || null,
|
|
@@ -1459,7 +1456,6 @@ module.exports = {
|
|
|
1459
1456
|
runWatchActionChain: watchActions.runWatchActionChain,
|
|
1460
1457
|
validateAction: watchActions.validateAction,
|
|
1461
1458
|
_captureState, // exported for testing
|
|
1462
|
-
_watchesPath, // exported for testing — dynamic, respects MINIONS_TEST_DIR
|
|
1463
1459
|
_runActionTask, // exported for testing — invoked by checkWatches per fired action
|
|
1464
1460
|
_TARGET_TYPES: TARGET_TYPES, // exported for testing — direct registry access
|
|
1465
1461
|
_loadPluginTargetTypes, // exported for testing — re-scans watches.d/
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
function _hasText(value) {
|
|
2
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function _hasObjectValue(value) {
|
|
6
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
7
|
+
&& Object.keys(value).length > 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function hasWorkItemSemantics(item) {
|
|
11
|
+
return _hasText(item?.title)
|
|
12
|
+
|| _hasText(item?.description)
|
|
13
|
+
|| _hasText(item?.type)
|
|
14
|
+
|| _hasText(item?.itemType);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function hasWorkItemProvenance(item) {
|
|
18
|
+
return [
|
|
19
|
+
'created', 'created_at', 'createdAt', 'createdBy', 'sourcePlan', 'planFile',
|
|
20
|
+
'project', 'parent_id', 'prd_item_id', '_originWi', '_originAgent',
|
|
21
|
+
'_pipelineRun', '_pipelineStage',
|
|
22
|
+
].some(key => item?.[key] !== undefined && item[key] !== null && item[key] !== '')
|
|
23
|
+
|| _hasObjectValue(item?.meta)
|
|
24
|
+
|| (Array.isArray(item?.references) && item.references.length > 0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function validateNewWorkItem(item) {
|
|
28
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
29
|
+
throw new Error('malformed work item: record must be an object');
|
|
30
|
+
}
|
|
31
|
+
if (!_hasText(String(item.id || ''))) {
|
|
32
|
+
throw new Error('malformed work item: id is required');
|
|
33
|
+
}
|
|
34
|
+
if (!hasWorkItemSemantics(item) && !hasWorkItemProvenance(item)) {
|
|
35
|
+
throw new Error(`malformed work item ${item.id}: semantic content or provenance is required`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isMalformedPhantomWorkItem(item) {
|
|
40
|
+
return !!item
|
|
41
|
+
&& typeof item === 'object'
|
|
42
|
+
&& item.status === 'failed'
|
|
43
|
+
&& !hasWorkItemSemantics(item)
|
|
44
|
+
&& !hasWorkItemProvenance(item);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = {
|
|
48
|
+
hasWorkItemSemantics,
|
|
49
|
+
hasWorkItemProvenance,
|
|
50
|
+
validateNewWorkItem,
|
|
51
|
+
isMalformedPhantomWorkItem,
|
|
52
|
+
};
|