@yemi33/minions 0.1.2285 → 0.1.2287

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/dashboard.js CHANGED
@@ -37,6 +37,7 @@ const _dashboardVersion = {
37
37
  };
38
38
  const shared = require('./engine/shared');
39
39
  const queries = require('./engine/queries');
40
+ const prdStore = require('./engine/prd-store');
40
41
  const ado = require('./engine/ado');
41
42
  const gh = require('./engine/github');
42
43
  const ghToken = require('./engine/gh-token');
@@ -429,6 +430,21 @@ function mergeSettingsConfigUpdate(current, candidate, body, patch = {}) {
429
430
  // `worktreeMode`. Drop any stale legacy key on every settings save so a
430
431
  // migrated project never carries both fields.
431
432
  delete currentProject.worktreeMode;
433
+ // M006 — mirror liveValidation: check both candidateProject (modified
434
+ // in-memory config) and the original body update. When handleSettingsUpdate
435
+ // deletes the field (null or empty type sent), candidateProject won't have
436
+ // it but the body update will — mirror the deletion to disk.
437
+ if (Object.prototype.hasOwnProperty.call(candidateProject, 'liveValidation')) {
438
+ if (candidateProject.liveValidation && typeof candidateProject.liveValidation === 'object' && candidateProject.liveValidation.type) {
439
+ currentProject.liveValidation = candidateProject.liveValidation;
440
+ } else {
441
+ delete currentProject.liveValidation;
442
+ }
443
+ } else if (Object.prototype.hasOwnProperty.call(update, 'liveValidation')) {
444
+ // Body sent liveValidation key but handleSettingsUpdate deleted it from
445
+ // the in-memory config (null / empty type) — propagate to disk.
446
+ delete currentProject.liveValidation;
447
+ }
432
448
  }
433
449
  }
434
450
  shared.pruneDefaultClaudeConfig(current);
@@ -1299,7 +1315,7 @@ function collectArchivedWorkItems(minionsDir = MINIONS_DIR, projects = PROJECTS)
1299
1315
  }
1300
1316
  return archived;
1301
1317
  }
