@yemi33/minions 0.1.2378 → 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.
Files changed (100) hide show
  1. package/bin/minions.js +19 -9
  2. package/dashboard/js/refresh.js +2 -3
  3. package/dashboard/js/render-prd.js +1 -1
  4. package/dashboard/js/render-work-items.js +13 -6
  5. package/dashboard.js +393 -721
  6. package/docs/README.md +1 -0
  7. package/docs/architecture-review-2026-07-09.md +2 -4
  8. package/docs/auto-discovery.md +36 -49
  9. package/docs/blog-first-successful-dispatch.md +1 -1
  10. package/docs/branch-derivation.md +2 -2
  11. package/docs/command-center.md +1 -1
  12. package/docs/completion-reports.md +14 -4
  13. package/docs/constellation-bridge.md +59 -10
  14. package/docs/constellation-style-telemetry.md +6 -6
  15. package/docs/cooldown-merge-semantics.md +4 -0
  16. package/docs/copilot-cli-schema.md +3 -3
  17. package/docs/cross-repo-plans.md +17 -17
  18. package/docs/deprecated.json +2 -2
  19. package/docs/design-inbox-entries-schema.md +31 -39
  20. package/docs/design-state-storage.md +1 -1
  21. package/docs/documentation-audit-2026-07-09.md +2 -2
  22. package/docs/engine-restart.md +20 -8
  23. package/docs/harness-mode.md +1 -1
  24. package/docs/managed-spawn.md +4 -4
  25. package/docs/onboarding.md +1 -2
  26. package/docs/pr-comment-followup.md +3 -3
  27. package/docs/pr-review-fix-loop.md +1 -1
  28. package/docs/proposals/repo-pool-for-live-checkout.md +1 -2
  29. package/docs/qa-runbook-lifecycle.md +4 -4
  30. package/docs/qa-runbooks.md +2 -2
  31. package/docs/rfc-completion-json.md +4 -1
  32. package/docs/runtime-adapters.md +1 -1
  33. package/docs/self-improvement.md +4 -5
  34. package/docs/shared-lifecycle-module-map.md +3 -1
  35. package/docs/slim-ux/architecture-suggestions.md +5 -6
  36. package/docs/slim-ux/concepts.md +23 -25
  37. package/docs/watches.md +7 -7
  38. package/docs/workspace-manifests.md +1 -1
  39. package/docs/worktree-lifecycle.md +1 -1
  40. package/engine/abandoned-pr-reconciliation.js +4 -5
  41. package/engine/acp-transport.js +49 -8
  42. package/engine/ado-status.js +5 -5
  43. package/engine/ado.js +20 -25
  44. package/engine/agent-worker-pool.js +124 -15
  45. package/engine/bridge.js +260 -5
  46. package/engine/cleanup.js +48 -131
  47. package/engine/cli.js +125 -83
  48. package/engine/cooldown.js +9 -16
  49. package/engine/db/index.js +22 -9
  50. package/engine/db/migrations/009-qa.js +1 -1
  51. package/engine/db/migrations/020-qa-session-scopes.js +23 -0
  52. package/engine/db/migrations/021-archived-work-items.js +72 -0
  53. package/engine/db/migrations/022-global-cc-session.js +31 -0
  54. package/engine/db/migrations/023-engine-state.js +30 -0
  55. package/engine/db/migrations/024-prd-ghost-collisions.js +53 -0
  56. package/engine/db/migrations/025-malformed-work-item-phantoms.js +33 -0
  57. package/engine/dispatch-store.js +2 -7
  58. package/engine/dispatch.js +34 -41
  59. package/engine/github.js +20 -27
  60. package/engine/lifecycle.js +221 -283
  61. package/engine/llm.js +1 -1
  62. package/engine/logs-store.js +2 -2
  63. package/engine/managed-spawn.js +2 -23
  64. package/engine/meeting.js +6 -6
  65. package/engine/metrics-store.js +2 -2
  66. package/engine/note-link-backfill.js +6 -11
  67. package/engine/pipeline.js +18 -36
  68. package/engine/playbook.js +1 -1
  69. package/engine/pooled-agent-process.js +46 -26
  70. package/engine/prd-store.js +73 -54
  71. package/engine/preflight.js +2 -5
  72. package/engine/projects.js +15 -62
  73. package/engine/pull-requests-store.js +0 -17
  74. package/engine/qa-runbooks.js +2 -2
  75. package/engine/qa-runs.js +1 -8
  76. package/engine/qa-sessions.js +41 -64
  77. package/engine/queries.js +120 -219
  78. package/engine/routing.js +3 -5
  79. package/engine/scheduler.js +0 -4
  80. package/engine/shared-branch-pr-reconcile.js +2 -3
  81. package/engine/shared.js +132 -637
  82. package/engine/small-state-store.js +89 -10
  83. package/engine/state-operations.js +16 -4
  84. package/engine/stdio-timestamps.js +1 -1
  85. package/engine/timeout.js +5 -12
  86. package/engine/watch-actions.js +20 -22
  87. package/engine/watches-store.js +1 -1
  88. package/engine/watches.js +6 -10
  89. package/engine/work-item-validation.js +52 -0
  90. package/engine/work-items-store.js +127 -29
  91. package/engine/worktree-gc.js +2 -2
  92. package/engine/worktree-pool.js +8 -18
  93. package/engine.js +167 -349
  94. package/minions.js +2 -2
  95. package/package.json +1 -1
  96. package/playbooks/plan-to-prd.md +2 -2
  97. package/playbooks/shared-rules.md +3 -3
  98. package/playbooks/templates/followup-dispatch.md +1 -1
  99. package/playbooks/verify.md +1 -1
  100. package/prompts/cc-system.md +2 -2
