@yemi33/minions 0.1.2369 → 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.
Files changed (79) hide show
  1. package/bin/minions.js +38 -11
  2. package/dashboard/js/memory-search.js +112 -0
  3. package/dashboard/js/render-kb.js +15 -0
  4. package/dashboard/js/render-other.js +2 -30
  5. package/dashboard/js/render-work-items.js +0 -34
  6. package/dashboard/js/settings.js +27 -27
  7. package/dashboard/pages/inbox.html +1 -1
  8. package/dashboard/pages/tools.html +1 -1
  9. package/dashboard.js +148 -258
  10. package/docs/README.md +2 -3
  11. package/docs/architecture.excalidraw +2 -2
  12. package/docs/auto-discovery.md +3 -3
  13. package/docs/completion-reports.md +0 -121
  14. package/docs/copilot-cli-schema.md +9 -17
  15. package/docs/deprecated.json +0 -51
  16. package/docs/design-state-storage.md +4 -4
  17. package/docs/harness-propagation.md +33 -263
  18. package/docs/human-vs-automated.md +1 -1
  19. package/docs/named-agents.md +0 -2
  20. package/docs/plan-lifecycle.md +5 -6
  21. package/docs/qa-runbook-lifecycle.md +10 -25
  22. package/docs/shared-lifecycle-module-map.md +2 -10
  23. package/docs/team-memory.md +18 -4
  24. package/engine/ado-comment.js +5 -11
  25. package/engine/ado.js +10 -5
  26. package/engine/cleanup.js +16 -36
  27. package/engine/cli.js +13 -175
  28. package/engine/comment-format.js +8 -182
  29. package/engine/consolidation.js +72 -1
  30. package/engine/db/index.js +60 -10
  31. package/engine/db/migrations/017-agent-memory.js +208 -0
  32. package/engine/db/migrations/018-sql-only-cutover.js +56 -0
  33. package/engine/dispatch-store.js +0 -114
  34. package/engine/dispatch.js +1 -6
  35. package/engine/features.js +6 -0
  36. package/engine/gh-comment.js +2 -10
  37. package/engine/github.js +8 -6
  38. package/engine/lifecycle.js +141 -173
  39. package/engine/llm.js +9 -11
  40. package/engine/managed-spawn.js +1 -6
  41. package/engine/memory-retrieval.js +165 -0
  42. package/engine/memory-store.js +367 -0
  43. package/engine/metrics-store.js +4 -128
  44. package/engine/pipeline.js +4 -7
  45. package/engine/playbook.js +64 -198
  46. package/engine/prd-store.js +115 -141
  47. package/engine/preflight.js +8 -55
  48. package/engine/projects.js +34 -41
  49. package/engine/pull-requests-store.js +9 -234
  50. package/engine/qa-runs.js +4 -15
  51. package/engine/qa-sessions.js +5 -17
  52. package/engine/queries.js +23 -103
  53. package/engine/routing.js +1 -1
  54. package/engine/runtimes/claude.js +1 -2
  55. package/engine/runtimes/codex.js +0 -2
  56. package/engine/runtimes/copilot.js +1 -4
  57. package/engine/shared-branch-pr-reconcile.js +2 -5
  58. package/engine/shared.js +179 -533
  59. package/engine/small-state-store.js +12 -647
  60. package/engine/spawn-agent.js +5 -96
  61. package/engine/state-operations.js +166 -0
  62. package/engine/supervisor.js +0 -1
  63. package/engine/watch-actions.js +2 -3
  64. package/engine/watches-store.js +2 -128
  65. package/engine/work-items-store.js +3 -254
  66. package/engine.js +102 -334
  67. package/package.json +2 -2
  68. package/playbooks/build-fix-complex.md +0 -4
  69. package/playbooks/fix.md +0 -4
  70. package/playbooks/implement-shared.md +0 -4
  71. package/playbooks/implement.md +0 -4
  72. package/playbooks/plan-to-prd.md +16 -11
  73. package/playbooks/plan.md +0 -4
  74. package/playbooks/review.md +0 -6
  75. package/playbooks/shared-rules.md +0 -65
  76. package/docs/harness-transparency.md +0 -199
  77. package/docs/project-skills.md +0 -193
  78. package/engine/discover-project-skills.js +0 -673
  79. package/engine/playbook-intents.js +0 -76
