@yemi33/minions 0.1.2345 → 0.1.2347

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/engine/shared.js CHANGED
@@ -3193,6 +3193,24 @@ const ENGINE_DEFAULTS = {
3193
3193
  'build-reports': 14,
3194
3194
  'reviews': 30,
3195
3195
  },
3196
+ // W-mr973knu — structural consolidation for the KB sweep (PRIMARY balloon
3197
+ // fix; TTL above is the secondary safety net). The sweep groups boilerplate
3198
+ // transient-category notes by (category, agent, kind, time-bucket) — where
3199
+ // kind is the agent-failure `failureClass`, or a no-op / merge-conflict fix
3200
+ // report — and rolls any group of at least `minGroupSize` notes into a single
3201
+ // recoverable digest (`_digest-<agent>-<kind>-<period>.md`), archiving the
3202
+ // originals to knowledge/_swept/. Unlike TTL expiry this collapses RECENT
3203
+ // duplication too, so the KB shrinks continuously instead of only aging out.
3204
+ // Only `categories` listed here are consolidated; durable categories
3205
+ // (architecture, conventions) and per-agent memory are never touched.
3206
+ // config.engine.kbConsolidation is merged over these defaults; set
3207
+ // `enabled: false` or `categories: []` to disable.
3208
+ kbConsolidation: {
3209
+ enabled: true,
3210
+ categories: ['project-notes', 'build-reports', 'reviews'],
3211
+ minGroupSize: 5, // collapse a group only once it reaches this many near-duplicate notes
3212
+ periodDays: 7, // one digest per (agent, kind) per this many days
3213
+ },
3196
3214
  // W-mqtvnnj1000357fa — fleet-wide fallback for live-checkout auto-stash. When
3197
3215
  // true, a dirty live-checkout tree is `git stash push --include-untracked`'d
3198
3216
  // before dispatch instead of failing with FAILURE_CLASS.LIVE_CHECKOUT_DIRTY,
@@ -4698,12 +4716,19 @@ function mutateWatches(mutator) {
4698
4716
  function mutateMetrics(mutator) {
4699
4717
  const metricsPath = path.join(MINIONS_DIR, 'engine', 'metrics.json');
4700
4718
  const store = require('./metrics-store');
4719
+ // W-mradg74d000rfd45 — the JSON mirror is now written INSIDE
4720
+ // applyMetricsMutation's SQL transaction (before commit), not here
4721
+ // after the fact. Mirroring post-commit left a cross-process window
4722
+ // where a sibling process (dashboard vs. engine — separate Node
4723
+ // processes, each with its own in-memory mirror-hash cache) could
4724
+ // observe the newly committed SQL rows before the JSON file caught up
4725
+ // and read stale/missing metrics off the sidecar. See
4726
+ // metrics-store.js#applyMetricsMutation for the full rationale.
4701
4727
  const { wrote, result } = store.applyMetricsMutation((m) => {
4702
4728
  if (!m || typeof m !== 'object') m = {};
4703
4729
  return mutator(m) || m;
4704
- });
4730
+ }, { mirrorPath: metricsPath });
4705
4731
  if (wrote) {
4706
- try { store._mirrorJsonFromSql(metricsPath); } catch { /* mirror best-effort */ }
4707
4732
  try { require('./db-events').emitStateEvent('metrics'); } catch { /* optional */ }
4708
4733
  }
4709
4734
  return result;