@@ -164,13 +164,6 @@ function _isSafeSessionId(id) {
164
164
  return _isNonEmptyString(id) && id.length <= LIMITS.idMax && _KEBAB_RE.test(id);
165
165
  }
166
166
 
167
- // Dynamic paths — respect MINIONS_TEST_DIR for test isolation. shared.MINIONS_DIR
168
- // resolves at every call so MINIONS_TEST_DIR=foo flips the resolution
169
- // without re-requiring this module (mirrors qa-runs.js pattern).
170
- function qaSessionsPath() {
171
- return path.join(shared.MINIONS_DIR, 'engine', 'qa-sessions.json');
172
- }
173
-
174
167
  function qaTestsDir() {
175
168
  return path.join(shared.MINIONS_DIR, 'engine', 'qa-tests');
176
169
  }
@@ -332,11 +325,11 @@ function validateSpec(spec) {
332
325
  // ── CRUD ────────────────────────────────────────────────────────────────────
333
326
 
334
327
  /**
335
- * Create a session in `pending` state and persist it to qa-sessions.json.
328
+ * Create a session in `pending` state and persist it to SQL.
336
329
  *
337
330
  * The caller (POST /api/qa/session handler) is responsible for queuing the
338
331
  * SETUP work item via buildSetupWorkItem() + the standard work-items/dispatch
339
- * flow. createSession() intentionally does NOT touch dispatch.json so the
332
+ * flow. createSession() intentionally does not touch dispatch state so the
340
333
  * pure persistence layer stays unit-testable without standing up the whole
341
334
  * engine.
342
335
  *
@@ -407,10 +400,8 @@ function createSession(spec) {
407
400
  // 1 (multi-project fan-out). Single-project sessions skip this map and
408
401
  // use session.state directly.
409
402
  setupStatus: null,
410
- // primaryWiPath is stamped at queueSetup time so handleSetupComplete can
411
- // queue DRAFT on the right work-items file without re-resolving the
412
- // project through dashboard config.
413
- primaryWiPath: null,
403
+ // Stamped at queueSetup time so DRAFT stays in the primary scope.
404
+ primaryScope: null,
414
405
  // Per-phase WI links — back-filled by setSessionWorkItem when the
415
406
  // dashboard endpoint or lifecycle hook queues the next phase.
416
407
  workItems: { setup: null, draft: null, execute: null },
@@ -641,10 +632,10 @@ function transitionSession(id, toState, patch = {}) {
641
632
  if (patch && typeof patch === 'object' && !Array.isArray(patch)) {
642
633
  // Whitelist mutable fields to keep transitionSession from rewriting
643
634
  // immutable spec/createdAt fields by mistake.
644
- // W-mpq6xqzj000606d0 — added `setupStatus` and `primaryWiPath` so the
635
+ // W-mpq6xqzj000606d0 — `setupStatus` and `primaryScope` let the
645
636
  // multi-project fan-in handler can stamp per-project completion state
646
637
  // through the same locked path used by every other mutation.
647
- for (const field of ['summary', 'error', 'failureClass', 'testFile', 'qaRunId', 'managedSpawnHealth', 'setupStatus', 'primaryWiPath']) {
638
+ for (const field of ['summary', 'error', 'failureClass', 'testFile', 'qaRunId', 'managedSpawnHealth', 'setupStatus', 'primaryScope']) {
648
639
  if (Object.prototype.hasOwnProperty.call(patch, field)) {
649
640
  session[field] = patch[field];
650
641
  }
@@ -761,7 +752,7 @@ function _baseWorkItem(session, phase, { title, description, project, primary, c
761
752
  * W-mpq6xqzj000606d0 — When the session has multiple projects (fan-out),
762
753
  * `primary` distinguishes the orchestrator WI (true; carries the full
763
754
  * coServices list) from each co-service WI (false; coServices === []). The
764
- * session's DRAFT phase keys off the primary's wiPath.
755
+ * session's DRAFT phase keys off the primary scope.
765
756
  */
766
757
  function buildSetupWorkItem(session, { project, primary, coServices, primaryProject } = {}) {
767
758
  const isPrimary = primary === undefined ? true : !!primary;
@@ -881,12 +872,8 @@ function _summarizeTarget(target) {
881
872
  // dispatch + work-items are lazy-required inside _queueWorkItem to keep
882
873
  // `require('./qa-sessions')` cycle-safe at the top of lifecycle.js.
883
874
 
884
- function _queueWorkItem(wi, wiPath) {
885
- // Append the WI to the project (or central) work-items file, then queue a
886
- // dispatch entry that wraps it. Mirrors the qa-validate flow at
887
- // dashboard.js handleQaRunbookRun (line 9985+). Both writes go through their
888
- // module-internal locks so concurrent dashboard calls don't lose entries.
889
- shared.mutateWorkItems(wiPath, (items) => {
875
+ function _queueWorkItem(wi, scope) {
876
+ shared.mutateWorkItems(scope, (items) => {
890
877
  if (!Array.isArray(items)) items = [];
891
878
  if (!items.some(i => i && i.id === wi.id)) items.push(wi);
892
879
  return items;
@@ -905,35 +892,27 @@ function _queueWorkItem(wi, wiPath) {
905
892
  * Validates pending → spawning, queues the SETUP WI(s), returns the queued
906
893
  * primary WI id (back-compat).
907
894
  *
908
- * W-mpq6xqzj000606d0 — multi-project fan-out. Two calling shapes are
909
- * accepted:
910
- * 1. Legacy single-target: `{ wiPath, project }` (project may be null for
911
- * central). Builds and queues one SETUP WI exactly like before.
912
- * 2. Multi-target: `{ resolvedTargets: [{ project, wiPath }, ...] }`. The
913
- * FIRST entry is treated as the primary (drives DRAFT/EXECUTE later);
914
- * the rest are co-services. One SETUP WI is queued per entry, and
915
- * `session.setupStatus` is initialized with a `pending` entry per
916
- * project so handleSetupComplete can fan-in.
895
+ * W-mpq6xqzj000606d0 — multi-project fan-out. The first target drives
896
+ * DRAFT/EXECUTE; the rest are co-services.
917
897
  *
918
898
  * @param {string} sessionId
919
899
  * @param {object} opts
920
- * @param {string} [opts.wiPath] - legacy single-target wiPath
921
- * @param {string} [opts.project] - legacy single-target project name
922
- * @param {Array<{project:?string, wiPath:string}>} [opts.resolvedTargets]
900
+ * @param {string} [opts.scope]
901
+ * @param {string} [opts.project]
902
+ * @param {Array<{project:?string, scope:string}>} [opts.resolvedTargets]
923
903
  * @returns {string} the queued PRIMARY SETUP WI id
924
904
  */
925
905
  function queueSetup(sessionId, opts = {}) {
926
906
  let targets = Array.isArray(opts.resolvedTargets) ? opts.resolvedTargets.slice() : null;
927
907
  if (!targets || targets.length === 0) {
928
- if (!_isNonEmptyString(opts.wiPath)) {
929
- throw new Error('qa-sessions: queueSetup requires wiPath or resolvedTargets');
908
+ if (!_isNonEmptyString(opts.scope)) {
909
+ throw new Error('qa-sessions: queueSetup requires scope or resolvedTargets');
930
910
  }
931
- targets = [{ project: opts.project || null, wiPath: opts.wiPath }];
911
+ targets = [{ project: opts.project || null, scope: opts.scope }];
932
912
  }
933
- // Defensive: each target needs a wiPath. project may be null for central.
934
913
  for (const t of targets) {
935
- if (!t || !_isNonEmptyString(t.wiPath)) {
936
- throw new Error('qa-sessions: queueSetup target missing wiPath');
914
+ if (!t || !_isNonEmptyString(t.scope)) {
915
+ throw new Error('qa-sessions: queueSetup target missing scope');
937
916
  }
938
917
  }
939
918
  if (targets.length > LIMITS.projectsMax) {
@@ -947,7 +926,7 @@ function queueSetup(sessionId, opts = {}) {
947
926
  const coServiceProjects = targets.slice(1).map(t => t.project || null).filter(Boolean);
948
927
  const isMulti = targets.length > 1;
949
928
 
950
- // Initialize setupStatus + primaryWiPath BEFORE transitioning so the
929
+ // Initialize setupStatus + primaryScope BEFORE transitioning so the
951
930
  // fan-in handler sees the full project list even on a racing completion.
952
931
  // Single-project sessions skip the map to keep blast radius minimal.
953
932
  const setupStatusPatch = isMulti
@@ -964,7 +943,7 @@ function queueSetup(sessionId, opts = {}) {
964
943
  // double-queueing.
965
944
  markSpawning(sessionId, {
966
945
  setupStatus: setupStatusPatch,
967
- primaryWiPath: primaryTarget.wiPath,
946
+ primaryScope: primaryTarget.scope,
968
947
  });
969
948
 
970
949
  // Build + queue one WI per target. Primary is index 0; coServices is the
@@ -981,7 +960,7 @@ function queueSetup(sessionId, opts = {}) {
981
960
  coServices: isPrimary ? coServiceProjects : [],
982
961
  primaryProject: primaryTarget.project || null,
983
962
  });
984
- _queueWorkItem(wi, t.wiPath);
963
+ _queueWorkItem(wi, t.scope);
985
964
  builtWiIds[t.project || '__central__'] = wi.id;
986
965
  if (isPrimary) primaryWiId = wi.id;
987
966
  }
@@ -1026,7 +1005,7 @@ function queueSetup(sessionId, opts = {}) {
1026
1005
  * @param {string} sessionId
1027
1006
  * @param {object} opts
1028
1007
  * @param {boolean} opts.success
1029
- * @param {string} [opts.wiPath] - required when success=true (single-project) OR for the PRIMARY target on multi-project DRAFT queueing
1008
+ * @param {string} [opts.scope] - required when success=true
1030
1009
  * @param {string} [opts.project] - which project the completing WI was for (multi-project routing key)
1031
1010
  * @param {string} [opts.failureClass]
1032
1011
  * @param {string} [opts.reason]
@@ -1043,13 +1022,13 @@ function handleSetupComplete(sessionId, opts = {}) {
1043
1022
  // ── Single-project fast path: original behavior, untouched semantics ──
1044
1023
  if (!isMulti) {
1045
1024
  if (opts.success) {
1046
- if (!_isNonEmptyString(opts.wiPath)) {
1047
- throw new Error('qa-sessions: handleSetupComplete success requires wiPath');
1025
+ if (!_isNonEmptyString(opts.scope)) {
1026
+ throw new Error('qa-sessions: handleSetupComplete success requires scope');
1048
1027
  }
1049
1028
  markDrafting(sessionId, { managedSpawnHealth: 'healthy' });
1050
1029
  const updated = getSession(sessionId);
1051
1030
  const wi = buildDraftWorkItem(updated, { project: opts.project || updated.spec.project || null });
1052
- _queueWorkItem(wi, opts.wiPath);
1031
+ _queueWorkItem(wi, opts.scope);
1053
1032
  setSessionWorkItem(sessionId, SESSION_PHASE.DRAFT, wi.id);
1054
1033
  return wi.id;
1055
1034
  }
@@ -1112,17 +1091,16 @@ function handleSetupComplete(sessionId, opts = {}) {
1112
1091
  return null;
1113
1092
  }
1114
1093
 
1115
- // All success → advance to drafting and queue DRAFT on the PRIMARY's
1116
- // wiPath (captured at queueSetup time).
1094
+ // All success → queue DRAFT on the primary scope captured by queueSetup.
1117
1095
  const fresh = getSession(sessionId);
1118
- const draftWiPath = fresh && fresh.primaryWiPath;
1119
- if (!_isNonEmptyString(draftWiPath)) {
1120
- throw new Error('qa-sessions: handleSetupComplete multi-project missing session.primaryWiPath');
1096
+ const draftScope = fresh && fresh.primaryScope;
1097
+ if (!_isNonEmptyString(draftScope)) {
1098
+ throw new Error('qa-sessions: handleSetupComplete multi-project missing session.primaryScope');
1121
1099
  }
1122
1100
  markDrafting(sessionId, { managedSpawnHealth: 'healthy' });
1123
1101
  const updated = getSession(sessionId);
1124
1102
  const wi = buildDraftWorkItem(updated, { project: updated.primaryProject || updated.spec.project || null });
1125
- _queueWorkItem(wi, draftWiPath);
1103
+ _queueWorkItem(wi, draftScope);
1126
1104
  setSessionWorkItem(sessionId, SESSION_PHASE.DRAFT, wi.id);
1127
1105
  return wi.id;
1128
1106
  }
@@ -1136,7 +1114,7 @@ function handleSetupComplete(sessionId, opts = {}) {
1136
1114
  * @param {object} opts
1137
1115
  * @param {boolean} opts.success
1138
1116
  * @param {string} [opts.testFile] - relative path under qa-tests/<id>/, captured for EXECUTE
1139
- * @param {string} [opts.wiPath] - required when success=true and mode=auto
1117
+ * @param {string} [opts.scope] - required when success=true and mode=auto
1140
1118
  * @param {string} [opts.project]
1141
1119
  * @param {string} [opts.qaRunId] - required when success=true and mode=auto (caller creates the qa-runs record)
1142
1120
  * @param {string} [opts.reason]
@@ -1158,8 +1136,8 @@ function handleDraftComplete(sessionId, opts = {}) {
1158
1136
  }
1159
1137
  const testFilePatch = opts.testFile ? { testFile: opts.testFile } : {};
1160
1138
  if (session.spec.mode === 'auto') {
1161
- if (!_isNonEmptyString(opts.wiPath)) {
1162
- throw new Error('qa-sessions: handleDraftComplete (auto) requires wiPath');
1139
+ if (!_isNonEmptyString(opts.scope)) {
1140
+ throw new Error('qa-sessions: handleDraftComplete (auto) requires scope');
1163
1141
  }
1164
1142
  if (!_isNonEmptyString(opts.qaRunId)) {
1165
1143
  throw new Error('qa-sessions: handleDraftComplete (auto) requires qaRunId');
@@ -1170,7 +1148,7 @@ function handleDraftComplete(sessionId, opts = {}) {
1170
1148
  qaRunId: opts.qaRunId,
1171
1149
  project: opts.project || updated.spec.project || null,
1172
1150
  });
1173
- _queueWorkItem(wi, opts.wiPath);
1151
+ _queueWorkItem(wi, opts.scope);
1174
1152
  setSessionWorkItem(sessionId, SESSION_PHASE.EXECUTE, wi.id);
1175
1153
  return { nextState: QA_SESSION_STATE.EXECUTING, queuedExecuteWi: wi.id };
1176
1154
  }
@@ -1227,8 +1205,8 @@ function handleExecuteComplete(sessionId, opts = {}) {
1227
1205
  * POST /api/qa/sessions/<id>/approve — awaiting-approval → executing, queues
1228
1206
  * the EXECUTE WI. Caller creates the qa-runs record and passes its id.
1229
1207
  */
1230
- function approveDraft(sessionId, { wiPath, qaRunId, project } = {}) {
1231
- if (!_isNonEmptyString(wiPath)) throw new Error('qa-sessions: approveDraft requires wiPath');
1208
+ function approveDraft(sessionId, { scope, qaRunId, project } = {}) {
1209
+ if (!_isNonEmptyString(scope)) throw new Error('qa-sessions: approveDraft requires scope');
1232
1210
  if (!_isNonEmptyString(qaRunId)) throw new Error('qa-sessions: approveDraft requires qaRunId');
1233
1211
  const session = getSession(sessionId);
1234
1212
  if (!session) throw new Error('qa-sessions: session not found: ' + sessionId);
@@ -1241,7 +1219,7 @@ function approveDraft(sessionId, { wiPath, qaRunId, project } = {}) {
1241
1219
  qaRunId,
1242
1220
  project: project || updated.spec.project || null,
1243
1221
  });
1244
- _queueWorkItem(wi, wiPath);
1222
+ _queueWorkItem(wi, scope);
1245
1223
  setSessionWorkItem(sessionId, SESSION_PHASE.EXECUTE, wi.id);
1246
1224
  return wi.id;
1247
1225
  }
@@ -1250,8 +1228,8 @@ function approveDraft(sessionId, { wiPath, qaRunId, project } = {}) {
1250
1228
  * POST /api/qa/sessions/<id>/edit — awaiting-approval → drafting, re-queue the
1251
1229
  * DRAFT WI with the user's natural-language feedback threaded into the prompt.
1252
1230
  */
1253
- function editDraft(sessionId, { wiPath, feedback, project } = {}) {
1254
- if (!_isNonEmptyString(wiPath)) throw new Error('qa-sessions: editDraft requires wiPath');
1231
+ function editDraft(sessionId, { scope, feedback, project } = {}) {
1232
+ if (!_isNonEmptyString(scope)) throw new Error('qa-sessions: editDraft requires scope');
1255
1233
  if (!_isNonEmptyString(feedback)) throw new Error('qa-sessions: editDraft requires feedback');
1256
1234
  if (feedback.length > LIMITS.feedbackMax) {
1257
1235
  throw new Error(`qa-sessions: editDraft feedback exceeds ${LIMITS.feedbackMax} chars`);
@@ -1267,7 +1245,7 @@ function editDraft(sessionId, { wiPath, feedback, project } = {}) {
1267
1245
  project: project || updated.spec.project || null,
1268
1246
  feedback,
1269
1247
  });
1270
- _queueWorkItem(wi, wiPath);
1248
+ _queueWorkItem(wi, scope);
1271
1249
  setSessionWorkItem(sessionId, SESSION_PHASE.DRAFT, wi.id);
1272
1250
  return wi.id;
1273
1251
  }
@@ -1365,7 +1343,6 @@ module.exports = {
1365
1343
  LIMITS,
1366
1344
  QA_SESSIONS_MAX_RECORDS,
1367
1345
  // Paths
1368
- qaSessionsPath,
1369
1346
  qaTestsDir,
1370
1347
  qaTestsDirForSession,
1371
1348
  // Validation