@@ -0,0 +1,56 @@
1
+ // Retire live JSON mirror metadata after all runtime stores become SQL-only.
2
+ module.exports = {
3
+ version: 18,
4
+ description: 'SQL-only runtime-state cutover; drop obsolete PR mirror hashes',
5
+ up(db, { fs, path }) {
6
+ let legacyPrdCount = 0;
7
+ let importedPrdCount = 0;
8
+ const minionsDir = process.env.MINIONS_TEST_DIR || process.env.MINIONS_HOME
9
+ || (() => { try { return require('../../shared').MINIONS_DIR; } catch { return null; } })();
10
+ if (minionsDir) {
11
+ const importDir = (dir, archived) => {
12
+ let files = [];
13
+ try { files = fs.readdirSync(dir).filter(f => f.endsWith('.json')); } catch { return; }
14
+ const { _upsertPrd } = require('../../prd-store');
15
+ for (const filename of files) {
16
+ legacyPrdCount++;
17
+ const filePath = path.join(dir, filename);
18
+ let prd;
19
+ try { prd = JSON.parse(fs.readFileSync(filePath, 'utf8')); }
20
+ catch (e) { throw new Error(`cannot import legacy PRD ${filePath}: ${e.message}`); }
21
+ if (!prd || typeof prd !== 'object' || Array.isArray(prd)) {
22
+ throw new Error(`cannot import legacy PRD ${filePath}: expected an object`);
23
+ }
24
+ _upsertPrd(db, filename, archived, prd, Date.now());
25
+ importedPrdCount++;
26
+ }
27
+ };
28
+ importDir(path.join(minionsDir, 'prd'), 0);
29
+ importDir(path.join(minionsDir, 'prd', 'archive'), 1);
30
+ }
31
+ if (importedPrdCount !== legacyPrdCount) {
32
+ throw new Error(`legacy PRD import count mismatch: found ${legacyPrdCount}, imported ${importedPrdCount}`);
33
+ }
34
+ db.exec(`
35
+ DROP TABLE IF EXISTS pr_mirror_hashes;
36
+ CREATE TABLE IF NOT EXISTS state_markers (
37
+ key TEXT PRIMARY KEY,
38
+ value TEXT NOT NULL,
39
+ updated_at INTEGER NOT NULL
40
+ );
41
+ `);
42
+ const quickCheck = db.prepare('PRAGMA quick_check').all();
43
+ if (quickCheck.length !== 1 || quickCheck[0].quick_check !== 'ok') {
44
+ throw new Error(`SQLite quick_check failed: ${JSON.stringify(quickCheck)}`);
45
+ }
46
+ const foreignKeyErrors = db.prepare('PRAGMA foreign_key_check').all();
47
+ if (foreignKeyErrors.length) {
48
+ throw new Error(`SQLite foreign_key_check failed: ${JSON.stringify(foreignKeyErrors)}`);
49
+ }
50
+ db.prepare(`
51
+ INSERT INTO state_markers (key, value, updated_at)
52
+ VALUES ('sql_only_cutover', '1', ?)
53
+ ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at
54
+ `).run(Date.now());
55
+ },
56
+ };
@@ -17,18 +17,6 @@
17
17
 
18
18
  const SECTIONS = ['pending', 'active', 'completed', 'review'];
19
19
 
20
- // First-touch guard (P-5f0e8c27). Tracks whether a dispatch mutation has
21
- // written SQL in THIS process. Once SQL has been written, an empty read
22
- // reflects a genuine prune-to-zero — NOT a fresh install — so we must not
23
- // resurrect a stale dispatch.json (e.g. a crash between the SQL COMMIT and
24
- // the post-commit mirror write leaves dispatch.json holding the pre-prune
25
- // rows). This mirrors the `_resyncIfJsonDiverged` first-touch discipline the
26
- // work-items / pull-requests / metrics / watches stores already use: once SQL
27
- // is the established source of truth this process, JSON never overrides it.
28
- // Reset only by process restart (module reload); test isolation re-requires
29
- // the module via withFreshDb's cache-bust, so each test starts fresh.
30
- let _sqlWrittenThisProcess = false;
31
-
32
20
  function _emptySectioned() {
33
21
  return { pending: [], active: [], completed: [], review: [] };
34
22
  }