@@ -4877,6 +4902,7 @@ const FAILURE_CLASS = {
4877
4902
  LIVE_CHECKOUT_MID_OPERATION: 'live-checkout-mid-operation', // P-a7f3c1d9 (live-checkout dispatch mode): spawnAgent could not switch/create the target branch in project.localPath because the operator tree is mid-operation — an in-progress merge/rebase/cherry-pick/bisect or a detached HEAD. Distinct from LIVE_CHECKOUT_DIRTY (uncommitted changes): here the tree may be clean but the branch op cannot proceed. Engine refuses to spawn (it never runs `git reset`/`git clean`/`git rebase --abort` against the operator tree). Non-retryable — operator must finish or abort the in-progress operation, or checkout a branch, before re-dispatch.
4878
4903
  LIVE_CHECKOUT_BLOB_FETCH: 'live-checkout-blob-fetch', // PL-live-checkout-reliability-hardening (live-checkout dispatch mode): `git checkout <existing-branch>` in project.localPath failed because the tree could not be materialized — on a Scalar/GVFS-managed ADO partial (blobless) clone, switching onto a branch whose tree differs from HEAD hydrates the changed paths' blobs through the GVFS cache server (`*.gvfscache.dev.azure.com`), a different endpoint than the main git remote that receives NO auth in the headless engine shell, so the fetch fails DETERMINISTICALLY. Distinct from LIVE_CHECKOUT_FAILED (transient) because retrying reproduces it identically — surfaced once as an operator-actionable refusal instead of retry-storming to the cap. Non-retryable — the operator hydrates the branch once with their own credentials (`git checkout <branch>` interactively, or `scalar prefetch` / `git -C <repo> fetch`), then re-dispatches. The engine NEVER forces, resets, cleans, or stashes the operator tree, and best-effort switches HEAD back to the original ref so the tree is not stranded half-populated.
4879
4904
  LIVE_CHECKOUT_WORKTREE_CONFLICT: 'live-checkout-worktree-conflict', // W-mr28h2j2000y0de1 (live-checkout dispatch mode): `git checkout <existing-branch>` in project.localPath failed because that exact branch is ALSO checked out in a SECOND worktree elsewhere (a leftover from a prior isolated-worktree dispatch, a manually-created worktree, or a stale worktree left by a checkoutMode change). git refuses deterministically with `fatal: '<branch>' is already used by worktree at '<path>'`. Distinct from LIVE_CHECKOUT_FAILED (transient) because this is a STRUCTURAL conflict — it does NOT clear on retry, ever, until a human or the engine removes/reassigns the other worktree; retrying just reproduces it identically and burns maxRetries. Non-retryable — the operator either `git worktree remove <path>` (freeing the branch) if that worktree is stale, or finishes/commits/pushes from it directly if it holds real WIP. The engine NEVER forces, resets, cleans, or stashes the operator tree, and best-effort switches HEAD back to the original ref so the tree is not stranded.
4905
+ LIVE_CHECKOUT_STALE_BASE: 'live-checkout-stale-base', // W-mr98op8w000ma4ad (live-checkout dispatch mode, STALE-BASE GUARD): spawnAgent was about to fork a NEW branch off the operator's current HEAD while HEAD sits on the project base ref (mainRef), but the LOCAL mainRef tip has DIVERGED from origin/<mainRef> with unpushed commit(s) (`git rev-list --count origin/<mainRef>..<mainRef>` > 0). Forking would silently inherit that COMMITTED contamination into the new branch and its PR — the exact scope-contamination class first seen on ADO PR 5411214 and recurring on AB#12016662 / ADO PR 5419146. Survives the dirty check (contamination is committed, not uncommitted) and cannot be caught by a headBranch !== mainRef check (headBranch IS mainRef here). Detected WITHOUT a fetch (issue #226) via the existing origin/<mainRef> remote-tracking ref. Distinct from LIVE_CHECKOUT_FAILED (transient) because the contaminated local base does NOT change on retry — it reproduces identically until a human reconciles the local base branch. Non-retryable — the engine NEVER resets/cleans the operator's local mainRef; the operator must reconcile it (push/reset/rebase their own way), then re-dispatch. The guard SKIPS (proceeds normally) when HEAD is not on mainRef, when no local origin/<mainRef> ref exists (local-only/no-remote repo), or when the divergence count cannot be computed.
4880
4906
  LIVE_CHECKOUT_WRONG_BASE: 'live-checkout-wrong-base', // W-mr3lunnq000o9f41 (live-checkout dispatch mode): a NEW-branch fork was requested but the operator's checkout HEAD is on an unrelated topic/feature branch, NOT the project base (mainRef), and prepareLiveCheckout could not switch to a LOCAL base ref (no `refs/heads/<mainRef>`, or the local checkout itself failed). Forking off the stale HEAD would silently bake every commit already on that branch into the new branch AND its PR — committed-history scope contamination that survives the dirty/mid-operation preflights (the ADO PR 5411214 incident: ~22 unrelated OCM files, author-disavowed). The engine never fetches origin/<mainRef> in live mode (issue #226) so it cannot self-heal a missing local base. Non-retryable — the operator must `git checkout <mainRef>` in the operator checkout before re-dispatch. Does NOT fire for PR-targeted fixes or shared-branch continuations (those pass allowNonMainBase, intentionally continuing a non-main branch), nor when HEAD is already on mainRef, nor when a local mainRef ref exists (the guard switches to it and forks cleanly). OPT-IN AUTO-RECOVERY (`liveCheckoutAutoBaseRepair`, per-project > fleet-wide, default OFF): when enabled AND a `refs/remotes/origin/<mainRef>` remote-tracking ref exists, the guard materializes a local `<mainRef>` branch from it (a LOCAL ref op — still no `git fetch`, issue #226 preserved; origin/<mainRef> IS the base so no contamination) and forks cleanly instead of failing — eliminating the manual `git checkout <mainRef>` step. Still fails closed when neither a local base nor an origin-tracking base ref exists, or when the base checkout itself fails/hangs.
4881
4907
  INVALID_WORKDIR: 'invalid-workdir', // P-714ef144: dispatch carried a meta.workdir override that failed validation — non-string, absolute path, drive-letter prefix, null byte, ".." segment, or post-resolve containment escape against project.localPath / worktree root. Engine refuses to spawn (the subpath would either be unreachable on disk or point outside the operator's allowed surface). Non-retryable — operator must fix the WI's meta.workdir before re-dispatch. Inbox alert lists the offending value + the resolved-vs-base mismatch.
4882
4908
  MODEL_UNAVAILABLE: 'model-unavailable', // W-mpg6isvy000xca4d: requested model returned overloaded_error / 503 / service_unavailable. Retriable — engine swaps in the runtime-appropriate fallback model on next spawn (Claude leans on --fallback-model already plumbed; Copilot overrides --model with engine.copilotFallbackModel).
package/engine.js CHANGED
@@ -2934,6 +2934,83 @@ async function spawnAgent(dispatchItem, config) {
2934
2934
  cleanupTempAgent(agentId);
2935
2935
  return null;
2936
2936
  }
2937
+ // W-mr98op8w000ma4ad: STALE-BASE GUARD refusal. prepareLiveCheckout was about
2938
+ // to fork a NEW branch off the operator's current HEAD while HEAD sits on the
2939
+ // project base ref (mainRef), but the LOCAL mainRef tip has diverged from
2940
+ // origin/<mainRef> with unpushed commit(s). Forking would silently bake that
2941
+ // committed contamination into the new branch and its PR (the scope-leak class
2942
+ // first seen on ADO PR 5411214, recurring on AB#12016662 / ADO PR 5419146).
2943
+ // Deterministic — a contaminated local base reproduces identically on retry —
2944
+ // so it gets its own non-retryable FAILURE_CLASS instead of LIVE_CHECKOUT_FAILED's
2945
+ // bounded retry-storm. The engine NEVER resets/cleans the operator's local
2946
+ // mainRef; the operator must reconcile it themselves, then re-dispatch.
2947
+ if (_liveResult && _liveResult.ok === false && _liveResult.reason === 'stale-main') {
2948
+ const _staleMainRef = typeof _liveResult.mainRef === 'string' && _liveResult.mainRef ? _liveResult.mainRef : 'main';
2949
+ const _staleAhead = Number.isFinite(_liveResult.ahead) ? _liveResult.ahead : 0;
2950
+ const _localSha = typeof _liveResult.localMainSha === 'string' && _liveResult.localMainSha ? _liveResult.localMainSha : '(unknown)';
2951
+ const _originSha = typeof _liveResult.originMainSha === 'string' && _liveResult.originMainSha ? _liveResult.originMainSha : '(unknown)';
2952
+ const _alertBody = [
2953
+ '# Live-checkout blocked: local base branch has diverged (stale/contaminated base)',
2954
+ '',
2955
+ `**Project:** ${project.name || '(unknown)'}`,
2956
+ `**Local path:** ${cwd}`,
2957
+ `**New branch (refused):** ${branchName}`,
2958
+ `**Base ref:** ${_staleMainRef}`,
2959
+ `**Local ${_staleMainRef}:** ${_localSha}`,
2960
+ `**origin/${_staleMainRef}:** ${_originSha}`,
2961
+ `**Unpushed commits on local ${_staleMainRef}:** ${_staleAhead}`,
2962
+ `**Work item:** ${_wiIdForAlert}`,
2963
+ `**Dispatch:** ${id}`,
2964
+ '',
2965
+ `The engine refused to fork \`${branchName}\` off local \`${_staleMainRef}\` because your local \`${_staleMainRef}\` is **${_staleAhead} commit(s) ahead of \`origin/${_staleMainRef}\`**. Those commits were never pushed — forking a new branch (and its PR) off this base would silently inherit them, contaminating an otherwise-clean change with unrelated commits. The working tree was clean (no uncommitted changes), so the dirty check could not catch this: the contamination is COMMITTED. Your tree was left untouched — the engine never resets, cleans, or rebases your \`${_staleMainRef}\`.`,
2966
+ '',
2967
+ '## Recovery',
2968
+ '',
2969
+ `Reconcile your local \`${_staleMainRef}\` so it no longer carries unpushed commits, then re-dispatch. Pick ONE, depending on whether those commits are real work:`,
2970
+ '',
2971
+ `1. **If the extra commits are real work you want to keep** — move them onto their own branch, then reset \`${_staleMainRef}\` to the remote:`,
2972
+ '',
2973
+ '```',
2974
+ `git -C "${cwd}" branch salvage-${_staleMainRef} ${_staleMainRef}`,
2975
+ `git -C "${cwd}" checkout ${_staleMainRef}`,
2976
+ `git -C "${cwd}" reset --hard origin/${_staleMainRef}`,
2977
+ '```',
2978
+ '',
2979
+ `2. **If the extra commits are leftover junk** (e.g. a prior dispatch's abandoned WIP) — discard them by resetting \`${_staleMainRef}\` to the remote (this DISCARDS those commits):`,
2980
+ '',
2981
+ '```',
2982
+ `git -C "${cwd}" checkout ${_staleMainRef}`,
2983
+ `git -C "${cwd}" reset --hard origin/${_staleMainRef}`,
2984
+ '```',
2985
+ '',
2986
+ `The salvage branch in option 1 keeps the commits recoverable; both leave \`${_staleMainRef}\` matching \`origin/${_staleMainRef}\` so the next dispatch forks off a clean base.`,
2987
+ ].join('\n');
2988
+ try { writeInboxAlert(`live-checkout-stale-base-${_wiIdForAlert}`, _alertBody); }
2989
+ catch (e) { log('warn', `live-checkout: writeInboxAlert failed: ${e.message}`); }
2990
+ try {
2991
+ const _wiPath = resolveWorkItemPath(dispatchItem.meta);
2992
+ if (_wiPath && dispatchItem.meta?.item?.id) {
2993
+ mutateJsonFileLocked(_wiPath, (data) => {
2994
+ if (!Array.isArray(data)) return data;
2995
+ const wi = data.find(i => i && i.id === dispatchItem.meta.item.id);
2996
+ if (wi) wi._pendingReason = 'live_checkout_stale_base';
2997
+ return data;
2998
+ });
2999
+ }
3000
+ } catch (e) { log('warn', `live-checkout: failed to stamp _pendingReason: ${e.message}`); }
3001
+ const _shortMsg = `live-checkout blocked: local ${_staleMainRef} is ${_staleAhead} commit(s) ahead of origin/${_staleMainRef} (would contaminate ${branchName})`;
3002
+ log('error', `spawnAgent: ${_shortMsg}`);
3003
+ _cleanupPromptFiles();
3004
+ completeDispatch(
3005
+ id,
3006
+ DISPATCH_RESULT.ERROR,
3007
+ _shortMsg.slice(0, 800),
3008
+ `Live-checkout STALE-BASE GUARD: forking ${branchName} off local ${_staleMainRef} would inherit ${_staleAhead} unpushed commit(s) not on origin/${_staleMainRef}. Deterministic — operator must reconcile the local base branch (the engine never resets it) before re-dispatch. Retrying is provably useless.`,
3009
+ { failureClass: FAILURE_CLASS.LIVE_CHECKOUT_STALE_BASE, agentRetryable: false },
3010
+ );
3011
+ cleanupTempAgent(agentId);
3012
+ return null;
3013
+ }
2937
3014
  log('info', `live-checkout: ${_liveResult.created ? 'created' : 'switched to'} branch ${branchName} in ${cwd} (in-place; no worktree)`);
2938
3015
  // PL-live-checkout-reliability-hardening: a clean spawn clears the two-strike
2939
3016
  // dirty counter so a project that was transiently dirty once isn't treated as
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2345",
3
+ "version": "0.1.2347",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "minions": "bin/minions.js"