@yemi33/minions 0.1.2370 → 0.1.2371
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 +38 -11
- package/dashboard/js/render-other.js +2 -30
- package/dashboard/js/render-work-items.js +0 -34
- package/dashboard/js/settings.js +12 -27
- package/dashboard/pages/tools.html +1 -1
- package/dashboard.js +80 -257
- package/docs/README.md +2 -3
- package/docs/architecture.excalidraw +2 -2
- package/docs/auto-discovery.md +3 -3
- package/docs/completion-reports.md +0 -121
- package/docs/copilot-cli-schema.md +9 -17
- package/docs/deprecated.json +0 -51
- package/docs/design-state-storage.md +4 -4
- package/docs/harness-propagation.md +33 -263
- package/docs/human-vs-automated.md +1 -1
- package/docs/named-agents.md +0 -2
- package/docs/plan-lifecycle.md +5 -6
- package/docs/qa-runbook-lifecycle.md +10 -25
- package/docs/shared-lifecycle-module-map.md +2 -10
- package/engine/ado-comment.js +5 -11
- package/engine/ado.js +10 -5
- package/engine/cleanup.js +16 -36
- package/engine/cli.js +13 -175
- package/engine/comment-format.js +8 -182
- package/engine/db/index.js +60 -10
- package/engine/db/migrations/018-sql-only-cutover.js +56 -0
- package/engine/dispatch-store.js +0 -114
- package/engine/dispatch.js +1 -6
- package/engine/gh-comment.js +2 -10
- package/engine/github.js +8 -6
- package/engine/lifecycle.js +58 -173
- package/engine/llm.js +9 -11
- package/engine/managed-spawn.js +1 -6
- package/engine/memory-store.js +8 -6
- package/engine/metrics-store.js +4 -128
- package/engine/pipeline.js +4 -7
- package/engine/playbook.js +8 -196
- package/engine/prd-store.js +115 -141
- package/engine/preflight.js +8 -55
- package/engine/projects.js +34 -41
- package/engine/pull-requests-store.js +9 -234
- package/engine/qa-runs.js +4 -15
- package/engine/qa-sessions.js +5 -17
- package/engine/queries.js +23 -103
- package/engine/routing.js +1 -1
- package/engine/runtimes/claude.js +1 -2
- package/engine/runtimes/codex.js +0 -2
- package/engine/runtimes/copilot.js +1 -4
- package/engine/shared-branch-pr-reconcile.js +2 -5
- package/engine/shared.js +174 -533
- package/engine/small-state-store.js +12 -647
- package/engine/spawn-agent.js +5 -96
- package/engine/state-operations.js +166 -0
- package/engine/supervisor.js +0 -1
- package/engine/watch-actions.js +2 -3
- package/engine/watches-store.js +2 -128
- package/engine/work-items-store.js +3 -254
- package/engine.js +102 -334
- package/package.json +2 -2
- package/playbooks/build-fix-complex.md +0 -4
- package/playbooks/fix.md +0 -4
- package/playbooks/implement-shared.md +0 -4
- package/playbooks/implement.md +0 -4
- package/playbooks/plan-to-prd.md +16 -11
- package/playbooks/plan.md +0 -4
- package/playbooks/review.md +0 -6
- package/playbooks/shared-rules.md +0 -65
- package/docs/harness-transparency.md +0 -199
- package/docs/project-skills.md +0 -193
- package/engine/discover-project-skills.js +0 -673
- package/engine/playbook-intents.js +0 -76
package/engine/spawn-agent.js
CHANGED
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
* --verbose / --no-verbose → opts.verbose
|
|
23
23
|
* --stream <on|off> → opts.stream (Copilot)
|
|
24
24
|
* --disable-builtin-mcps → opts.disableBuiltinMcps (Copilot)
|
|
25
|
-
* --no-custom-instructions → opts.suppressAgentsMd (Copilot)
|
|
26
25
|
* --enable-reasoning-summaries→ opts.reasoningSummaries (Copilot)
|
|
27
26
|
*
|
|
28
27
|
* Legacy --permission-mode <X> is dropped: the new Claude adapter emits
|
|
@@ -55,15 +54,6 @@ function parseSpawnArgs(argv) {
|
|
|
55
54
|
let runtimeName = 'claude';
|
|
56
55
|
const opts = {};
|
|
57
56
|
const passthrough = [];
|
|
58
|
-
// P-08b62d49 — accumulating array of project-local-on-main harness dirs that
|
|
59
|
-
// engine.js wants `computeAddDirs` to surface as additional `--add-dir`
|
|
60
|
-
// entries. Multiple `--project-harness-dir <path>` flags accumulate in
|
|
61
|
-
// declaration order; the dedup/exists filter happens inside `computeAddDirs`.
|
|
62
|
-
const projectHarnessDirs = [];
|
|
63
|
-
// P-49e1c8b7 — hermetic harness opt-out (boolean). Set by engine.js when
|
|
64
|
-
// shared.resolveAgentHermeticHarness(agent, engine) is true; propagates to
|
|
65
|
-
// computeAddDirs which then returns [minionsDir] only.
|
|
66
|
-
let hermetic = false;
|
|
67
57
|
|
|
68
58
|
for (let i = 2; i < args.length; i++) {
|
|
69
59
|
const a = args[i];
|
|
@@ -105,7 +95,7 @@ function parseSpawnArgs(argv) {
|
|
|
105
95
|
}
|
|
106
96
|
}
|
|
107
97
|
|
|
108
|
-
return { promptFile, sysPromptFile, runtimeName, opts, passthrough
|
|
98
|
+
return { promptFile, sysPromptFile, runtimeName, opts, passthrough };
|
|
109
99
|
}
|
|
110
100
|
|
|
111
101
|
/**
|
|
@@ -121,9 +111,8 @@ function parseSpawnArgs(argv) {
|
|
|
121
111
|
* usingNodeShim, // true → runtime returned a non-native binary (Claude cli.js)
|
|
122
112
|
* }
|
|
123
113
|
*/
|
|
124
|
-
function buildSpawnInvocation({ runtime, resolved, promptText, sysPromptText, opts, passthrough
|
|
114
|
+
function buildSpawnInvocation({ runtime, resolved, promptText, sysPromptText, opts, passthrough }) {
|
|
125
115
|
const adapterOpts = { ...opts };
|
|
126
|
-
if (Array.isArray(addDirs) && addDirs.length) adapterOpts.addDirs = addDirs;
|
|
127
116
|
const finalPrompt = runtime.buildPrompt(promptText, sysPromptText, adapterOpts);
|
|
128
117
|
const deliveryMode = typeof runtime.getPromptDeliveryMode === 'function'
|
|
129
118
|
? runtime.getPromptDeliveryMode(adapterOpts)
|
|
@@ -317,67 +306,6 @@ function createParentPipeForwarder(stream, outputPath, prefix = '') {
|
|
|
317
306
|
};
|
|
318
307
|
}
|
|
319
308
|
|
|
320
|
-
/**
|
|
321
|
-
* Build the `--add-dir` list passed to the runtime CLI. Pure: takes
|
|
322
|
-
* `{ runtime, minionsDir, homeDir, projectHarnessDirs, hermetic, exists }`
|
|
323
|
-
* and returns an ordered, deduped array of dirs the agent should be able to
|
|
324
|
-
* read from outside its worktree.
|
|
325
|
-
*
|
|
326
|
-
* Order:
|
|
327
|
-
* 1. minionsDir (always first, so playbooks/system-prompt are reachable).
|
|
328
|
-
* 2. Every existing dir from `runtime.getUserAssetDirs({ homeDir })` —
|
|
329
|
-
* user-scope skill/command/MCP roots (`~/.claude`, `~/.copilot`,
|
|
330
|
-
* `~/.agents`, …) so worktree-bound agents inherit user-installed
|
|
331
|
-
* harnesses.
|
|
332
|
-
* 3. P-08b62d49 — Every existing dir in `projectHarnessDirs` (engine.js
|
|
333
|
-
* passes the union of `queries.getProjectHarnesses(project).{skills,
|
|
334
|
-
* commands}` dirs that live in `project.localPath` but NOT under the
|
|
335
|
-
* worktree, so uncommitted `<repo>/.claude/skills/…` etc. on the main
|
|
336
|
-
* checkout are reachable from worktree dispatches). See
|
|
337
|
-
* `docs/harness-propagation.md` → "The worktree-uncommitted footgun".
|
|
338
|
-
*
|
|
339
|
-
* Non-existent dirs are dropped at every layer — Claude CLI rejects unknown
|
|
340
|
-
* `--add-dir` entries. The dedup compares resolved paths so we never emit a
|
|
341
|
-
* dir twice (e.g. when a runtime asset dir IS the minions repo in unusual
|
|
342
|
-
* setups, or when a project-harness dir duplicates a user-asset dir).
|
|
343
|
-
*
|
|
344
|
-
* P-49e1c8b7 — When `hermetic === true`, the result is exactly `[minionsDir]`:
|
|
345
|
-
* both the runtime adapter's user-asset dirs AND `projectHarnessDirs` are
|
|
346
|
-
* dropped so the spawned agent runs with a known-empty harness surface.
|
|
347
|
-
* Engine.js sets this via `shared.resolveAgentHermeticHarness(agent, engine)`
|
|
348
|
-
* and forwards `--hermetic-harness` to spawn-agent so the flag survives the
|
|
349
|
-
* subprocess hop.
|
|
350
|
-
*
|
|
351
|
-
* `exists` is injectable for tests; defaults to `fs.existsSync`.
|
|
352
|
-
*/
|
|
353
|
-
function computeAddDirs({ runtime, minionsDir, homeDir, projectHarnessDirs, hermetic, exists = fs.existsSync } = {}) {
|
|
354
|
-
if (hermetic) return [minionsDir];
|
|
355
|
-
const out = [minionsDir];
|
|
356
|
-
const seen = new Set([path.resolve(minionsDir)]);
|
|
357
|
-
const assetDirs = typeof runtime?.getUserAssetDirs === 'function'
|
|
358
|
-
? runtime.getUserAssetDirs({ homeDir })
|
|
359
|
-
: [];
|
|
360
|
-
for (const d of assetDirs) {
|
|
361
|
-
if (!d) continue;
|
|
362
|
-
const resolved = path.resolve(d);
|
|
363
|
-
if (seen.has(resolved)) continue;
|
|
364
|
-
if (!exists(d)) continue;
|
|
365
|
-
out.push(d);
|
|
366
|
-
seen.add(resolved);
|
|
367
|
-
}
|
|
368
|
-
if (Array.isArray(projectHarnessDirs)) {
|
|
369
|
-
for (const d of projectHarnessDirs) {
|
|
370
|
-
if (!d) continue;
|
|
371
|
-
const resolved = path.resolve(d);
|
|
372
|
-
if (seen.has(resolved)) continue;
|
|
373
|
-
if (!exists(d)) continue;
|
|
374
|
-
out.push(d);
|
|
375
|
-
seen.add(resolved);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
return out;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
309
|
/**
|
|
382
310
|
* P-7d31a06b — Pre-approve a worktree's `.mcp.json` servers in `~/.claude.json`
|
|
383
311
|
* so Claude Code's first invocation in a fresh worktree cwd doesn't show the
|
|
@@ -498,7 +426,7 @@ function main() {
|
|
|
498
426
|
console.error('Usage: node spawn-agent.js <prompt-file> <sysprompt-file> [--runtime <name>] [args...]');
|
|
499
427
|
process.exit(1);
|
|
500
428
|
}
|
|
501
|
-
const { promptFile, sysPromptFile, runtimeName, opts, passthrough
|
|
429
|
+
const { promptFile, sysPromptFile, runtimeName, opts, passthrough } = parsed;
|
|
502
430
|
|
|
503
431
|
const env = cleanChildEnv();
|
|
504
432
|
injectAdoTokenEnvForRepoHost(env);
|
|
@@ -532,25 +460,6 @@ function main() {
|
|
|
532
460
|
opts.sysPromptFile = sysTmpPath;
|
|
533
461
|
}
|
|
534
462
|
|
|
535
|
-
// Skill discovery dirs — agents run with CWD set to an external repo
|
|
536
|
-
// worktree, so runtime-native global assets would otherwise be invisible.
|
|
537
|
-
// The adapter owns both where those assets live and how to surface them.
|
|
538
|
-
// P-08b62d49: engine.js also forwards uncommitted project-local harness
|
|
539
|
-
// dirs from the operator's main checkout via `--project-harness-dir <path>`
|
|
540
|
-
// (one per dir, accumulated in `projectHarnessDirs`) so worktree-bound
|
|
541
|
-
// agents inherit them too. See docs/harness-propagation.md.
|
|
542
|
-
// P-49e1c8b7: when engine.js passes `--hermetic-harness`, computeAddDirs
|
|
543
|
-
// returns [minionsDir] only — both user-asset dirs and project harness
|
|
544
|
-
// dirs are dropped for a known-empty harness surface.
|
|
545
|
-
const minionsDir = path.resolve(__dirname, '..');
|
|
546
|
-
const addDirs = computeAddDirs({
|
|
547
|
-
runtime,
|
|
548
|
-
minionsDir,
|
|
549
|
-
homeDir: os.homedir(),
|
|
550
|
-
projectHarnessDirs,
|
|
551
|
-
hermetic,
|
|
552
|
-
});
|
|
553
|
-
|
|
554
463
|
let resolved;
|
|
555
464
|
try { resolved = runtime.resolveBinary({ env }); }
|
|
556
465
|
catch (err) {
|
|
@@ -565,7 +474,7 @@ function main() {
|
|
|
565
474
|
const invocation = buildSpawnInvocation({
|
|
566
475
|
runtime, resolved,
|
|
567
476
|
promptText, sysPromptText,
|
|
568
|
-
opts, passthrough,
|
|
477
|
+
opts, passthrough,
|
|
569
478
|
});
|
|
570
479
|
|
|
571
480
|
// Debug log (async — not on critical path). Lives inside the per-dispatch
|
|
@@ -805,6 +714,6 @@ function main() {
|
|
|
805
714
|
});
|
|
806
715
|
}
|
|
807
716
|
|
|
808
|
-
module.exports = { parseSpawnArgs, buildSpawnInvocation, normalizeRuntimeExit, shouldInjectAdoTokenEnv, injectAdoTokenEnv, injectAdoTokenEnvForRepoHost, writeProcessExitSentinel,
|
|
717
|
+
module.exports = { parseSpawnArgs, buildSpawnInvocation, normalizeRuntimeExit, shouldInjectAdoTokenEnv, injectAdoTokenEnv, injectAdoTokenEnvForRepoHost, writeProcessExitSentinel, preApproveWorkspaceMcps, createParentPipeForwarder, assertStaleHeadOk };
|
|
809
718
|
|
|
810
719
|
if (require.main === module) main();
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// Explicit operational tooling for the SQL-only runtime state store.
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { getDb } = require('./db');
|
|
6
|
+
|
|
7
|
+
function _quoteSqlString(value) {
|
|
8
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function checkState() {
|
|
12
|
+
const db = getDb();
|
|
13
|
+
const schemaVersion = Number(db.prepare('SELECT COALESCE(MAX(version), 0) AS version FROM schema_version').get().version) || 0;
|
|
14
|
+
const quickCheck = db.prepare('PRAGMA quick_check').all().map(row => Object.values(row)[0]);
|
|
15
|
+
const foreignKeyViolations = db.prepare('PRAGMA foreign_key_check').all();
|
|
16
|
+
const invariants = {
|
|
17
|
+
orphanPrdItems: Number(db.prepare('SELECT COUNT(*) AS n FROM prd_items pi LEFT JOIN prds p ON p.id=pi.prd_id WHERE p.id IS NULL').get().n) || 0,
|
|
18
|
+
orphanVerifyPrs: Number(db.prepare('SELECT COUNT(*) AS n FROM prd_verify_prs vp LEFT JOIN prds p ON p.id=vp.prd_id WHERE p.id IS NULL').get().n) || 0,
|
|
19
|
+
brokenWorkItemPrdLinks: Number(db.prepare('SELECT COUNT(*) AS n FROM work_items wi LEFT JOIN prd_items pi ON pi.id=wi.prd_item_id WHERE wi.prd_item_id IS NOT NULL AND pi.id IS NULL').get().n) || 0,
|
|
20
|
+
};
|
|
21
|
+
const ok = quickCheck.length === 1
|
|
22
|
+
&& quickCheck[0] === 'ok'
|
|
23
|
+
&& foreignKeyViolations.length === 0
|
|
24
|
+
&& Object.values(invariants).every(count => count === 0);
|
|
25
|
+
return { ok, schemaVersion, quickCheck, foreignKeyViolations, invariants };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function backupState(targetPath) {
|
|
29
|
+
if (!targetPath) throw new Error('state backup requires an output path');
|
|
30
|
+
const resolved = path.resolve(targetPath);
|
|
31
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
32
|
+
if (fs.existsSync(resolved)) throw new Error(`backup target already exists: ${resolved}`);
|
|
33
|
+
const db = getDb();
|
|
34
|
+
db.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
35
|
+
db.exec(`VACUUM INTO ${_quoteSqlString(resolved)}`);
|
|
36
|
+
return { ok: true, path: resolved, bytes: fs.statSync(resolved).size };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function exportState(targetPath) {
|
|
40
|
+
if (!targetPath) throw new Error('state export requires an output path');
|
|
41
|
+
const resolved = path.resolve(targetPath);
|
|
42
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
43
|
+
const db = getDb();
|
|
44
|
+
const tables = db.prepare(`
|
|
45
|
+
SELECT name FROM sqlite_master
|
|
46
|
+
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
|
47
|
+
ORDER BY name
|
|
48
|
+
`).all().map(row => row.name);
|
|
49
|
+
const data = {
|
|
50
|
+
format: 'minions-sql-diagnostic-export-v1',
|
|
51
|
+
exportedAt: new Date().toISOString(),
|
|
52
|
+
integrity: checkState(),
|
|
53
|
+
tables: {},
|
|
54
|
+
};
|
|
55
|
+
for (const table of tables) {
|
|
56
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table)) throw new Error(`unsafe table name: ${table}`);
|
|
57
|
+
data.tables[table] = db.prepare(`SELECT * FROM "${table}"`).all();
|
|
58
|
+
}
|
|
59
|
+
const tmp = `${resolved}.tmp.${process.pid}`;
|
|
60
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2));
|
|
61
|
+
fs.renameSync(tmp, resolved);
|
|
62
|
+
return { ok: true, path: resolved, tables: tables.length, bytes: fs.statSync(resolved).size };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function _readJson(filePath, expected) {
|
|
66
|
+
if (!fs.existsSync(filePath)) return null;
|
|
67
|
+
const value = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
68
|
+
if (expected === 'array' && !Array.isArray(value)) throw new Error(`expected array in ${filePath}`);
|
|
69
|
+
if (expected === 'object' && (!value || typeof value !== 'object' || Array.isArray(value))) {
|
|
70
|
+
throw new Error(`expected object in ${filePath}`);
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function importLegacyJson(sourceRoot) {
|
|
76
|
+
if (!sourceRoot) throw new Error('state import-legacy-json requires a source directory');
|
|
77
|
+
const root = path.resolve(sourceRoot);
|
|
78
|
+
if (!fs.statSync(root).isDirectory()) throw new Error(`legacy source is not a directory: ${root}`);
|
|
79
|
+
const imported = {};
|
|
80
|
+
const replace = (modulePath, fn, value) => {
|
|
81
|
+
if (value == null) return;
|
|
82
|
+
require(modulePath)[fn](() => value);
|
|
83
|
+
imported[fn] = (imported[fn] || 0) + 1;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
replace('./dispatch-store', 'applyDispatchMutation', _readJson(path.join(root, 'engine', 'dispatch.json'), 'object'));
|
|
87
|
+
replace('./metrics-store', 'applyMetricsMutation', _readJson(path.join(root, 'engine', 'metrics.json'), 'object'));
|
|
88
|
+
replace('./watches-store', 'applyWatchesMutation', _readJson(path.join(root, 'engine', 'watches.json'), 'array'));
|
|
89
|
+
|
|
90
|
+
const small = [
|
|
91
|
+
['schedule-runs.json', 'applyScheduleRunsMutation', 'object'],
|
|
92
|
+
['pipeline-runs.json', 'applyPipelineRunsMutation', 'object'],
|
|
93
|
+
['managed-processes.json', 'applyManagedProcessesMutation', 'object'],
|
|
94
|
+
['worktree-pool.json', 'applyWorktreePoolMutation', 'object'],
|
|
95
|
+
['qa-runs.json', 'applyQaRunsMutation', 'array'],
|
|
96
|
+
['qa-sessions.json', 'applyQaSessionsMutation', 'array'],
|
|
97
|
+
['pr-links.json', 'applyPrLinksMutation', 'object'],
|
|
98
|
+
['cooldowns.json', 'applyCooldownsMutation', 'object'],
|
|
99
|
+
['pending-rebases.json', 'applyPendingRebasesMutation', 'array'],
|
|
100
|
+
['cc-sessions.json', 'applyCcSessionsMutation', 'array'],
|
|
101
|
+
['doc-sessions.json', 'applyDocSessionsMutation', 'object'],
|
|
102
|
+
];
|
|
103
|
+
for (const [filename, fn, shape] of small) {
|
|
104
|
+
replace('./small-state-store', fn, _readJson(path.join(root, 'engine', filename), shape));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const workItemsStore = require('./work-items-store');
|
|
108
|
+
const pullRequestsStore = require('./pull-requests-store');
|
|
109
|
+
const importScope = (scope, dir) => {
|
|
110
|
+
const workItems = _readJson(path.join(dir, 'work-items.json'), 'array');
|
|
111
|
+
if (workItems) {
|
|
112
|
+
workItemsStore.applyWorkItemsMutation(scope, () => workItems);
|
|
113
|
+
imported.workItemScopes = (imported.workItemScopes || 0) + 1;
|
|
114
|
+
}
|
|
115
|
+
const pullRequests = _readJson(path.join(dir, 'pull-requests.json'), 'array');
|
|
116
|
+
if (pullRequests) {
|
|
117
|
+
pullRequestsStore.applyPullRequestsMutation(scope, () => pullRequests);
|
|
118
|
+
imported.pullRequestScopes = (imported.pullRequestScopes || 0) + 1;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
importScope('central', root);
|
|
122
|
+
try {
|
|
123
|
+
for (const entry of fs.readdirSync(path.join(root, 'projects'), { withFileTypes: true })) {
|
|
124
|
+
if (entry.isDirectory() && entry.name !== '.archived') {
|
|
125
|
+
importScope(entry.name, path.join(root, 'projects', entry.name));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch { /* projects are optional */ }
|
|
129
|
+
|
|
130
|
+
const prdStore = require('./prd-store');
|
|
131
|
+
for (const [dir, archived] of [[path.join(root, 'prd'), false], [path.join(root, 'prd', 'archive'), true]]) {
|
|
132
|
+
let files = [];
|
|
133
|
+
try { files = fs.readdirSync(dir).filter(name => name.endsWith('.json')); } catch { continue; }
|
|
134
|
+
for (const filename of files) {
|
|
135
|
+
const prd = _readJson(path.join(dir, filename), 'object');
|
|
136
|
+
const target = archived
|
|
137
|
+
? path.join(require('./shared').MINIONS_DIR, 'prd', 'archive', filename)
|
|
138
|
+
: path.join(require('./shared').MINIONS_DIR, 'prd', filename);
|
|
139
|
+
prdStore.writePrd(target, prd);
|
|
140
|
+
imported.prds = (imported.prds || 0) + 1;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const logs = _readJson(path.join(root, 'engine', 'log.json'), 'array');
|
|
145
|
+
if (logs) {
|
|
146
|
+
const db = getDb();
|
|
147
|
+
const { withTransaction } = require('./db');
|
|
148
|
+
withTransaction(db, () => {
|
|
149
|
+
db.exec('DELETE FROM logs');
|
|
150
|
+
const insert = db.prepare('INSERT INTO logs (ts_ms, level, message, meta) VALUES (?, ?, ?, ?)');
|
|
151
|
+
for (const entry of logs) {
|
|
152
|
+
const timestamp = Date.parse(entry.timestamp);
|
|
153
|
+
const meta = { ...entry };
|
|
154
|
+
delete meta.timestamp;
|
|
155
|
+
delete meta.level;
|
|
156
|
+
delete meta.message;
|
|
157
|
+
const { level, message } = entry;
|
|
158
|
+
insert.run(Number.isFinite(timestamp) ? timestamp : Date.now(), level || 'info', String(message || ''), Object.keys(meta).length ? JSON.stringify(meta) : null);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
imported.logs = logs.length;
|
|
162
|
+
}
|
|
163
|
+
return { ok: true, source: root, imported, integrity: checkState() };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = { checkState, backupState, exportState, importLegacyJson };
|
package/engine/supervisor.js
CHANGED
|
@@ -339,7 +339,6 @@ function openAppendFd(name) {
|
|
|
339
339
|
}
|
|
340
340
|
|
|
341
341
|
function _sqliteSpawnFlags() {
|
|
342
|
-
if (process.env.MINIONS_FORCE_JSON === '1') return [];
|
|
343
342
|
const major = parseInt(String(process.versions.node).split('.')[0], 10);
|
|
344
343
|
if (!Number.isFinite(major) || major < 22 || major >= 24) return [];
|
|
345
344
|
const nodeOpts = String(process.env.NODE_OPTIONS || '');
|
package/engine/watch-actions.js
CHANGED
|
@@ -687,7 +687,6 @@ registerActionType(WATCH_ACTION_TYPE.CANCEL_WORK_ITEM, {
|
|
|
687
687
|
|
|
688
688
|
let cancelled = false;
|
|
689
689
|
for (const wiPath of candidates) {
|
|
690
|
-
if (!fs.existsSync(wiPath)) continue;
|
|
691
690
|
mutateWorkItems(wiPath, (items) => {
|
|
692
691
|
const it = items.find(i => i && i.id === targetId);
|
|
693
692
|
if (!it) return items;
|
|
@@ -743,7 +742,7 @@ registerActionType(WATCH_ACTION_TYPE.ARCHIVE_PLAN, {
|
|
|
743
742
|
const planFile = String(p.planFile || ctx.planFile || '').trim();
|
|
744
743
|
if (!planFile) return { ok: false, summary: 'archive-plan: planFile is required' };
|
|
745
744
|
const prdPath = path.join(shared.MINIONS_DIR, 'prd', planFile);
|
|
746
|
-
if (!
|
|
745
|
+
if (!shared.safeJsonNoRestore(prdPath)) return { ok: false, summary: `archive-plan: ${planFile} not found` };
|
|
747
746
|
let updated = false;
|
|
748
747
|
mutateJsonFileLocked(prdPath, (data) => {
|
|
749
748
|
if (!data || typeof data !== 'object' || Array.isArray(data)) data = {};
|
|
@@ -770,7 +769,7 @@ registerActionType(WATCH_ACTION_TYPE.RESUME_PLAN, {
|
|
|
770
769
|
const planFile = String(p.planFile || ctx.planFile || '').trim();
|
|
771
770
|
if (!planFile) return { ok: false, summary: 'resume-plan: planFile is required' };
|
|
772
771
|
const prdPath = path.join(shared.MINIONS_DIR, 'prd', planFile);
|
|
773
|
-
if (!
|
|
772
|
+
if (!shared.safeJsonNoRestore(prdPath)) return { ok: false, summary: `resume-plan: ${planFile} not found` };
|
|
774
773
|
let updated = false;
|
|
775
774
|
mutateJsonFileLocked(prdPath, (data) => {
|
|
776
775
|
if (!data || typeof data !== 'object' || Array.isArray(data)) data = {};
|
package/engine/watches-store.js
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
// engine/watches-store.js — SQL-backed implementation of engine/watches.json.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// table (no per-scope split — watches.json is a flat array, not
|
|
5
|
-
// per-project), diff-then-apply mutator, JSON dual-write mirror with
|
|
6
|
-
// (mtime, size) fingerprint for external-edit detection.
|
|
3
|
+
// A single SQL table (no per-scope split), with a diff-then-apply mutator.
|
|
7
4
|
//
|
|
8
5
|
// readWatches() -> [watch, watch, ...]
|
|
9
6
|
// applyWatchesMutation(fn) -> diff-then-apply, returns { wrote, result }
|
|
10
7
|
|
|
11
|
-
const fs = require('fs');
|
|
12
|
-
const path = require('path');
|
|
13
|
-
|
|
14
8
|
function _toMs(v) {
|
|
15
9
|
if (v == null) return null;
|
|
16
10
|
if (typeof v === 'number') return Number.isFinite(v) ? v : null;
|
|
@@ -18,99 +12,12 @@ function _toMs(v) {
|
|
|
18
12
|
return Number.isFinite(parsed) ? parsed : null;
|
|
19
13
|
}
|
|
20
14
|
|
|
21
|
-
function _watchesFilePath() {
|
|
22
|
-
const shared = require('./shared');
|
|
23
|
-
return path.join(shared.MINIONS_DIR, 'engine', 'watches.json');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
15
|
function _parseRow(row) {
|
|
27
16
|
if (!row || !row.data) return null;
|
|
28
17
|
try { return JSON.parse(row.data); }
|
|
29
18
|
catch { return null; }
|
|
30
19
|
}
|
|
31
20
|
|
|
32
|
-
function _readJsonArrayFallback() {
|
|
33
|
-
const fp = _watchesFilePath();
|
|
34
|
-
let raw;
|
|
35
|
-
try { raw = fs.readFileSync(fp, 'utf8'); }
|
|
36
|
-
catch { return []; }
|
|
37
|
-
try {
|
|
38
|
-
const parsed = JSON.parse(raw);
|
|
39
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
40
|
-
} catch (e) {
|
|
41
|
-
try {
|
|
42
|
-
// eslint-disable-next-line no-console
|
|
43
|
-
console.warn(`[watches-store] corrupt JSON in ${fp}: ${e.message}`);
|
|
44
|
-
} catch { /* console may be wrapped */ }
|
|
45
|
-
return [];
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Content-hash fingerprint — (mtime, size) is too coarse: same-size
|
|
50
|
-
// content swaps (e.g. last_checked timestamps that always have the same
|
|
51
|
-
// byte length) inside the same ms tick collide on both axes. Hashing the
|
|
52
|
-
// bytes is the unambiguous signal. SHA-1 on a few-KB file is sub-ms.
|
|
53
|
-
let _lastMirrorHash = null;
|
|
54
|
-
|
|
55
|
-
const crypto = require('crypto');
|
|
56
|
-
|
|
57
|
-
function _fileContentHash(filePath) {
|
|
58
|
-
try {
|
|
59
|
-
const buf = fs.readFileSync(filePath);
|
|
60
|
-
return crypto.createHash('sha1').update(buf).digest('hex');
|
|
61
|
-
}
|
|
62
|
-
catch { return null; }
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function _hydrateFromJson(db) {
|
|
66
|
-
const items = _readJsonArrayFallback();
|
|
67
|
-
db.prepare('DELETE FROM watches').run();
|
|
68
|
-
if (items.length === 0) return;
|
|
69
|
-
const now = Date.now();
|
|
70
|
-
const ins = db.prepare(`
|
|
71
|
-
INSERT INTO watches (id, target, target_type, condition, status, owner, created_at, last_checked, last_triggered, data, updated_at)
|
|
72
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
73
|
-
ON CONFLICT(id) DO NOTHING
|
|
74
|
-
`);
|
|
75
|
-
for (const w of items) {
|
|
76
|
-
if (!w || !w.id) continue;
|
|
77
|
-
ins.run(
|
|
78
|
-
String(w.id),
|
|
79
|
-
typeof w.target === 'string' ? w.target : (w.target == null ? null : JSON.stringify(w.target)),
|
|
80
|
-
w.targetType || null,
|
|
81
|
-
w.condition || null,
|
|
82
|
-
String(w.status || 'active'),
|
|
83
|
-
w.owner || null,
|
|
84
|
-
_toMs(w.created_at),
|
|
85
|
-
_toMs(w.last_checked),
|
|
86
|
-
_toMs(w.last_triggered),
|
|
87
|
-
JSON.stringify(w),
|
|
88
|
-
now,
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function _resyncIfJsonDiverged(db) {
|
|
94
|
-
const jsonPath = _watchesFilePath();
|
|
95
|
-
const currentHash = _fileContentHash(jsonPath);
|
|
96
|
-
if (currentHash == null) return;
|
|
97
|
-
if (_lastMirrorHash != null && currentHash === _lastMirrorHash) return;
|
|
98
|
-
if (_lastMirrorHash == null) {
|
|
99
|
-
const sqlHas = db.prepare('SELECT 1 FROM watches LIMIT 1').get();
|
|
100
|
-
if (sqlHas) {
|
|
101
|
-
_lastMirrorHash = currentHash;
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
_hydrateFromJson(db);
|
|
106
|
-
_lastMirrorHash = currentHash;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Read all watch rows from SQL only. No JSON fallback — used by the
|
|
110
|
-
// mirror writer to avoid resurrecting deleted rows: after the mutation
|
|
111
|
-
// deletes a watch from SQL but BEFORE the mirror lands, the JSON still
|
|
112
|
-
// holds the pre-mutation content, so a fallback read would round-trip
|
|
113
|
-
// the deleted row right back into JSON.
|
|
114
21
|
function _readWatchesFromSqlOnly(db) {
|
|
115
22
|
const rows = db.prepare('SELECT data FROM watches ORDER BY rowid').all();
|
|
116
23
|
const out = [];
|
|
@@ -123,22 +30,7 @@ function _readWatchesFromSqlOnly(db) {
|
|
|
123
30
|
|
|
124
31
|
function readWatches() {
|
|
125
32
|
const { getDb } = require('./db');
|
|
126
|
-
|
|
127
|
-
try { db = getDb(); }
|
|
128
|
-
catch { return _readJsonArrayFallback(); }
|
|
129
|
-
|
|
130
|
-
_resyncIfJsonDiverged(db);
|
|
131
|
-
|
|
132
|
-
const out = _readWatchesFromSqlOnly(db);
|
|
133
|
-
if (out.length === 0) {
|
|
134
|
-
// SQL is empty for first-time hydrate: trust the JSON if it has
|
|
135
|
-
// content. After hydrate ran, SQL is empty iff the user really has
|
|
136
|
-
// no watches.
|
|
137
|
-
const fallback = _readJsonArrayFallback();
|
|
138
|
-
if (fallback.length > 0) return fallback;
|
|
139
|
-
return [];
|
|
140
|
-
}
|
|
141
|
-
return out;
|
|
33
|
+
return _readWatchesFromSqlOnly(getDb());
|
|
142
34
|
}
|
|
143
35
|
|
|
144
36
|
function _indexById(arr) {
|
|
@@ -216,7 +108,6 @@ function applyWatchesMutation(mutator) {
|
|
|
216
108
|
}
|
|
217
109
|
|
|
218
110
|
return withTransaction(db, () => {
|
|
219
|
-
_resyncIfJsonDiverged(db);
|
|
220
111
|
const before = readWatches();
|
|
221
112
|
const beforeSnapshot = JSON.parse(JSON.stringify(before));
|
|
222
113
|
const next = mutator(before);
|
|
@@ -229,24 +120,7 @@ function applyWatchesMutation(mutator) {
|
|
|
229
120
|
});
|
|
230
121
|
}
|
|
231
122
|
|
|
232
|
-
function _mirrorJsonFromSql(filePath) {
|
|
233
|
-
try {
|
|
234
|
-
const shared = require('./shared');
|
|
235
|
-
const { getDb } = require('./db');
|
|
236
|
-
// Read SQL directly — bypass the JSON fallback so a deletion that
|
|
237
|
-
// leaves SQL empty doesn't resurrect from the stale JSON content.
|
|
238
|
-
const items = _readWatchesFromSqlOnly(getDb());
|
|
239
|
-
const target = filePath || _watchesFilePath();
|
|
240
|
-
shared.safeWrite(target, items);
|
|
241
|
-
const h = _fileContentHash(target);
|
|
242
|
-
if (h != null) _lastMirrorHash = h;
|
|
243
|
-
} catch {
|
|
244
|
-
// Mirror failures are non-fatal — SQL has already committed.
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
123
|
module.exports = {
|
|
249
124
|
readWatches,
|
|
250
125
|
applyWatchesMutation,
|
|
251
|
-
_mirrorJsonFromSql,
|
|
252
126
|
};
|