1302
- function linkPullRequestForTracking({ url, title, project: projectName, contextOnly, autoObserve, context, workItemId }, config = CONFIG, options = {}) {
1318
+ function linkPullRequestForTracking({ url, title, project: projectName, contextOnly, context, workItemId }, config = CONFIG, options = {}) {
1303
1319
  if (!url) {
1304
1320
  const err = new Error('url required');
1305
1321
  err.statusCode = 400;
@@ -1315,12 +1331,7 @@ function linkPullRequestForTracking({ url, title, project: projectName, contextO
1315
1331
  const linkedWorkItemId = getWorkItemIdFromPrLinkContext(context, workItemId);
1316
1332
  const contextText = typeof context === 'string' ? context : (context == null ? '' : JSON.stringify(context));
1317
1333
  const metadata = normalizePrMetadata(options.metadata);
1318
- // W-mq5s5ttx000j7ab8-c: canonical `contextOnly` field with `autoObserve`
1319
- // deprecated alias. Explicit canonical wins; otherwise fall back to the
1320
- // alias (autoObserve:true → contextOnly:false); otherwise default false.
1321
- const resolvedContextOnly = typeof contextOnly === 'boolean'
1322
- ? contextOnly
1323
- : (autoObserve === undefined ? false : !autoObserve);
1334
+ const resolvedContextOnly = typeof contextOnly === 'boolean' ? contextOnly : false;
1324
1335
  const result = shared.upsertPullRequestRecord(prPath, {
1325
1336
  id: prId,
1326
1337
  prNumber: parseInt(prNum, 10) || null,
@@ -6974,7 +6985,11 @@ const server = http.createServer(async (req, res) => {
6974
6985
  if (!fs.existsSync(PRD_DIR)) fs.mkdirSync(PRD_DIR, { recursive: true });
6975
6986
 
6976
6987
  const planFile = 'manual-' + shared.uid() + '.json';
6977
- safeWrite(path.join(PRD_DIR, planFile), manualPrd.plan);
6988
+ const manualPrdPath = path.join(PRD_DIR, planFile);
6989
+ safeWrite(manualPrdPath, manualPrd.plan);
6990
+ // Phase 10 step 2 — this create uses safeWrite (not mutateJsonFileLocked),
6991
+ // so mirror to SQL explicitly. Best-effort.
6992
+ prdStore.mirrorPrdToSql(manualPrdPath, manualPrd.plan);
6978
6993
  invalidatePlansCache();
6979
6994
  return jsonReply(res, 200, { ok: true, id: manualPrd.id, file: planFile });
6980
6995
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
@@ -8022,77 +8037,103 @@ const server = http.createServer(async (req, res) => {
8022
8037
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
8023
8038
  }
8024
8039
 
8040
+ // Shared by pause + reject (and any future PRD-stop handler): stop a PRD's
8041
+ // materialized work. Kills any active dispatch for the PRD's items, transitions
8042
+ // every non-completed WI sourced from the PRD to `targetStatus`, AND cancels the
8043
+ // still-running plan-to-prd regeneration WI for the PRD's source plan so it can't
8044
+ // silently rebuild the PRD we just paused/rejected. Returns the count of WIs
8045
+ // transitioned. Lockless reads → cleanDispatchEntries (atomic kill + remove) →
8046
+ // mutateWorkItems, mirroring the original pause flow. (RC3 — reject had no
8047
+ // cleanup; pause never stopped regeneration.)
8048
+ function stopPlanMaterializedWork(prdFile, prdSourcePlan, opts) {
8049
+ const targetStatus = opts.targetStatus;
8050
+ const wiPaths = [path.join(MINIONS_DIR, 'work-items.json')];
8051
+ for (const proj of PROJECTS) wiPaths.push(shared.projectWorkItemsPath(proj));
8052
+
8053
+ // Step 1: find dispatched item ids (read-only, no lock).
8054
+ const dispatchedItemIds = new Set();
8055
+ for (const wiPath of wiPaths) {
8056
+ try {
8057
+ for (const w of safeJsonArr(wiPath)) {
8058
+ if (w.sourcePlan !== prdFile) continue;
8059
+ if (w.completedAt || DONE_STATUSES.has(w.status)) continue;
8060
+ if (w.status === WI_STATUS.DISPATCHED && w.id) dispatchedItemIds.add(w.id);
8061
+ }
8062
+ } catch { /* file may not exist */ }
8063
+ }
8064
+
8065
+ // Step 2: kill active dispatches via the canonical primitive (resolves PIDs from
8066
+ // the pid sidecar, kills outside the dispatch lock, removes via mutateDispatch).
8067
+ if (dispatchedItemIds.size > 0) {
8068
+ cleanDispatchEntries((d) => {
8069
+ const itemId = d.meta?.item?.id;
8070
+ if (itemId && dispatchedItemIds.has(itemId)) return true;
8071
+ if (d.meta?.dispatchKey && [...dispatchedItemIds].some(id => d.meta.dispatchKey.includes(id))) return true;
8072
+ return false;
8073
+ });
8074
+ }
8075
+
8076
+ // Step 3: transition WIs per path (each lock held briefly, no nesting).
8077
+ let affected = 0;
8078
+ for (const wiPath of wiPaths) {
8079
+ try {
8080
+ mutateWorkItems(wiPath, items => {
8081
+ for (const w of items) {
8082
+ if (w.sourcePlan !== prdFile) continue;
8083
+ if (w.completedAt || DONE_STATUSES.has(w.status)) continue;
8084
+ if (w.status !== targetStatus) affected++;
8085
+ w.status = targetStatus;
8086
+ if (opts.stampField) w[opts.stampField] = opts.stampValue;
8087
+ delete w._resumedAt;
8088
+ delete w.dispatched_at;
8089
+ delete w.dispatched_to;
8090
+ delete w.failReason;
8091
+ delete w.failedAt;
8092
+ }
8093
+ });
8094
+ } catch (e) { console.error('stopPlanMaterializedWork work items:', e.message); }
8095
+ }
8096
+
8097
+ // Step 4: cancel the still-running plan-to-prd regeneration WI for this source
8098
+ // plan — otherwise a pending/dispatched plan-to-prd run rebuilds the PRD we just
8099
+ // stopped. (delete handles the DONE plan-to-prd WI separately to revert to draft.)
8100
+ if (prdSourcePlan) {
8101
+ try {
8102
+ const centralPath = path.join(MINIONS_DIR, 'work-items.json');
8103
+ mutateWorkItems(centralPath, items => {
8104
+ for (const w of items) {
8105
+ if (w.type === WORK_TYPE.PLAN_TO_PRD && w.planFile === prdSourcePlan &&
8106
+ !DONE_STATUSES.has(w.status) && w.status !== WI_STATUS.CANCELLED) {
8107
+ w.status = WI_STATUS.CANCELLED;
8108
+ w._cancelledBy = opts.stampValue || 'prd-stopped';
8109
+ }
8110
+ }
8111
+ });
8112
+ } catch (e) { console.error('stopPlanMaterializedWork plan-to-prd:', e.message); }
8113
+ }
8114
+ return affected;
8115
+ }
8116
+
8025
8117
  async function handlePlansPause(req, res) {
8026
8118
  try {
8027
8119
  const body = await readBody(req);
8028
8120
  if (!body.file) return jsonReply(res, 400, { error: 'file required' });
8029
8121
  if (!body.file.endsWith('.json')) return jsonReply(res, 400, { error: 'expected a PRD JSON filename (got `' + body.file + '`). Pass prd/<plan>.json, not the source plans/<plan>.md.' });
8030
8122
  const planPath = resolvePlanPath(body.file);
8031
- mutateJsonFileLocked(planPath, (plan) => {
8123
+ let prdSourcePlan = null;
8124
+ const updated = mutateJsonFileLocked(planPath, (plan) => {
8032
8125
  if (!plan || Array.isArray(plan) || typeof plan !== 'object') plan = {};
8033
8126
  plan.status = 'paused';
8034
8127
  plan.pausedAt = new Date().toISOString();
8035
8128
  return plan;
8036
8129
  }, { defaultValue: {} });
8130
+ prdSourcePlan = updated?.source_plan || null;
8037
8131
 
8038
- // Propagate pause to materialized work items across all projects:
8039
- // kill any active agent process and reset non-completed items to paused.
8040
- // Pattern: lockless reads → cleanDispatchEntries (atomic kill + remove) → mutateWorkItems.
8041
- const wiPaths = [path.join(MINIONS_DIR, 'work-items.json')];
8042
- for (const proj of PROJECTS) {
8043
- wiPaths.push(shared.projectWorkItemsPath(proj));
8044
- }
8045
- const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
8046
-
8047
- // Step 1: Read work items (read-only, no lock) to find plan items that are dispatched.
8048
- const dispatchedItemIds = new Set();
8049
- for (const wiPath of wiPaths) {
8050
- try {
8051
- const items = safeJsonArr(wiPath);
8052
- for (const w of items) {
8053
- if (w.sourcePlan !== body.file) continue;
8054
- if (w.completedAt || DONE_STATUSES.has(w.status)) continue;
8055
- if (w.status === WI_STATUS.DISPATCHED && w.id) dispatchedItemIds.add(w.id);
8056
- }
8057
- } catch { /* file may not exist */ }
8058
- }
8059
-
8060
- // Step 2: Route PID resolution + kill + dispatch removal through the canonical primitive.
8061
- // cleanDispatchEntries resolves PIDs from engine/tmp/dispatch-<id>-*/pid-<id>.pid
8062
- // (see shared.findDispatchPidFile), kills outside the dispatch lock, and removes the
8063
- // entries via mutateDispatch (SQL + JSON mirror in one atomic write).
8064
- // The defunct per-agent status sidecar reads/writes that used to live here are gone
8065
- // (engine/queries.js documents that file no longer exists).
8066
- if (dispatchedItemIds.size > 0) {
8067
- const matchFn = (d) => {
8068
- const itemId = d.meta?.item?.id;
8069
- if (itemId && dispatchedItemIds.has(itemId)) return true;
8070
- if (d.meta?.dispatchKey && [...dispatchedItemIds].some(id => d.meta.dispatchKey.includes(id))) return true;
8071
- return false;
8072
- };
8073
- cleanDispatchEntries(matchFn);
8074
- }
8075
-
8076
- // Step 3: Mutate work-items.json per path — pause items (each lock held briefly, no nesting).
8077
- let reset = 0;
8078
- for (const wiPath of wiPaths) {
8079
- try {
8080
- mutateWorkItems(wiPath, items => {
8081
- for (const w of items) {
8082
- if (w.sourcePlan !== body.file) continue;
8083
- if (w.completedAt || DONE_STATUSES.has(w.status)) continue;
8084
- if (w.status !== WI_STATUS.PAUSED) reset++;
8085
- w.status = WI_STATUS.PAUSED;
8086
- w._pausedBy = 'prd-pause';
8087
- delete w._resumedAt;
8088
- delete w.dispatched_at;
8089
- delete w.dispatched_to;
8090
- delete w.failReason;
8091
- delete w.failedAt;
8092
- }
8093
- });
8094
- } catch (e) { console.error('reset work items:', e.message); }
8095
- }
8132
+ // Propagate pause to materialized work items across all projects + cancel the
8133
+ // plan-to-prd regeneration WI so a paused PRD can't be silently rebuilt.
8134
+ const reset = stopPlanMaterializedWork(body.file, prdSourcePlan, {
8135
+ targetStatus: WI_STATUS.PAUSED, stampField: '_pausedBy', stampValue: 'prd-pause',
8136
+ });
8096
8137
 
8097
8138
  invalidateStatusCache();
8098
8139
  invalidatePlansCache();
@@ -8149,8 +8190,17 @@ const server = http.createServer(async (req, res) => {
8149
8190
  return data;
8150
8191
  }, { defaultValue: {} });
8151
8192
 
8193
+ // RC3: reject used to flip only the PRD status — its materialized work items
8194
+ // kept dispatching and any active agent kept running, and a pending plan-to-prd
8195
+ // run could rebuild the PRD. Reject is terminal, so cancel the materialized WIs,
8196
+ // kill active dispatches, and cancel the plan-to-prd regeneration WI.
8197
+ const cancelled = stopPlanMaterializedWork(body.file, plan?.source_plan || null, {
8198
+ targetStatus: WI_STATUS.CANCELLED, stampField: '_cancelledBy', stampValue: 'prd-rejected',
8199
+ });
8200
+
8201
+ invalidateStatusCache();
8152
8202
  invalidatePlansCache();
8153
- return jsonReply(res, 200, { ok: true, status: 'rejected' });
8203
+ return jsonReply(res, 200, { ok: true, status: 'rejected', cancelledWorkItems: cancelled });
8154
8204
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
8155
8205
  }
8156
8206
 
@@ -8268,6 +8318,10 @@ const server = http.createServer(async (req, res) => {
8268
8318
  cleanupPlanWorktrees(body.file, planObj || {}, PROJECTS, getConfig());
8269
8319
  } catch (e) { console.error('plan worktree cleanup:', e.message); }
8270
8320
  safeUnlink(planPath);
8321
+ // Phase 10 step 2 — a deleted PRD must also leave SQL, or the read-flip
8322
+ // (step 3) would resurrect it from a stale mirror row. Best-effort; no-op
8323
+ // for non-PRD paths (parsePrdPath returns null for plan .md deletes).
8324
+ if (body.file.endsWith('.json')) prdStore.removePrdFromSql(planPath);
8271
8325
  // Neutralize the `.backup` sidecar so `safeJson` auto-restore can't
8272
8326
  // RESURRECT the PRD we just deleted (the live file is gone, but a stray
8273
8327
  // `prd/<plan>.json.backup` would be auto-restored on the next safeJson read
@@ -9823,6 +9877,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
9823
9877
  _emitTimingLog(_lifecycle, _tSessionReady, Date.now(), 'cancelled-pre-stream');
9824
9878
  return resolveResult({ text: accumulated, sessionId: sessionHandle.sessionId, code: 0, usage: {}, raw: accumulated, stderr: '' });
9825
9879
  }
9880
+ let poolSegmentId = 0;
9881
+ let segmentStart = 0;
9826
9882
  await sessionHandle.stream(prompt, {
9827
9883
  systemPromptText: systemPrompt,
9828
9884
  onChunk: (delta) => {
@@ -9831,7 +9887,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
9831
9887
  accumulated += delta;
9832
9888
  _touchCcLiveStream(liveState);
9833
9889
  liveState.text = accumulated;
9834
- if (liveState.writer) liveState.writer({ type: 'chunk', text: accumulated });
9890
+ const segmentText = accumulated.slice(segmentStart);
9891
+ if (liveState.writer) liveState.writer({ type: 'chunk', text: segmentText, segmentId: poolSegmentId });
9835
9892
  },
9836
9893
  onToolUse: (name, input, toolCallId) => {
9837
9894
  if (_tFirstTool == null) _tFirstTool = Date.now();
@@ -9842,6 +9899,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
9842
9899
  if (Array.isArray(toolUses)) toolUses.push(entry);
9843
9900
  if (Array.isArray(liveState.tools)) liveState.tools.push(entry);
9844
9901
  if (liveState.writer) liveState.writer({ type: 'tool', name, input: _lightToolInput(safeInput), id: toolCallId || null });
9902
+ // Advance segment tracking so the next onChunk creates a fresh segment
9903
+ poolSegmentId++;
9904
+ segmentStart = accumulated.length;
9845
9905
  },
9846
9906
  onToolUpdate: (toolCallId, status) => {
9847
9907
  _touchCcLiveStream(liveState);
@@ -10830,6 +10890,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
10830
10890
  // the per-project dropdown. resolveCheckoutMode honors the legacy
10831
10891
  // worktreeMode field; 'worktree' (default) or 'live'.
10832
10892
  checkoutMode: shared.resolveCheckoutMode(p),
10893
+ // M006 — surface liveValidation so the Settings UI can pre-fill
10894
+ // the type input and autoDispatch toggle. Null when not configured.
10895
+ liveValidation: (p.liveValidation && typeof p.liveValidation === 'object' && p.liveValidation.type)
10896
+ ? { type: p.liveValidation.type, autoDispatch: !!p.liveValidation.autoDispatch }
10897
+ : null,
10833
10898
  workSources: {
10834
10899
  pullRequests: { enabled: p.workSources?.pullRequests?.enabled !== false, cooldownMinutes: p.workSources?.pullRequests?.cooldownMinutes ?? 30 },
10835
10900
  workItems: { enabled: p.workSources?.workItems?.enabled !== false, cooldownMinutes: p.workSources?.workItems?.cooldownMinutes ?? 0 }
@@ -11254,10 +11319,19 @@ What would you like to discuss or change? When you're happy, say "approve" and I
11254
11319
  // Drop the legacy field so a migrated project never carries both.
11255
11320
  delete proj.worktreeMode;
11256
11321
  }
11322
+ // M006 — per-project liveValidation: { type, autoDispatch }.
11323
+ // Only meaningful when checkoutMode is 'live'. Null / missing type
11324
+ // clears the field; explicit object with a non-empty type persists it.
11325
+ if (Object.prototype.hasOwnProperty.call(update, 'liveValidation')) {
11326
+ const lv = update.liveValidation;
11327
+ if (!lv || typeof lv !== 'object' || !lv.type || typeof lv.type !== 'string' || !lv.type.trim()) {
11328
+ delete proj.liveValidation;
11329
+ } else {
11330
+ proj.liveValidation = { type: lv.type.trim(), autoDispatch: !!lv.autoDispatch };
11331
+ }
11332
+ }
11257
11333
  }
11258
11334
  }
11259
-
11260
- shared.pruneDefaultClaudeConfig(config);
11261
11335
  mutateDashboardConfig(current => mergeSettingsConfigUpdate(current, config, body, _configPatch));
11262
11336
  // Refresh in-memory CONFIG so subsequent reads see the update
11263
11337
  reloadConfig();
@@ -13322,7 +13396,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
13322
13396
  // /api/prd/regenerate removed — use /api/plans/approve which does diff-aware update
13323
13397
 
13324
13398
  // Agents
13325
- { method: 'POST', path: '/api/pull-requests/link', desc: 'Manually link an external PR for tracking', params: 'url, title?, project?, contextOnly?, autoObserve? (deprecated alias for !contextOnly), context?, workItemId?', handler: async (req, res) => {
13399
+ { method: 'POST', path: '/api/pull-requests/link', desc: 'Manually link an external PR for tracking', params: 'url, title?, project?, contextOnly?, context?, workItemId?', handler: async (req, res) => {
13326
13400
  const body = await readBody(req);
13327
13401
  const { url } = body;
13328
13402
  if (!url) return jsonReply(res, 400, { error: 'url required' });
@@ -3,26 +3,10 @@
3
3
  "id": "agent-config-skills-field",
4
4
  "description": "Legacy per-agent descriptive-metadata array `agents.<id>.skills` in config.json, renamed to `agents.<id>.expertise` to remove the name collision with executable runtime/harness skills (SKILL.md). The field is metadata only (capability tags like `architecture`, `bug-fixes`); nothing in the dispatch path reads it for behavior. A read-compat shim honors the old key so operator configs still carrying `skills` (and no `expertise`) keep working.",
5
5
  "code": [
6
- {
7
- "file": "engine/playbook.js",
8
- "lines": "950",
9
- "note": "buildSystemPrompt reads `agent.expertise ?? agent.skills ?? []` for the `Expertise:` identity line"
10
- },
11
- {
12
- "file": "engine/lifecycle.js",
13
- "lines": "4620-4621",
14
- "note": "pickReReviewAgentHints reads `agent.expertise` with an `agent.skills` array fallback"
15
- },
16
- {
17
- "file": "engine/queries.js",
18
- "lines": "731",
19
- "note": "getAgents normalizes `expertise: a.expertise ?? a.skills ?? []` so the dashboard/settings UI always receives `expertise`"
20
- },
21
- {
22
- "file": "dashboard.js",
23
- "lines": "10708-10716",
24
- "note": "settings POST accepts a legacy `updates.skills` key, persists as `config.agents[id].expertise`, and deletes the old `skills` key"
25
- }
6
+ { "file": "engine/playbook.js", "lines": "950", "note": "buildSystemPrompt reads `agent.expertise ?? agent.skills ?? []` for the `Expertise:` identity line" },
7
+ { "file": "engine/lifecycle.js", "lines": "4620-4621", "note": "pickReReviewAgentHints reads `agent.expertise` with an `agent.skills` array fallback" },
8
+ { "file": "engine/queries.js", "lines": "731", "note": "getAgents normalizes `expertise: a.expertise ?? a.skills ?? []` so the dashboard/settings UI always receives `expertise`" },
9
+ { "file": "dashboard.js", "lines": "10708-10716", "note": "settings POST accepts a legacy `updates.skills` key, persists as `config.agents[id].expertise`, and deletes the old `skills` key" }
26
10
  ],
27
11
  "removalGate": "Telemetry / a config sweep across all known engines must show no persisted `config.agents.<id>.skills` key (only `expertise`) for >=30 consecutive days, confirming every operator config has been re-saved through the dashboard (which drops the legacy key) or hand-migrated.",
28
12
  "targetRemovalDate": "2026-09-17",
@@ -47,10 +31,7 @@
47
31
  {
48
32
  "id": "legacy-done-aliases",
49
33
  "location": "engine/cleanup.js:1165-1166",
50
- "constants": [
51
- "LEGACY_DONE_ALIASES",
52
- "LEGACY_NEEDS_REVIEW_STATUS"
53
- ],
34
+ "constants": ["LEGACY_DONE_ALIASES", "LEGACY_NEEDS_REVIEW_STATUS"],
54
35
  "reason": "Read-side tolerance: cleanup sweep auto-migrates four obsolete work-item / PRD status strings ('in-pr', 'implemented', 'complete', 'needs-human-review') to the canonical 'done' / 'failed' values. The aliases are no longer written anywhere in the engine; the constants exist only to repair stale on-disk values from old engine versions.",
55
36
  "targetRemovalDate": null,
56
37
  "notes": "Keep indefinitely until telemetry / a sweep log shows zero migrations performed for 30 consecutive days across all known projects (work-items.json + prd/*.json). At that point the constants and both _migrateLegacyItem branches in engine/cleanup.js (definitions at :1165-1166; usage at :1168-1183 for work items and :1269-1272 for PRD missing_features) can be deleted. Total cost on disk today: 4 strings."
@@ -59,21 +40,9 @@
59
40
  "id": "config-claude-binary-override",
60
41
  "description": "Legacy `config.claude.binary` runtime-resolution override. Older `minions init` versions persisted a `config.claude.binary` field that pointed the Claude runtime at a specific binary path. The runtime adapter still honors this override on every Claude spawn; the engine emits a `deprecated-config-claude` warning at config-load time but does NOT delete the override, so the override branch in claude.js is load-bearing for any install that still carries a non-default value.",
61
42
  "code": [
62
- {
63
- "file": "engine/runtimes/claude.js",
64
- "lines": "82-86",
65
- "note": "resolveBinary() respects config.claude.binary on every Claude spawn (probes npm package dir or direct binary path)"
66
- },
67
- {
68
- "file": "engine/shared.js",
69
- "lines": "2482-2496",
70
- "note": "warnings.push({ id: 'deprecated-config-claude' }) — surface-only; never deletes the override"
71
- },
72
- {
73
- "file": "engine/shared.js",
74
- "lines": "3120-3124",
75
- "note": "DEFAULT_CLAUDE.binary baseline that the warning + prune logic compares against"
76
- }
43
+ { "file": "engine/runtimes/claude.js", "lines": "82-86", "note": "resolveBinary() respects config.claude.binary on every Claude spawn (probes npm package dir or direct binary path)" },
44
+ { "file": "engine/shared.js", "lines": "2482-2496", "note": "warnings.push({ id: 'deprecated-config-claude' }) — surface-only; never deletes the override" },
45
+ { "file": "engine/shared.js", "lines": "3120-3124", "note": "DEFAULT_CLAUDE.binary baseline that the warning + prune logic compares against" }
77
46
  ],
78
47
  "removalGate": "Telemetry: the `deprecated-config-claude` warning emitted at engine/shared.js:2492-2495 must report zero hits across all known engines for >=30 consecutive days, AND a sweep of every persisted config.json must show no `config.claude.binary` value that diverges from DEFAULT_CLAUDE.binary. Only then is the override branch in resolveBinary() (engine/runtimes/claude.js:82-86) removable, along with the `_deprecatedConfigClaudeFields` membership for `binary` and the warning emitter at engine/shared.js:2482-2496.",
79
48
  "targetRemovalDate": null,
@@ -83,41 +52,13 @@
83
52
  "id": "legacy-cc-model-migration",
84
53
  "description": "applyLegacyCcModelMigration: in-memory shim that promotes legacy `engine.ccModel` to `engine.defaultModel` when defaultModel is unset, so single-model installs keep working after the runtime fleet refactor (P-3b8e5f1d). No on-disk rewrite — the persisted config.json continues to carry the legacy `ccModel` field. Called unconditionally on every engine boot from cli.start().",
85
54
  "code": [
86
- {
87
- "file": "engine/shared.js",
88
- "lines": "2407",
89
- "note": "applyLegacyCcModelMigration definition (function signature + once-per-process flag via _resetLegacyCcModelMigrationFlag)"
90
- },
91
- {
92
- "file": "engine/cli.js",
93
- "lines": "477",
94
- "note": "Boot call site inside start(); wrapped in try/catch so a migration failure cannot block startup"
95
- },
96
- {
97
- "file": "CLAUDE.md",
98
- "lines": "316",
99
- "note": "Architectural documentation calling out the in-memory promotion contract"
100
- },
101
- {
102
- "file": "docs/slim-ux/concepts.md",
103
- "lines": "671",
104
- "note": "Surface-level concepts doc cross-reference"
105
- },
106
- {
107
- "file": "test/unit.test.js",
108
- "lines": "19801",
109
- "note": "Source-inspection test pinning the CLAUDE.md description against the function name"
110
- },
111
- {
112
- "file": "test/unit/runtime-fleet-helpers.test.js",
113
- "lines": "209-254",
114
- "note": "Behavioural unit tests (promotion, no-op when defaultModel set, no-op when ccModel unset, empty-string handling, once-only logging, null-safety)"
115
- },
116
- {
117
- "file": "test/unit/runtime-fleet-helpers.test.js",
118
- "lines": "500-505",
119
- "note": "Source-inspection test pinning the cli.js boot call site"
120
- }
55
+ { "file": "engine/shared.js", "lines": "2407", "note": "applyLegacyCcModelMigration definition (function signature + once-per-process flag via _resetLegacyCcModelMigrationFlag)" },
56
+ { "file": "engine/cli.js", "lines": "477", "note": "Boot call site inside start(); wrapped in try/catch so a migration failure cannot block startup" },
57
+ { "file": "CLAUDE.md", "lines": "316", "note": "Architectural documentation calling out the in-memory promotion contract" },
58
+ { "file": "docs/slim-ux/concepts.md", "lines": "671", "note": "Surface-level concepts doc cross-reference" },
59
+ { "file": "test/unit.test.js", "lines": "19801", "note": "Source-inspection test pinning the CLAUDE.md description against the function name" },
60
+ { "file": "test/unit/runtime-fleet-helpers.test.js", "lines": "209-254", "note": "Behavioural unit tests (promotion, no-op when defaultModel set, no-op when ccModel unset, empty-string handling, once-only logging, null-safety)" },
61
+ { "file": "test/unit/runtime-fleet-helpers.test.js", "lines": "500-505", "note": "Source-inspection test pinning the cli.js boot call site" }
121
62
  ],
122
63
  "removalGate": "Telemetry: the once-per-boot deprecation log line emitted by applyLegacyCcModelMigration (via the injected logger at engine/shared.js:2407) must show zero promotion events across all known engines for >=30 consecutive days, AND a sweep of every persisted config.json must confirm no `engine.ccModel` field remains. Once both conditions hold, removal deletes the function + _resetLegacyCcModelMigrationFlag export at engine/shared.js:4977, the boot call at engine/cli.js:477, the CLAUDE.md:316 paragraph and docs/slim-ux/concepts.md:671 reference, and the tests at runtime-fleet-helpers.test.js:209-254 + :500-505 + unit.test.js:19801.",
123
64
  "targetRemovalDate": null,
@@ -127,39 +68,14 @@
127
68
  "id": "sql-state-json-mirrors",
128
69
  "description": "Phase X.5 follow-up to the SQL state migration (commits 62bd6a2c..1111cf54, phases 0–7). Every engine state file that previously used mutateJsonFileLocked now routes through a SQL store, but each store still writes a JSON dual-write mirror after every mutation because a handful of direct-readers (a few unit tests + a couple of inline safeJson calls) have not been migrated to the SQL read path. Once those readers are confirmed routed through the SQL store (or rewritten to use the store's read helper), the mirror writers can be deleted and the JSON files retired.",
129
70
  "code": [
130
- {
131
- "file": "engine/dispatch-store.js",
132
- "note": "_mirrorJsonFromSql + _readDispatchJsonFallback — used when SQL is empty AND JSON has content (test seeding + first-time hydrate)"
133
- },
134
- {
135
- "file": "engine/work-items-store.js",
136
- "note": "_mirrorJsonFromSql + _readJsonArrayFallback (per scope) same fallback contract as dispatch-store"
137
- },
138
- {
139
- "file": "engine/pull-requests-store.js",
140
- "note": "_mirrorJsonFromSql + _readJsonArrayFallback (per scope)"
141
- },
142
- {
143
- "file": "engine/logs-store.js",
144
- "note": "engine/log.json mirror written by shared._flushLogBuffer's byJsonPath loop — Phase 4.5 will retire"
145
- },
146
- {
147
- "file": "engine/metrics-store.js",
148
- "note": "_mirrorJsonFromSql + _readJsonObjectFallback"
149
- },
150
- {
151
- "file": "engine/watches-store.js",
152
- "note": "_mirrorJsonFromSql + _readJsonArrayFallback"
153
- },
154
- {
155
- "file": "engine/small-state-store.js",
156
- "note": "_mirrorScheduleRunsJson, _mirrorPipelineRunsJson, _mirrorManagedProcessesJson, _mirrorWorktreePoolJson + each store's _readJson fallback path"
157
- },
158
- {
159
- "file": "CLAUDE.md",
160
- "lines": "47-66, 240-265",
161
- "note": "State Files + Concurrency sections still describe JSON files as the source of truth; they describe a layered SQLite-then-mirror reality in places but the headline contract still reads as JSON-primary. Rewrite these sections to make SQL-as-source-of-truth the headline and the JSON mirrors a transitional compatibility detail."
162
- }
71
+ { "file": "engine/dispatch-store.js", "note": "_mirrorJsonFromSql + _readDispatchJsonFallback — used when SQL is empty AND JSON has content (test seeding + first-time hydrate)" },
72
+ { "file": "engine/work-items-store.js", "note": "_mirrorJsonFromSql + _readJsonArrayFallback (per scope) — same fallback contract as dispatch-store" },
73
+ { "file": "engine/pull-requests-store.js", "note": "_mirrorJsonFromSql + _readJsonArrayFallback (per scope)" },
74
+ { "file": "engine/logs-store.js", "note": "engine/log.json mirror written by shared._flushLogBuffer's byJsonPath loop — Phase 4.5 will retire" },
75
+ { "file": "engine/metrics-store.js", "note": "_mirrorJsonFromSql + _readJsonObjectFallback" },
76
+ { "file": "engine/watches-store.js", "note": "_mirrorJsonFromSql + _readJsonArrayFallback" },
77
+ { "file": "engine/small-state-store.js", "note": "_mirrorScheduleRunsJson, _mirrorPipelineRunsJson, _mirrorManagedProcessesJson, _mirrorWorktreePoolJson + each store's _readJson fallback path" },
78
+ { "file": "CLAUDE.md", "lines": "47-66, 240-265", "note": "State Files + Concurrency sections still describe JSON files as the source of truth; they describe a layered SQLite-then-mirror reality in places but the headline contract still reads as JSON-primary. Rewrite these sections to make SQL-as-source-of-truth the headline and the JSON mirrors a transitional compatibility detail." }
163
79
  ],
164
80
  "removalGate": "All direct-readers of the mirror JSON files must be confirmed routed through their respective SQL store's read helper. Specifically: (a) grep the codebase for `safeJson`, `safeJsonArr`, `safeJsonObj`, `readFileSync(...work-items.json|pull-requests.json|metrics.json|watches.json|schedule-runs.json|pipeline-runs.json|managed-processes.json|worktree-pool.json|log.json|dispatch.json...)` and confirm every hit is either (i) a test fixture that can move to the SQL helper, or (ii) intentionally documented as bypassing SQL. (b) Run the full test suite with each store's _mirrorJsonFromSql temporarily neutered (returning early before safeWrite) and confirm 0 failures — that proves no production code path depends on the mirror. Once both conditions hold, removal deletes each store's _mirrorJsonFromSql call site in shared.js (mutateWorkItems/mutatePullRequests/etc.), the corresponding _readJsonArrayFallback paths, and the JSON file gitignore entries. CLAUDE.md update can ship independently as soon as someone has bandwidth.",
165
81
  "targetRemovalDate": null,
@@ -169,51 +85,15 @@
169
85
  "id": "prune-default-claude-config",
170
86
  "description": "pruneDefaultClaudeConfig: active sanitizer that strips generated `config.claude.{binary,outputFormat,allowedTools,permissionMode}` defaults from persisted config.json so the `deprecated-config-claude` warning stops tripping on stale defaults left by older `minions init` versions. Sub-cluster of `config-claude-binary-override` — the prune deliberately preserves non-default user overrides (binary/allowedTools), which is what keeps the override branch in engine/runtimes/claude.js load-bearing.",
171
87
  "code": [
172
- {
173
- "file": "engine/shared.js",
174
- "lines": "3126",
175
- "note": "pruneDefaultClaudeConfig definition: preserves non-default binary/allowedTools, always strips permissionMode + outputFormat"
176
- },
177
- {
178
- "file": "engine/shared.js",
179
- "lines": "5673",
180
- "note": "Module export entry"
181
- },
182
- {
183
- "file": "dashboard.js",
184
- "lines": "202",
185
- "note": "Called when loading config for the dashboard UI"
186
- },
187
- {
188
- "file": "dashboard.js",
189
- "lines": "9116",
190
- "note": "Called during first config save handler"
191
- },
192
- {
193
- "file": "dashboard.js",
194
- "lines": "9331",
195
- "note": "Called during second config save path"
196
- },
197
- {
198
- "file": "dashboard.js",
199
- "lines": "9450",
200
- "note": "Called during third config save path"
201
- },
202
- {
203
- "file": "minions.js",
204
- "lines": "385",
205
- "note": "Called during CLI init/update flow"
206
- },
207
- {
208
- "file": "test/unit.test.js",
209
- "lines": "2260-2303",
210
- "note": "Behavioural unit tests (default strip, override preservation, outputFormat unconditional strip) + dashboard call-site source pin"
211
- },
212
- {
213
- "file": "test/unit/runtime-fleet-helpers.test.js",
214
- "lines": "546",
215
- "note": "Source-inspection test pinning the dashboard handler call site"
216
- }
88
+ { "file": "engine/shared.js", "lines": "3126", "note": "pruneDefaultClaudeConfig definition: preserves non-default binary/allowedTools, always strips permissionMode + outputFormat" },
89
+ { "file": "engine/shared.js", "lines": "5673", "note": "Module export entry" },
90
+ { "file": "dashboard.js", "lines": "202", "note": "Called when loading config for the dashboard UI" },
91
+ { "file": "dashboard.js", "lines": "9116", "note": "Called during first config save handler" },
92
+ { "file": "dashboard.js", "lines": "9331", "note": "Called during second config save path" },
93
+ { "file": "dashboard.js", "lines": "9450", "note": "Called during third config save path" },
94
+ { "file": "minions.js", "lines": "385", "note": "Called during CLI init/update flow" },
95
+ { "file": "test/unit.test.js", "lines": "2260-2303", "note": "Behavioural unit tests (default strip, override preservation, outputFormat unconditional strip) + dashboard call-site source pin" },
96
+ { "file": "test/unit/runtime-fleet-helpers.test.js", "lines": "546", "note": "Source-inspection test pinning the dashboard handler call site" }
217
97
  ],
218
98
  "removalGate": "Telemetry: pruneDefaultClaudeConfig must return false (no mutation) for every call across all known engines for >=30 consecutive days (add an `_engine.pruneDefaultClaudeConfigStrips` counter if needed to observe this), AND the parent `config-claude-binary-override` entry must have already cleared its own gate. The dependency is strict: removing the prune while users still rely on the override branch would surface the `deprecated-config-claude` warning on every stale generated default. Once both conditions hold, removal is the function definition (engine/shared.js:3126), the export at :5673, all 5 call sites (dashboard.js:202, :9116, :9331, :9450; minions.js:385), and the tests at unit.test.js:2260-2303 + runtime-fleet-helpers.test.js:546.",
219
99
  "targetRemovalDate": null,
@@ -226,10 +106,7 @@
226
106
  "status": "removed",
227
107
  "removedDate": "2026-06-25",
228
108
  "code": [
229
- {
230
- "file": "engine/ado.js",
231
- "note": "isAdoThrottled() arg-less branch and the global-OR fold over the per-org Map. Single call site to migrate: shared.getAdoOrgBase(project) is already in scope at every consumer."
232
- }
109
+ { "file": "engine/ado.js", "note": "isAdoThrottled() arg-less branch and the global-OR fold over the per-org Map. Single call site to migrate: shared.getAdoOrgBase(project) is already in scope at every consumer." }
233
110
  ],
234
111
  "removalGate": "Two conditions must hold simultaneously: (a) grep `engine/ado.js` for `isAdoThrottled\\s*\\(\\s*\\)` and confirm zero arg-less call sites remain across the engine — every caller passes a concrete `orgBase` resolved via `shared.getAdoOrgBase(project)`; (b) `GET /api/diagnostics/ado-throttle` on a live engine has been observed for >=2 consecutive weeks reporting per-org keys (proves the per-org Map is populated under load and the global-OR isn't masking a regression). Once both hold, removal deletes the arg-less branch in isAdoThrottled and the global-OR fold; callers that still pass no argument become an immediate, surfaced bug rather than a silent over-throttle.",
235
112
  "targetRemovalDate": "2026-08-03",
@@ -273,13 +150,10 @@
273
150
  "id": "pr-observe-observe-body-param",
274
151
  "description": "Legacy `observe` body parameter on `POST /api/pull-requests/observe`. The W-mq5s5ttx000j7ab8 endpoint sub-WI introduces canonical `contextOnly` as the inverse (`observe: false` ⇔ `contextOnly: true`) and keeps `observe` accepted for backward compat. Registering the deprecation here so the alias has a documented removal path; the WI explicitly notes this entry is the implementer's call (it is kept for backward compat and may live longer than the underscore-prefixed record fields).",
275
152
  "code": [
276
- {
277
- "file": "dashboard.js",
278
- "note": "POST /api/pull-requests/observe handler reads `body.contextOnly` first, then falls back to `!body.observe` for backwards compat."
279
- }
153
+ { "file": "dashboard.js", "note": "POST /api/pull-requests/observe handler reads `body.contextOnly` first, then falls back to `!body.observe` for backwards compat." }
280
154
  ],
281
155
  "deprecated": "2026-06-08",
282
156
  "targetRemovalDate": null,
283
157
  "notes": "targetRemovalDate intentionally null — unlike the record-field aliases (`_contextOnly`, `_autoObserve`, `_manual`) which carry a 7-day clock, the `observe` body param is documented as a longer-lived back-compat alias. Set targetRemovalDate to a concrete future date once the dashboard UI + any client scripts are confirmed to POST `contextOnly` exclusively. Removal scope when the date is set: drop the `body.observe` fallback in dashboard.js, drop `observe` from the route registry params, and update any client still POSTing `observe`."
284
158
  }
285
- ]
159
+ ]