@@ -60,33 +48,6 @@ function readDispatchSectioned() {
60
48
  ORDER BY status, created_at
61
49
  `).all();
62
50
 
63
- if (rows.length === 0 && !_sqlWrittenThisProcess) {
64
- // SQL has no live dispatches AND no mutation has written SQL this process.
65
- // Only then is the JSON file a trustworthy fallback. Two scenarios:
66
- // (a) Production fresh install — JSON file is empty/missing, fallback
67
- // returns the same empty sectioned object. No harm.
68
- // (b) Test that wrote dispatch.json directly (legacy test helpers in
69
- // timeout-behavioral / orphan-* / etc. — they bypass the proper
70
- // mutateDispatch API and seed via fs.writeFileSync). Picking up
71
- // the JSON keeps those tests working through the transition without
72
- // touching every helper. Phase 1.5 migrates them to the proper API.
73
- //
74
- // The `!_sqlWrittenThisProcess` gate is the first-touch guard: once a
75
- // mutation has written SQL this process (e.g. the engine pruned the last
76
- // dispatch to zero), an empty SQL read is the real state. Adopting a stale
77
- // non-empty dispatch.json here would resurrect pruned records and could
78
- // trigger a duplicate dispatch before the next mutation self-heals the
79
- // mirror — the exact crash-window bug this guard closes.
80
- const fallback = _readDispatchJsonFallback();
81
- const hasContent = (
82
- (fallback.pending && fallback.pending.length > 0) ||
83
- (fallback.active && fallback.active.length > 0) ||
84
- (fallback.completed && fallback.completed.length > 0) ||
85
- (fallback.review && fallback.review.length > 0)
86
- );
87
- if (hasContent) return fallback;
88
- }
89
-
90
51
  const out = _emptySectioned();
91
52
  for (const row of rows) {
92
53
  const rec = _parseRow(row);
@@ -96,37 +57,6 @@ function readDispatchSectioned() {
96
57
  return out;
97
58
  }
98
59
 
99
- function _readDispatchJsonFallback() {
100
- const path = require('path');
101
- const fs = require('fs');
102
- const shared = require('./shared');
103
- const dispatchPath = path.join(shared.MINIONS_DIR, 'engine', 'dispatch.json');
104
- let raw;
105
- try {
106
- raw = fs.readFileSync(dispatchPath, 'utf8');
107
- } catch {
108
- // ENOENT / permission / etc. — file genuinely absent. Silently
109
- // return an empty sectioned object; that's the legacy semantics.
110
- return _emptySectioned();
111
- }
112
- try {
113
- const parsed = JSON.parse(raw);
114
- const out = _emptySectioned();
115
- for (const s of SECTIONS) {
116
- if (Array.isArray(parsed[s])) out[s] = parsed[s];
117
- }
118
- return out;
119
- } catch (e) {
120
- // File exists but parse failed — that's user-visible corruption.
121
- // Surface to logs the same way the legacy queries.getDispatch path
122
- // did via shared.readJsonNoRestore. (Phase 1 contract test
123
- // "getDispatch warns on corrupt dispatch.json".)
124
- // eslint-disable-next-line no-console
125
- console.warn(`[dispatch-store] corrupt JSON in ${dispatchPath}: ${e.message}`);
126
- return _emptySectioned();
127
- }
128
- }
129
-
130
60
  // Internal: read the full (unfiltered) dispatch state from SQL.
131
61
  // Used by applyDispatchMutation so the diff sees ALL completed rows,
132
62
  // not just the recent window. Mutators that prune old completed
@@ -272,57 +202,13 @@ function applyDispatchMutation(mutator) {
272
202
  const wrote = _applyDispatchDiff(db, diff);
273
203
  // First-touch guard: record that SQL was written this process so a later
274
204
  // empty read won't resurrect a stale dispatch.json (see readDispatchSectioned).
275
- if (wrote) _sqlWrittenThisProcess = true;
276
205
  return { wrote, result: after };
277
206
  });
278
207
  }
279
208
 
280
- // Mirror the canonical SQL state back to dispatch.json after a successful
281
- // mutation. Phase 1 keeps the JSON file as a *read-only derivative* of SQL
282
- // — never an independent source of truth — so legacy callers that
283
- // readFileSync the file directly (a couple of internal paths + the broader
284
- // test suite) keep seeing consistent data. There's no drift opportunity
285
- // because:
286
- //
287
- // - The mirror is always regenerated from the SQL table, never from
288
- // in-memory state, so a write that lands transactionally in SQL but
289
- // then crashes before the mirror is also written produces a stale
290
- // mirror — but the SQL state is correct, and the next successful
291
- // mutation overwrites the mirror.
292
- // - No code path writes to dispatch.json independently anymore;
293
- // mutateDispatch is the only writer.
294
- //
295
- // Phase 1.5 will remove the mirror once we've migrated the remaining
296
- // direct-readers (engine/queries.js fallback path, engine/routing.js,
297
- // the test infrastructure) to use readDispatchSectioned() / getDispatch().
298
- function _mirrorJsonFromSql() {
299
- try {
300
- const path = require('path');
301
- const fs = require('fs');
302
- const shared = require('./shared');
303
- // The full sectioned view (not the 24h-windowed one) so the JSON
304
- // mirror matches what a legacy reader would have seen before Phase 1.
305
- const full = _readDispatchSectionedFull();
306
- const dispatchPath = path.join(shared.MINIONS_DIR, 'engine', 'dispatch.json');
307
- // safeWrite handles the atomic rename + .backup sidecar. Best-effort —
308
- // failure here doesn't propagate (SQL is source of truth, mirror is
309
- // optional refresh material).
310
- shared.safeWrite(dispatchPath, full);
311
- // Re-derive what the parent shared.safeWrite would have done so an
312
- // operator copying the file out gets matched fs.statSync mtimes
313
- // (the dashboard's slow-state tracker watches this file as part of
314
- // the dispatch path list). No-op if safeWrite already advanced mtime.
315
- try { fs.utimesSync(dispatchPath, new Date(), new Date()); } catch { /* best-effort */ }
316
- } catch {
317
- // Mirror failures are non-fatal: SQL has already committed. Tests +
318
- // legacy readers will see the previous mirror until the next write.
319
- }
320
- }
321
-
322
209
  module.exports = {
323
210
  readDispatchSectioned,
324
211
  applyDispatchMutation,
325
212
  _readDispatchSectionedFull,
326
213
  _computeDispatchDiff,
327
- _mirrorJsonFromSql,
328
214
  };
@@ -122,11 +122,6 @@ function mutateDispatch(mutator) {
122
122
  if (wrote) {
123
123
  try { require('./queries').invalidateDispatchCache(); } catch {}
124
124
  try { require('./db-events').emitStateEvent('dispatch'); } catch { /* optional */ }
125
- // Mirror back to dispatch.json for tests + tools that fs.readFileSync
126
- // the file directly. SQL is the source of truth; the JSON file is
127
- // regenerated from SQL on every successful mutation, never independently
128
- // mutated. Cheap to delete once those direct-JSON readers are gone.
129
- try { store._mirrorJsonFromSql(); } catch { /* best-effort */ }
130
125
  }
131
126
  return result;
132
127
  }
@@ -784,6 +779,7 @@ function isRetryableFailureReason(reason = '', failureClass = '') {
784
779
  FAILURE_CLASS.LIVE_CHECKOUT_STALE_BASE, // W-mr98op8w000ma4ad — live-checkout STALE-BASE GUARD refused to fork a new branch off a local mainRef that has diverged from origin/<mainRef> with unpushed commits; contaminated local base does not change on retry (operator must reconcile the local base branch — the engine never resets it — then re-dispatch)
785
780
  FAILURE_CLASS.LIVE_CHECKOUT_WRONG_BASE, // W-mr3lunnq000o9f41 — live-checkout refused to fork a new branch because the operator HEAD is on an unrelated branch (not the project base) and no local base ref was usable; forking off it would bake that branch's commits into the new PR (scope contamination). Deterministic — mechanical retry reproduces it (operator must `git checkout <mainRef>`, then re-dispatch)
786
781
  FAILURE_CLASS.OUTPUT_TRUNCATED, // P-8e4c2a17 — agent stdout exceeded the hard capture cap before the terminal result event; mechanical retry just reproduces the overflow (agent must reduce output volume or the task must be split)
782
+ FAILURE_CLASS.PRE_DISPATCH_EVAL_STUCK, // W-mrcrh5hj0017d22f — pre-dispatch-eval rejected the SAME unchanged description ENGINE_DEFAULTS.preDispatchEvalMaxRejections times in a row; non-retryable until the description changes (engine/shared.js:4986)
787
783
  ]);
788
784
  if (neverRetry.has(failureClass)) return false;
789
785
  }
@@ -1454,7 +1450,6 @@ function cleanDispatchEntries(matchFn) {
1454
1450
  * Sets status=CANCELLED + _cancelledBy=reason. Returns count cancelled.
1455
1451
  */
1456
1452
  function cancelPendingWorkItems(wiPath, matchFn, reason) {
1457
- if (!fs.existsSync(wiPath)) return 0;
1458
1453
  let cancelled = 0;
1459
1454
  try {
1460
1455
  mutateWorkItems(wiPath, items => {
@@ -95,6 +95,12 @@ const FEATURES = {
95
95
  addedIn: '0.1.2232',
96
96
  expires: '2026-12-01',
97
97
  },
98
+ 'memoryRetrieval': {
99
+ description: 'Select bounded task-relevant long-term memory with SQLite FTS5 instead of injecting only recent team and personal notes. Starts in shadow mode by default.',
100
+ default: false,
101
+ addedIn: '0.1.2233',
102
+ expires: '2027-01-31',
103
+ },
98
104
  };
99
105
 
100
106
  const ENV_TRUTHY = new Set(['1', 'true', 'on', 'yes']);
@@ -42,8 +42,6 @@ const os = require('os');
42
42
  const { execFileSync: _execFileSync } = require('child_process');
43
43
  const { resolveTokenForSlug: _defaultResolveTokenForSlug } = require('./gh-token');
44
44
  const {
45
- buildHarnessUsedSection,
46
- buildSkippedSkillSection,
47
45
  linkifyBrandTrailer,
48
46
  MINIONS_BRAND_URL,
49
47
  // Neutral marker + body builder now lives in comment-format.js so the GitHub
@@ -155,15 +153,13 @@ function postPrComment({
155
153
  agentId,
156
154
  kind,
157
155
  workItemId,
158
- harnessUsed,
159
- skillSkipped,
160
156
  timeoutMs = 30000,
161
157
  execFileSync = _execFileSync,
162
158
  resolveTokenForSlug,
163
159
  } = {}) {
164
160
  _validateRepo(repo);
165
161
  _validatePrNumber(prNumber);
166
- const finalBody = buildMinionsCommentBody({ agentId, kind, workItemId, body, harnessUsed, skillSkipped });
162
+ const finalBody = buildMinionsCommentBody({ agentId, kind, workItemId, body });
167
163
  const file = _writeTempBodyFile(finalBody);
168
164
  const env = _resolveTokenEnvForRepo(repo, resolveTokenForSlug);
169
165
  try {
@@ -186,15 +182,13 @@ function postPrReviewComment({
186
182
  agentId,
187
183
  kind,
188
184
  workItemId,
189
- harnessUsed,
190
- skillSkipped,
191
185
  timeoutMs = 30000,
192
186
  execFileSync = _execFileSync,
193
187
  resolveTokenForSlug,
194
188
  } = {}) {
195
189
  _validateRepo(repo);
196
190
  _validatePrNumber(prNumber);
197
- const finalBody = buildMinionsCommentBody({ agentId, kind, workItemId, body, harnessUsed, skillSkipped });
191
+ const finalBody = buildMinionsCommentBody({ agentId, kind, workItemId, body });
198
192
  const file = _writeTempBodyFile(finalBody);
199
193
  const env = _resolveTokenEnvForRepo(repo, resolveTokenForSlug);
200
194
  try {
@@ -213,8 +207,6 @@ function postPrReviewComment({
213
207
  module.exports = {
214
208
  // Builders / parsers (pure functions — usable from anywhere)
215
209
  buildMinionsCommentBody,
216
- buildHarnessUsedSection, // re-export of engine/comment-format.js for comment callers
217
- buildSkippedSkillSection, // re-export of engine/comment-format.js for comment callers
218
210
  linkifyBrandTrailer, // re-export of engine/comment-format.js for comment callers
219
211
  MINIONS_BRAND_URL, // re-export of engine/comment-format.js for comment callers
220
212
  parseMinionsMarker,
package/engine/github.js CHANGED
@@ -840,12 +840,14 @@ async function pollPrStatus(config) {
840
840
  else if (prData.state === 'closed') newStatus = PR_STATUS.ABANDONED;
841
841
  else if (prData.state === 'open') newStatus = PR_STATUS.ACTIVE;
842
842
 
843
- // W-mpej044m00076d63: backfill `pr.created` from the platform's real
844
- // creation timestamp. lifecycle.js attach paths deliberately omit
845
- // `created` so this poll is the single source of truth — and historical
846
- // records that pre-date the fix get backfilled the next time they're
847
- // polled. Idempotent: only writes when missing.
848
- if (!pr.created && prData.created_at) {
843
+ // W-mpej044m00076d63 / W-mrezh0yb0007f733: keep `pr.created` aligned with
844
+ // GitHub's real `created_at`. The platform is authoritative for when a PR
845
+ // was opened, so adopt its value whenever the stored one is missing, a
846
+ // legacy date-only stamp, or a link/attach-time `now()` default (the bug
847
+ // where manually-linked PRs showed a "created today" date).
848
+ // `shouldAdoptPlatformCreated` compares instants so equivalent
849
+ // representations don't churn. Idempotent.
850
+ if (shared.shouldAdoptPlatformCreated(pr.created, prData.created_at)) {
849
851
  pr.created = prData.created_at;
850
852
  updated = true;
851
853
  }