@yemi33/minions 0.1.2177 → 0.1.2178

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
@@ -182,6 +182,27 @@ function mutateDashboardConfig(mutator) {
182
182
  }, { defaultValue: { projects: [], agents: {}, engine: {} }, skipWriteIfUnchanged: true });
183
183
  }
184
184
 
185
+ // P-f3c9d0e7 — shared body for the four convenience pause/resume endpoints
186
+ // (`/api/engine/polling/{pause,resume}` and `/api/engine/auto-fix/{pause,resume}`).
187
+ // Persists `config.engine[key] = paused`, busts the in-memory CONFIG so the
188
+ // dashboard sees the new value immediately, invalidates the status cache so
189
+ // the SPA's next poll picks it up, and replies `{ paused, at: <iso> }` per
190
+ // the plan's wire contract. Idempotent — repeating the same call writes the
191
+ // same value (mutateDashboardConfig short-circuits when unchanged) and
192
+ // returns the same shape.
193
+ function _setEnginePauseFlag(res, key, paused) {
194
+ mutateDashboardConfig(config => {
195
+ if (!config.engine || typeof config.engine !== 'object' || Array.isArray(config.engine)) {
196
+ config.engine = {};
197
+ }
198
+ config.engine[key] = paused;
199
+ return config;
200
+ });
201
+ reloadConfig();
202
+ invalidateStatusCache();
203
+ return jsonReply(res, 200, { paused, at: new Date().toISOString() });
204
+ }
205
+
185
206
  function mergeSettingsConfigUpdate(current, candidate, body, patch = {}) {
186
207
  if (!current || typeof current !== 'object' || Array.isArray(current)) current = {};
187
208
  if (body.engine) {
@@ -1907,6 +1928,11 @@ function _buildStatusFastState() {
1907
1928
  && hbAge > ENGINE_HEARTBEAT_STALE_MS
1908
1929
  && tickAge > tickStaleThresholdMs),
1909
1930
  tickInterval: tickInterval,
1931
+ // P-g7a2b4c5 — surface the two operator kill-switches on every status
1932
+ // payload so the cross-page sticky banner can render without a second
1933
+ // round-trip. Both default to false when unset in config.engine.
1934
+ pollingPaused: !!CONFIG?.engine?.pollingPaused,
1935
+ autoFixPaused: !!CONFIG?.engine?.autoFixPaused,
1910
1936
  },
1911
1937
  adoThrottle: ado.getAdoThrottleState(),
1912
1938
  ghThrottle: gh.getGhThrottleState(),
@@ -6700,9 +6726,50 @@ const server = http.createServer(async (req, res) => {
6700
6726
  try {
6701
6727
  const plan = JSON.parse(content);
6702
6728
  const status = plan.status || 'active';
6729
+ // W-mqacrzis0003df4a Bug 2 — fresh staleness from plans/*.md mtime.
6730
+ // plan.planStale lags by an engine tick; stat the source plan here
6731
+ // so the Plans tab does not silently mirror a stale-but-cached false.
6732
+ let freshStale = false;
6733
+ if (!archived && plan.source_plan) {
6734
+ try {
6735
+ const sourceMtime = Math.floor(fs.statSync(path.join(PLANS_DIR, plan.source_plan)).mtimeMs);
6736
+ const recorded = plan.sourcePlanModifiedAt ? new Date(plan.sourcePlanModifiedAt).getTime() : null;
6737
+ if (recorded && sourceMtime > recorded) freshStale = true;
6738
+ } catch { /* source plan may have been deleted/renamed */ }
6739
+ }
6740
+ // P-e8d49105 — _projects rollup feeds the plan-card multi-badge
6741
+ // path in dashboard/js/render-plans.js. For PRD JSONs, walk
6742
+ // missing_features[].project (order-preserving dedup) and fall
6743
+ // back to plan.project for items that omit it. Single-project
6744
+ // plans collapse to a single-element array, identical to today's
6745
+ // single `p.project` badge.
6746
+ const _projects = [];
6747
+ const _seen = new Set();
6748
+ for (const it of (plan.missing_features || [])) {
6749
+ const proj = (it && it.project) || plan.project || '';
6750
+ if (proj && !_seen.has(proj)) { _seen.add(proj); _projects.push(proj); }
6751
+ }
6752
+ if (_projects.length === 0 && plan.project) _projects.push(plan.project);
6753
+ // P-66b1faec — _perProjectProgress feeds the per-project status
6754
+ // pills in the plan card meta line (dashboard/js/render-plans.js
6755
+ // renderPlanCard). Group missing_features by item.project (with
6756
+ // fallback to plan.project) and count `status === 'done'` per
6757
+ // bucket. Keys are first-seen order so the rendered pill row is
6758
+ // stable. Always emitted on PRD JSON records (possibly empty);
6759
+ // MD drafts omit it since they carry no item-level statuses.
6760
+ const _perProjectProgress = {};
6761
+ for (const it of (plan.missing_features || [])) {
6762
+ const proj = (it && it.project) || plan.project || '';
6763
+ if (!proj) continue;
6764
+ if (!_perProjectProgress[proj]) _perProjectProgress[proj] = { complete: 0, total: 0 };
6765
+ _perProjectProgress[proj].total += 1;
6766
+ if (it && it.status === 'done') _perProjectProgress[proj].complete += 1;
6767
+ }
6703
6768
  return {
6704
6769
  file: f, format: 'prd', archived,
6705
6770
  project: plan.project || '',
6771
+ _projects,
6772
+ _perProjectProgress,
6706
6773
  summary: plan.plan_summary || '',
6707
6774
  status,
6708
6775
  branchStrategy: plan.branch_strategy || 'parallel',
@@ -6717,7 +6784,7 @@ const server = http.createServer(async (req, res) => {
6717
6784
  sourcePlan: plan.source_plan || null,
6718
6785
  archiveReady: plan._archiveReady || false,
6719
6786
  archiveReadyAt: plan._archiveReadyAt || null,
6720
- planStale: plan.planStale || false,
6787
+ planStale: freshStale || plan.planStale || false,
6721
6788
  };
6722
6789
  } catch { return null; /* JSON parse fallback */ }
6723
6790
  } else {
@@ -6726,9 +6793,19 @@ const server = http.createServer(async (req, res) => {
6726
6793
  const authorMatch = content.match(/\*\*Author:\*\*\s*(.+)/m);
6727
6794
  const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/m);
6728
6795
  const versionMatch = f.match(/-v(\d+)/);
6796
+ // P-e8d49105 — for MD drafts, the cross-repo marker / **Projects:**
6797
+ // line is the source of truth (parsed by shared.extractPlanTargetProjects).
6798
+ // If neither is present, fall back to the singular **Project:** header
6799
+ // so single-project drafts render the same badge as today.
6800
+ const declaredProject = projectMatch ? projectMatch[1].trim() : '';
6801
+ const targetProjects = shared.extractPlanTargetProjects(content);
6802
+ const _projects = targetProjects.length > 0
6803
+ ? targetProjects
6804
+ : (declaredProject ? [declaredProject] : []);
6729
6805
  return {
6730
6806
  file: f, format: 'draft', archived,
6731
- project: projectMatch ? projectMatch[1].trim() : '',
6807
+ project: declaredProject,
6808
+ _projects,
6732
6809
  summary: titleMatch ? titleMatch[1].trim() : f.replace('.md', ''),
6733
6810
  status: archived ? 'completed' : completedPrdFiles.has(f) ? 'converted' : 'draft',
6734
6811
  branchStrategy: '',
@@ -6842,6 +6919,22 @@ const server = http.createServer(async (req, res) => {
6842
6919
  return data;
6843
6920
  }, { defaultValue: {} });
6844
6921
 
6922
+ // W-mqacrzis0003df4a — Fresh source-plan staleness check. Approve is the
6923
+ // last gate before materialization, and the diff-aware regen block below
6924
+ // gates on `wasStale`. The persisted `data.planStale` flag lags by an
6925
+ // engine tick (~10s); without this fresh stat a fast user can Approve
6926
+ // within the tick window, `wasStale` stays false, the diff-aware regen
6927
+ // is silently skipped, and items materialize from the OLD PRD. Mirrors
6928
+ // the staleness logic in engine/queries.js#getPrdInfo + the /api/plans
6929
+ // handler above so all three readers agree.
6930
+ if (!wasStale && plan && plan.source_plan && plan.sourcePlanModifiedAt) {
6931
+ try {
6932
+ const sourceMtime = Math.floor(fs.statSync(path.join(PLANS_DIR, plan.source_plan)).mtimeMs);
6933
+ const recorded = new Date(plan.sourcePlanModifiedAt).getTime();
6934
+ if (recorded && sourceMtime > recorded) wasStale = true;
6935
+ } catch { /* source plan may have been deleted/renamed — fall through with wasStale=false */ }
6936
+ }
6937
+
6845
6938
  // Resume paused work items across all projects
6846
6939
  let resumed = 0;
6847
6940
  const resumedItemIds = [];
@@ -7193,7 +7286,9 @@ const server = http.createServer(async (req, res) => {
7193
7286
 
7194
7287
  let archivedSource = null;
7195
7288
  let plan = {};
7196
- let archiveWarnings = [];
7289
+ const archiveWarnings = [];
7290
+ let archivedPrd = null;
7291
+ const archivedPrds = [];
7197
7292
  if (isPrd) {
7198
7293
  const result = _archivePrdPostProcess({
7199
7294
  planFile: body.file,
@@ -7202,8 +7297,75 @@ const server = http.createServer(async (req, res) => {
7202
7297
  plansDir: PLANS_DIR,
7203
7298
  });
7204
7299
  archivedSource = result.archivedSource;
7205
- archiveWarnings = result.archiveWarnings;
7300
+ archiveWarnings.push(...result.archiveWarnings);
7206
7301
  plan = result.plan;
7302
+ } else {
7303
+ // W-mqa27r9b0004c055 — symmetric cascade: archiving a .md must also
7304
+ // archive any PRD whose `source_plan` points back at it. Without this,
7305
+ // the dashboard still renders the PRD as a status-completed plan card
7306
+ // forever (real incident: killswitches-and-granular-controls.md →
7307
+ // minions-opg-2026-06-10-2.json). The per-PRD status-flip / sidecar /
7308
+ // source-plan-move steps are delegated to _archivePrdPostProcess so
7309
+ // both branches share the per-concern try/catch granularity Dallas
7310
+ // shipped for the PRD branch in W-mqa13ulk0002def5 (PR #3222) — the
7311
+ // helper's source-plan move is a safe no-op here because the outer
7312
+ // handler already renamed body.file into plans/archive/.
7313
+ let prdFiles = [];
7314
+ try {
7315
+ prdFiles = fs.readdirSync(PRD_DIR).filter(f => f.endsWith('.json'));
7316
+ } catch (e) {
7317
+ // ENOENT on prd/ is expected for projects without a PRD dir yet —
7318
+ // don't surface as a user-visible warning. Other errors (EACCES,
7319
+ // EIO) get logged + warned.
7320
+ if (e.code !== 'ENOENT') {
7321
+ const warning = `Archive could not enumerate ${PRD_DIR}: ${e.message}`;
7322
+ archiveWarnings.push(warning);
7323
+ console.warn(warning);
7324
+ }
7325
+ }
7326
+ for (const prdFile of prdFiles) {
7327
+ const prdLivePath = path.join(PRD_DIR, prdFile);
7328
+ let prd = null;
7329
+ try {
7330
+ prd = safeJsonObj(prdLivePath);
7331
+ } catch (e) {
7332
+ const warning = `Archive could not read PRD ${prdFile}: ${e.message}`;
7333
+ archiveWarnings.push(warning);
7334
+ console.warn(warning);
7335
+ continue;
7336
+ }
7337
+ if (!prd || prd.source_plan !== body.file) continue;
7338
+
7339
+ const prdArchiveDir = path.join(PRD_DIR, 'archive');
7340
+ try {
7341
+ if (!fs.existsSync(prdArchiveDir)) fs.mkdirSync(prdArchiveDir, { recursive: true });
7342
+ } catch (e) {
7343
+ const warning = `Archive could not create PRD archive dir for ${prdFile}: ${e.message}`;
7344
+ archiveWarnings.push(warning);
7345
+ console.warn(warning);
7346
+ continue;
7347
+ }
7348
+ const prdArchivePath = path.join(prdArchiveDir, prdFile);
7349
+ try {
7350
+ fs.renameSync(prdLivePath, prdArchivePath);
7351
+ } catch (e) {
7352
+ const warning = `Archive could not move PRD ${prdFile}: ${e.message}`;
7353
+ archiveWarnings.push(warning);
7354
+ console.warn(warning);
7355
+ continue;
7356
+ }
7357
+ // Delegate status-flip + sidecar cleanup + (no-op) source-plan move
7358
+ // to the shared helper so both branches stay in lockstep.
7359
+ const cascadeResult = _archivePrdPostProcess({
7360
+ planFile: prdFile,
7361
+ archivePath: prdArchivePath,
7362
+ planPath: prdLivePath,
7363
+ plansDir: PLANS_DIR,
7364
+ });
7365
+ archiveWarnings.push(...cascadeResult.archiveWarnings);
7366
+ archivedPrds.push(prdFile);
7367
+ }
7368
+ if (archivedPrds.length > 0) archivedPrd = archivedPrds[0];
7207
7369
  }
7208
7370
 
7209
7371
  // Cancel pending work items linked to this plan so the engine stops
@@ -7233,6 +7395,8 @@ const server = http.createServer(async (req, res) => {
7233
7395
  invalidateStatusCache();
7234
7396
  invalidatePlansCache();
7235
7397
  const payload = { ok: true, archived: body.file, archivedSource, cancelledItems };
7398
+ if (archivedPrd) payload.archivedPrd = archivedPrd;
7399
+ if (archivedPrds.length > 1) payload.archivedPrds = archivedPrds;
7236
7400
  if (archiveWarnings.length > 0) payload.warnings = archiveWarnings;
7237
7401
  return jsonReply(res, 200, payload);
7238
7402
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
@@ -9501,6 +9665,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
9501
9665
  // larger models); max 1h (matches CC_CALL_TIMEOUT_MS so the watchdog
9502
9666
  // never outlives the outer abort).
9503
9667
  ccTurnTimeoutMs: [10000, 3600000],
9668
+ // W-mq9acoo800177bcb — bounded-concurrency for pre-dispatch validator.
9669
+ // 1 floor (sequential fallback) and 20 ceiling (above this the LLM
9670
+ // provider's per-second rate limits dominate; throughput gains taper).
9671
+ preDispatchEvalConcurrency: [1, 20],
9504
9672
  };
9505
9673
  for (const [key, [min, max]] of Object.entries(numericFields)) {
9506
9674
  if (e[key] !== undefined) {
@@ -9624,6 +9792,17 @@ What would you like to discuss or change? When you're happy, say "approve" and I
9624
9792
  else if (valid.includes(e.copilotStreamMode)) _setEngineConfig('copilotStreamMode', e.copilotStreamMode);
9625
9793
  else _clamped.push(`copilotStreamMode: "${e.copilotStreamMode}" not in [on, off] (kept previous value)`);
9626
9794
  }
9795
+ // P-mcp-storm — MCP server names to disable for autonomous Copilot agents
9796
+ // (--disable-mcp-server). Accept an array OR a comma/whitespace string;
9797
+ // normalize to a deduped array of trimmed names. Empty clears (inherit all).
9798
+ if (e.copilotAgentDisabledMcpServers !== undefined) {
9799
+ const src = Array.isArray(e.copilotAgentDisabledMcpServers)
9800
+ ? e.copilotAgentDisabledMcpServers
9801
+ : String(e.copilotAgentDisabledMcpServers || '').split(/[\s,]+/);
9802
+ const names = [...new Set(src.map(s => String(s == null ? '' : s).trim()).filter(Boolean))];
9803
+ if (names.length) _setEngineConfig('copilotAgentDisabledMcpServers', names);
9804
+ else _deleteEngineConfig('copilotAgentDisabledMcpServers');
9805
+ }
9627
9806
  // W-mpmwxkrw000872ec — fontSize allowlist. Clamps invalid values
9628
9807
  // (rather than silently failing) so the dashboard bootstrap never
9629
9808
  // ends up with an unknown data-font-size attribute.
@@ -11746,6 +11925,37 @@ What would you like to discuss or change? When you're happy, say "approve" and I
11746
11925
  return jsonReply(res, 200, { ok: true, cleared: cause, prId });
11747
11926
  }},
11748
11927
 
11928
+ // ─── P-f3c9d0e7: convenience pause/resume endpoints ─────────────────────
11929
+ // Single-call wrappers over `POST /api/settings { engine: { <flag>: bool } }`
11930
+ // for the two operator kill-switches:
11931
+ // - engine.pollingPaused (P-a1f3c2d4) — pauses all PR polling
11932
+ // - engine.autoFixPaused (P-b2e5d8c7) — pauses auto-fix dispatch
11933
+ //
11934
+ // These exist so the dashboard "Emergency Stop" buttons and any future
11935
+ // `minions pause` / `minions autopause` CLI commands are one-line
11936
+ // implementations (no JSON body construction needed).
11937
+ //
11938
+ // Engine pickup contract: engine.js calls reloadConfig() each tick and
11939
+ // reads `config.engine.<flag> === true` at every gate site, so the next
11940
+ // tick observes the change. Setting the flag to `false` on resume is
11941
+ // enough — the engine has its own log-throttle reset
11942
+ // (`_resetPollingPausedLogState`, `_resetAutoFixPausedLogState`) that
11943
+ // fires automatically when the gate reads `!paused`. The engine and
11944
+ // dashboard run in separate processes, so the dashboard can't (and
11945
+ // doesn't need to) scrub engine-process module state directly.
11946
+ { method: 'POST', path: '/api/engine/polling/pause', desc: 'Pause all PR polling — sets engine.pollingPaused=true (P-f3c9d0e7 convenience over POST /api/settings)', handler: async (req, res) => {
11947
+ return _setEnginePauseFlag(res, 'pollingPaused', true);
11948
+ }},
11949
+ { method: 'POST', path: '/api/engine/polling/resume', desc: 'Resume PR polling — sets engine.pollingPaused=false', handler: async (req, res) => {
11950
+ return _setEnginePauseFlag(res, 'pollingPaused', false);
11951
+ }},
11952
+ { method: 'POST', path: '/api/engine/auto-fix/pause', desc: 'Pause auto-fix dispatch — sets engine.autoFixPaused=true', handler: async (req, res) => {
11953
+ return _setEnginePauseFlag(res, 'autoFixPaused', true);
11954
+ }},
11955
+ { method: 'POST', path: '/api/engine/auto-fix/resume', desc: 'Resume auto-fix dispatch — sets engine.autoFixPaused=false', handler: async (req, res) => {
11956
+ return _setEnginePauseFlag(res, 'autoFixPaused', false);
11957
+ }},
11958
+
11749
11959
  { method: 'POST', path: '/api/plans/create', desc: 'Create a plan from user-provided content', params: 'title, content, project?', handler: async (req, res) => {
11750
11960
  const body = await readBody(req);
11751
11961
  const { title, content, project: projectName, meetingId } = body;
@@ -11758,6 +11968,29 @@ What would you like to discuss or change? When you're happy, say "approve" and I
11758
11968
  return jsonReply(res, 400, { error: 'Plan content must start with a markdown heading (#), bold text (**), or a list item' });
11759
11969
  }
11760
11970
 
11971
+ // P-2e9b54d1: `project` may be a string (today's behavior) OR an array
11972
+ // of project names (cross-repo plan). Normalize to a deduped, trimmed
11973
+ // string[]. Empty values are dropped; ≥2 entries triggers the cross-
11974
+ // repo plan shape (no singular `**Project:**` header, plural
11975
+ // `**Projects:**` line + `<!-- minions:targetProjects=... -->` marker).
11976
+ const rawProjects = Array.isArray(body.project)
11977
+ ? body.project
11978
+ : (body.project ? [body.project] : []);
11979
+ const projectNames = [];
11980
+ for (const raw of rawProjects) {
11981
+ const name = String(raw || '').trim();
11982
+ if (name && !projectNames.includes(name)) projectNames.push(name);
11983
+ }
11984
+ if (projectNames.length > 0) {
11985
+ reloadConfig();
11986
+ const projects = shared.getProjects(CONFIG);
11987
+ for (const name of projectNames) {
11988
+ if (!findProjectByName(projects, name)) {
11989
+ return jsonReply(res, 400, { error: formatUnknownProjectError(name, projects) });
11990
+ }
11991
+ }
11992
+ }
11993
+
11761
11994
  const plansDir = path.join(MINIONS_DIR, 'plans');
11762
11995
  if (!fs.existsSync(plansDir)) fs.mkdirSync(plansDir, { recursive: true });
11763
11996
  const slug = shared.slugify(title);
@@ -11765,8 +11998,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
11765
11998
  const filename = `${slug}-${date}.md`;
11766
11999
  const filePath = shared.uniquePath(path.join(plansDir, filename));
11767
12000
 
12001
+ const projectLines = projectNames.length >= 2
12002
+ ? `**Projects:** ${projectNames.join(', ')}\n` +
12003
+ `<!-- minions:targetProjects=${projectNames.join(',')} -->\n`
12004
+ : (projectNames.length === 1 ? `**Project:** ${projectNames[0]}\n` : '');
11768
12005
  const header = `# ${title}\n\n` +
11769
- (projectName ? `**Project:** ${projectName}\n` : '') +
12006
+ projectLines +
11770
12007
  (meetingId ? `**Source Meeting:** ${meetingId}\n` : '') +
11771
12008
  `**Created:** ${date}\n**By:** human teammate\n\n---\n\n`;
11772
12009
  safeWrite(filePath, header + content);
package/docs/README.md CHANGED
@@ -7,12 +7,13 @@ A navigable index of every Markdown file under `docs/`. Entries are grouped by a
7
7
  Hands-on stories and distribution guides for people running or evaluating Minions.
8
8
 
9
9
  - [blog-first-successful-dispatch.md](blog-first-successful-dispatch.md) — Narrative walkthrough of the first end-to-end agent dispatch and the seven failed spawn attempts that preceded it.
10
- - [distribution.md](distribution.md) — How Minions is published as the `@yemi33/minions` npm package and what the two-repo (origin / personal) sync strips.
10
+ - [distribution.md](distribution.md) — How Minions is published (this repo: `@opg-microsoft/minions` to GitHub Packages; paired peer `yemi33/minions` to npm) and the bidirectional sync contract automated opg → yemi33 backport workflow + manual yemi33 → opg sync PRs.
11
11
 
12
12
  ## Contributor-facing
13
13
 
14
14
  Architecture, design proposals, and lifecycle references for people working on the engine, dashboard, or playbooks.
15
15
 
16
+ - [branch-derivation.md](branch-derivation.md) — Engine-side branch fallback (`work/<wi-id>`) vs. agent-authored long form, the structured-vs-loose PR-pointer extractors, and the canonical PR-fix duplication incident.
16
17
  - [command-center.md](command-center.md) — Command Center (CC) chat panel: persistent Sonnet sessions, `--resume` semantics, system-prompt invalidation, and per-tab session storage.
17
18
  - [completion-reports.md](completion-reports.md) — Canonical schema for the per-spawn completion JSON: trust nonce, `failure_class` enum, `noop` semantics, `retryable` / `needs_rerun` shape, and the artifacts array.
18
19
  - [constants.md](constants.md) — Cross-cutting status / type / condition constants (`WI_STATUS`, `WORK_TYPE`, `PR_STATUS`, `WATCH_CONDITION`, …) and the no-magic-strings invariant.
@@ -21,14 +22,17 @@ Architecture, design proposals, and lifecycle references for people working on t
21
22
  - [cooldown-merge-semantics.md](cooldown-merge-semantics.md) — Scoping deliverable defining merge semantics for `saveCooldowns` (longer-of TTL merge, key-level upserts, gitignored on-disk format).
22
23
  - [copilot-cli-schema.md](copilot-cli-schema.md) — Behavior and schema reference for the GitHub Copilot CLI adapter (capability flags, stdin vs `-p`, model discovery, effort levels).
23
24
  - [dead-code-audit-retractions.md](dead-code-audit-retractions.md) — Retracted dead-code-audit findings (false positives) that future audits MUST read before re-citing.
25
+ - [deprecated-process.md](deprecated-process.md) — Schema for `docs/deprecated.json` and the weekly `cleanup-deprecated` audit walk that retires entries past their removal signal.
24
26
  - [design-state-storage.md](design-state-storage.md) — Design proposal evaluating five database options for replacing Minions' file-based JSON state; recommends `node:sqlite` as the medium-term target (accepted; implementation tracked in CHANGELOG.md Phases 0–9).
25
27
  - [harness-mode.md](harness-mode.md) — Tri-Agent Harness Mode (`harness_mode: "tri_agent"` on scheduled tasks): Planner → Generator → Evaluator loop that iterates a shared on-disk artifact until a rubric passes or the iteration cap fires.
26
28
  - [kb-sweep.md](kb-sweep.md) — Knowledge-base consolidation sweep (hash dedup → LLM batch dedup/reclassify → per-entry compress) and the detached runner that keeps it alive across `minions restart`.
27
29
  - [keep-processes.md](keep-processes.md) — `meta.keep_processes` sidecar contract: when to use it vs managed-spawn, sidecar schema, caps, and the [`engine/keep-process-sweep.js`](../engine/keep-process-sweep.js) lifecycle.
30
+ - [live-checkout-mode.md](live-checkout-mode.md) — Per-project opt-in `worktreeMode: 'live'`: skips `git worktree add` and dispatches in-place inside `project.localPath` for `repo`-managed trees, submodule-heavy repos, deep Windows paths, and native build state. Includes the refuse-on-dirty contract and the per-project mutating-concurrency cap of 1.
28
31
  - [managed-spawn.md](managed-spawn.md) — Engine-owned long-running services (managed-spawn primitive): sidecar schema, healthcheck examples, lifecycle, dashboard API, and the WI 1 (build) → WI 2 (test) chained-validation pattern.
29
32
  - [plan-lifecycle.md](plan-lifecycle.md) — Full plan pipeline from `/plan` through PRD materialization, dispatch with dependency gating, verify task, and human archive.
30
33
  - [pr-comment-followup.md](pr-comment-followup.md) — PR-comment follow-up dispatch contract: fix/review agents may spin off a new WI via `POST /api/work-items` with `meta.pr_followup` instead of broadening the current PR or rebutting the comment.
31
34
  - [pr-review-fix-loop.md](pr-review-fix-loop.md) — How the engine moves a PR from creation through review, fix dispatch, and re-review, including stale-status guards.
35
+ - [project-skills.md](project-skills.md) — Project-local skill discovery (`.claude/skills/`, `.claude/commands/`, `CLAUDE.md` / `.github/copilot-instructions.md` slash-command mentions): how dispatched agents see and steer toward purpose-built tooling the project ships, plus the intent-vocabulary contract.
32
36
  - [qa-runbook-lifecycle.md](qa-runbook-lifecycle.md) — End-to-end QA runbook lifecycle (W-mpeiwz6k0005bf34): runbook + run-record storage, `POST /api/qa/runbooks/run` dispatch into the `qa-validate` playbook, artifact contract, and how the `/qa` page mirrors managed-spawn observability.
33
37
  - [qa-runbooks.md](qa-runbooks.md) — Per-project QA runbook schema, storage layout (`projects/<name>/runbooks/<id>.json`), CRUD endpoints, run-record lifecycle, and the `qa-validate` agent sidecar contract.
34
38
  - [rfc-completion-json.md](rfc-completion-json.md) — RFC for replacing stdout regex-scraping with a structured `completion.json` control-plane protocol.
@@ -38,8 +42,10 @@ Architecture, design proposals, and lifecycle references for people working on t
38
42
  - [slim-ux/concepts.md](slim-ux/concepts.md) — Slim-UX design notes: simplified surface concepts driving the project picker, inline project link, and decoupled folder picker.
39
43
  - [slim-ux/architecture-suggestions.md](slim-ux/architecture-suggestions.md) — Slim-UX follow-up architecture suggestions paired with `concepts.md`.
40
44
  - [team-memory.md](team-memory.md) — Per-agent memory layer (`knowledge/agents/<id>.md`) and the consolidation/routing rules that populate it from `notes/inbox/`.
45
+ - [timeouts-and-liveness.md](timeouts-and-liveness.md) — What kills (or doesn't kill) a live tracked agent: the wall-clock vs steering kill invariants, spawn-phase watchdog gates, steering safety nets, and stale-orphan detection ladder.
41
46
  - [watches.md](watches.md) — Persistent monitoring jobs (`engine/watches.json`): target-type registry, conditions, follow-up actions, and the `watches.d/` plugin folder.
42
47
  - [workspace-manifests.md](workspace-manifests.md) — Declarative per-agent permission scoping: `allowed_tools` / `allowed_repos` / `allowed_external_urls` / `memory_scope`, dispatch-time repo gate, and runtime `--allowedTools` narrowing.
48
+ - [worktree-lifecycle.md](worktree-lifecycle.md) — Worktree pool recycling, the live-dispatch guard that prevents wiping an agent's unpushed work, the dirty/divergent quarantine path, and the Windows EPERM/EBUSY file-lock retry footgun.
43
49
 
44
50
  ## Operations
45
51
 
@@ -50,6 +56,7 @@ Operational runbooks for engine operators and fleet maintainers.
50
56
  - [human-vs-automated.md](human-vs-automated.md) — Quick reference table of which features humans start, run, decide, and recover, and the two human approval gates.
51
57
  - [kb-sweep.md](kb-sweep.md) — Knowledge-base sweep runbook: how `engine/kb-sweep.js` consolidates `notes/inbox/` into `knowledge/` and survives `minions restart`.
52
58
  - [onboarding.md](onboarding.md) — First-30-minutes walkthrough for a new operator: install, init, dispatch a first work item, watch it land.
59
+ - [preflight.md](preflight.md) — `minions doctor` and the lighter per-CLI `minions preflight` checks: what each non-self-explanatory row (permission bypass, runtime detection, drive-root, etc.) is asserting.
53
60
  - [security.md](security.md) — Threat model: single-user/loopback deployment assumptions, dashboard Origin gate, data-flow trust boundaries, secret handling, and known residual risks (CSRF sweep, prompt injection, log-redactor audit).
54
61
 
55
62
  ---
@@ -57,6 +57,46 @@ Skips PRs where `status !== "active"`.
57
57
 
58
58
  Inside `discoverFromPrs()`, ADO and GitHub projects first resolve their own provider poll gate (`adoPollEnabled` or `ghPollEnabled`). PR-derived automation is inert when that provider's polling is off, so cached build, vote, conflict, and comment state cannot trigger new dispatches. The shared dispatch toggles (`autoReviewPrs`, `autoReReviewPrs`, `autoFixReviewFeedback`, `autoFixHumanComments`, `autoFixBuilds`, and `autoFixConflicts`) apply to both providers. `evalLoop` gates the minion review loop: initial minion reviews, minion re-reviews, and minion review-feedback fixes. Human-feedback fixes are evaluated outside `evalLoop`. Conflict fixes are additionally gated by `!fixDispatched`, so an earlier successful human/review/build fix dispatch in the same PR discovery pass suppresses the conflict fix until a later pass.
59
59
 
60
+ **`autoReReviewPrs` gates both re-review paths (`P-e8b1c4d2`).** Re-reviews fire from two places: (1) the open-loop **discovery** path in `engine.js:discoverFromPrs` (PR shows `reviewStatus=waiting` after a fix push), and (2) the **closure-loop** in `engine/lifecycle.js:dispatchReReviewForFix` (a fix WI completes whose meta carries `addresses_review_wi`, queuing the next review against the same PR). The `engine.autoReReviewPrs` toggle is read at BOTH sites so flipping it OFF mutes the entire re-review cycle — not just the discovery sweep. Default ON; flip via Dashboard → Settings → Auto-fix & Review Loop, or `engine.autoReReviewPrs: false` in `config.json`.
61
+
62
+ **Hard-stop kill-switch (`pollingPaused`).** `engine.pollingPaused: true` is a master override that wins over both `adoPollEnabled` and `ghPollEnabled`. When ON, section 2.6/2.7 of the tick cycle skips `pollPrStatus` and `pollPrHumanComments` for both providers, and `discoverFromPrs` forces `pollEnabled=false` for every project so every per-PR auto-dispatch gate (`autoReviewPrs` / `autoFixBuilds` / `autoFixConflicts` / `autoFixReviewFeedback` / `autoFixHumanComments`) becomes inert. Reconciliation (the recovery sweep that follows section 2.7) is intentionally not gated. The engine logs `[engine] PR polling paused — …` once on the transition from unpaused → paused; routine PR poll log lines resume when cleared. Flip via Dashboard → Settings → Polling, or set `engine.pollingPaused: true` in `config.json`. Default OFF — fresh installs behave identically to before this knob existed.
63
+
64
+ **Hard-stop kill-switch (`autoFixPaused`).** `engine.autoFixPaused: true` is a narrower master override that wins over every auto-fix dispatch gate. When ON, `discoverFromPrs` forces `autoFixBuilds` / `autoFixConflicts` / `autoFixReviewFeedback` / `autoFixHumanComments` to false for every project, so no fix agent is auto-dispatched against any PR. Review dispatch (`autoReviewPrs` / `autoReReviewPrs`), PR status / human-comment polling, and reconciliation are intentionally not gated — operators can pause a fix-storm during an incident while still seeing fresh review verdicts and build status. The engine logs `[engine] auto-fix paused — …` once on the transition from unpaused → paused; routine discovery resumes when cleared. Flip via Dashboard → Settings → Auto-fix & Review Loop, or set `engine.autoFixPaused: true` in `config.json`. Default OFF — fresh installs behave identically to before this knob existed.
65
+
66
+ **Granular per-poller flags (`P-c4d8e1a3`).** The legacy `adoPollEnabled` / `ghPollEnabled` macros are bundle toggles that silence three axes at once (status, comments, reconcile) when set to `false`. To turn off only one axis, set the matching granular flag in `config.engine`:
67
+
68
+ | Flag | Default | Phase | Composes with `pollingPaused`? |
69
+ |------|---------|-------|--------------------------------|
70
+ | `adoPrStatusPollEnabled` | `true` | Section 2.6 (ADO `pollPrStatus`) | Yes — status polls honor the master killswitch |
71
+ | `adoPrCommentsPollEnabled` | `true` | Section 2.7 (ADO `pollPrHumanComments`) | Yes |
72
+ | `adoPrReconcileEnabled` | `true` | Section 2.7 tail (ADO `reconcilePrs`) | No — reconcile is a recovery sweep |
73
+ | `ghPrStatusPollEnabled` | `true` | Section 2.6 (GitHub `ghPollPrStatus`) | Yes |
74
+ | `ghPrCommentsPollEnabled` | `true` | Section 2.7 (GitHub `ghPollPrHumanComments`) | Yes |
75
+ | `ghPrReconcileEnabled` | `true` | Section 2.7 tail (GitHub `ghReconcilePrs`) | No — reconcile is a recovery sweep |
76
+ | `processPendingRebasesEnabled` | `true` | Section 2.6 tail (`processPendingRebases`) | No — rebase processor has no legacy macro |
77
+
78
+ Resolution order (`shared.resolvePollFlag(engineCfg, granularKey, legacyMacroKey)`): (1) granular flag explicitly set → wins; (2) legacy macro is `false` → propagates `false` to all three axes of that provider (including reconcile); (3) otherwise → `ENGINE_DEFAULTS[granularKey]` (`true`).
79
+
80
+ **Breaking change vs. legacy semantics:** Before P-c4d8e1a3, the legacy `adoPollEnabled: false` / `ghPollEnabled: false` silenced only status + comments polls, leaving reconcile running. The new `resolvePollFlag` contract propagates the legacy `false` to reconcile too. Operators who want the old behavior (status + comments OFF, reconcile ON) must now set `adoPrReconcileEnabled: true` / `ghPrReconcileEnabled: true` explicitly alongside the legacy macro. Default-config installs (no overrides) and installs that flip only the new granular flags are unaffected.
81
+
82
+ Flip via Dashboard → Settings → Polling → "Granular per-poller controls" collapsible, or set the keys directly in `config.engine`.
83
+
84
+ **Granular work-discovery flags (`P-d6f0a2b5`).** Each phase inside `engine.discoverWork()` is independently gateable, so operators can silence a single discovery source without disabling the other phases or touching the per-project `project.workSources.*.enabled` toggles. The two layers compose: a `false` at either the global flag or the per-project `workSources.*.enabled` level skips the matching call.
85
+
86
+ | Flag | Default | Phase inside `discoverWork()` |
87
+ |------|---------|--------------------------------|
88
+ | `prDiscoveryEnabled` | `true` | Per-project `discoverFromPrs(config, project)` — PR-derived fix / review / build-test work |
89
+ | `workItemsDiscoveryEnabled` | `true` | Per-project `discoverFromWorkItems(config, project)` — project-local `work-items.json` scan |
90
+ | `centralWorkDiscoveryEnabled` | `true` | `discoverCentralWorkItems(config)` — top-level project-agnostic `work-items.json` scan |
91
+ | `scheduledWorkDiscoveryEnabled` | `true` | `discoverScheduledWork(config)` — cron-style scheduled tasks + scheduled meetings |
92
+ | `planMaterializationEnabled` | `true` | `reconcilePrdStatuses(config)` + `materializePlansAsWorkItems(config)` (paired side-effect passes) |
93
+
94
+ Resolution is intentionally simpler than the granular per-poller flags above: each call site uses the inline check `if (config.engine?.<flag> !== false) { ... }`, so a missing or `true` value runs the phase and an explicit `false` skips it. There is no legacy macro to fall back to, and `shared.resolvePollFlag` is **not** used here. Default-config installs (no overrides) behave identically to before this knob existed.
95
+
96
+ Composition with per-project gates: setting `workItemsDiscoveryEnabled: false` skips `discoverFromWorkItems` even for projects whose own `workSources.workItems.enabled` is `true`; setting it back to `true` restores the per-project gate that `discoverFromWorkItems` already honors internally. Same pattern for `prDiscoveryEnabled` vs. `workSources.pullRequests.enabled`. `scheduledWorkDiscoveryEnabled: false` stops cron-style scheduled tasks from firing without touching the scheduler config; `planMaterializationEnabled: false` suppresses the `reconcilePrdStatuses` + `materializePlansAsWorkItems` pair atomically (useful during a long migration that must not auto-create work items).
97
+
98
+ Flip via Dashboard → Settings → Polling → "Granular work-discovery controls (P-d6f0a2b5)" collapsible, or set the keys directly in `config.engine`.
99
+
60
100
  ### Source 2: PRD Gap Analysis (via `materializePlansAsWorkItems`)
61
101
 
62
102
  PRD items flow through `materializePlansAsWorkItems()`, which scans `~/.minions/prd/*.json` for PRD files with `missing` / `updated` / `planned` items and creates work items in the target project's queue.