@yemi33/minions 0.1.2288 → 0.1.2290

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.js CHANGED
@@ -2380,6 +2380,7 @@ async function spawnAgent(dispatchItem, config) {
2380
2380
  gitOpts: _gitOpts,
2381
2381
  dispatchId: id,
2382
2382
  wiId: _wiIdForAlert,
2383
+ skipDirtyCheck: !!project.skipLiveCheckoutDirtyCheck,
2383
2384
  log: (msg, lvl) => log(lvl || 'info', msg),
2384
2385
  });
2385
2386
  } catch (liveErr) {
@@ -5283,6 +5284,7 @@ async function spawnAgent(dispatchItem, config) {
5283
5284
  isTerminalFailure: effectiveResult !== DISPATCH_RESULT.SUCCESS,
5284
5285
  resultLabel: errorReason || effectiveResult,
5285
5286
  gitOpts: _gitOpts,
5287
+ skipDirtyCheck: !!project?.skipLiveCheckoutDirtyCheck,
5286
5288
  log,
5287
5289
  writeInboxAlert,
5288
5290
  });
@@ -5792,6 +5794,63 @@ function reservePrdFilename(prdDir, baseFileName, content = '') {
5792
5794
  throw new Error('Could not reserve unique PRD filename after 100 attempts');
5793
5795
  }
5794
5796
 
5797
+ // Fleet branches the engine/CC create. A worktree-mode operator checkout should
5798
+ // NEVER sit on one of these (or any branch); when it does, a flow ran raw
5799
+ // `git checkout`/`merge` in the live checkout instead of a worktree.
5800
+ const _FLEET_BRANCH_RE = /^(cc-pr|backport|e2e|verify|sched-|work)\//;
5801
+
5802
+ // Self-heal: a worktree-mode project's operator checkout (project.localPath) must
5803
+ // always sit on its mainBranch — agents use isolated worktrees, CC Create-PR uses
5804
+ // prepare-create-pr-worktree. But fleet flows (CC raw git, backport, sync)
5805
+ // sometimes run `git checkout <branch>` + `git merge` THERE despite the rules,
5806
+ // leaving it stuck on a feature branch (often mid-conflict) — which restores
5807
+ // git-tracked dead PRDs and pollutes the operator's main, then the materializer
5808
+ // re-runs finished work. Detect + restore. Conservative: only acts on a stuck
5809
+ // merge/rebase OR a recognizably fleet-created branch, so a human's deliberate
5810
+ // `git checkout my-feature` in their checkout is left alone. Branch commits are
5811
+ // preserved on their ref (reset --hard only drops the uncommitted hijack state).
5812
+ function restoreHijackedOperatorCheckouts(config) {
5813
+ if (config?.engine?.autoRestoreOperatorCheckout === false) return;
5814
+ let projects;
5815
+ try { projects = getProjects(config); } catch { return; }
5816
+ for (const project of projects) {
5817
+ try {
5818
+ const localPath = project?.localPath;
5819
+ if (!localPath || !fs.existsSync(localPath)) continue;
5820
+ if (shared.resolveCheckoutMode(project) === shared.CHECKOUT_MODES.LIVE) continue; // live-mode: feature branch is intentional
5821
+ const main = project.mainBranch || 'main';
5822
+ if (String(execSilent('git rev-parse --is-inside-work-tree', { cwd: localPath }) || '').trim() !== 'true') continue;
5823
+ const cur = String(execSilent('git rev-parse --abbrev-ref HEAD', { cwd: localPath }) || '').trim();
5824
+ if (!cur || cur === 'HEAD' || cur === main) continue; // on main or detached — fine
5825
+ const gitDir = String(execSilent('git rev-parse --git-dir', { cwd: localPath }) || '').trim();
5826
+ const absGitDir = gitDir ? (path.isAbsolute(gitDir) ? gitDir : path.join(localPath, gitDir)) : path.join(localPath, '.git');
5827
+ const mergeStuck = fs.existsSync(path.join(absGitDir, 'MERGE_HEAD'));
5828
+ const rebaseStuck = fs.existsSync(path.join(absGitDir, 'rebase-merge')) || fs.existsSync(path.join(absGitDir, 'rebase-apply'));
5829
+ if (!mergeStuck && !rebaseStuck && !_FLEET_BRANCH_RE.test(cur)) continue; // likely a human's deliberate checkout — leave it
5830
+ const why = mergeStuck ? ' (stuck merge)' : rebaseStuck ? ' (stuck rebase)' : ' (fleet branch in live checkout)';
5831
+ log('warn', `Operator checkout for ${project.name} is on '${cur}'${why}; worktree-mode requires ${main} — restoring`);
5832
+ const opts = { cwd: localPath };
5833
+ if (mergeStuck) execSilent('git merge --abort', opts);
5834
+ if (rebaseStuck) execSilent('git rebase --abort', opts);
5835
+ execSilent('git reset --hard HEAD', opts); // drop uncommitted hijack state; branch commits stay on the ref
5836
+ try { shared.validateGitRef(main); } catch (ve) { throw new Error(`restoreHijackedOperatorCheckouts: invalid mainBranch ${JSON.stringify(main)}: ${ve.message}`); }
5837
+ shared.shellSafeGitSync(['checkout', main], opts);
5838
+ const now = String(execSilent('git rev-parse --abbrev-ref HEAD', { cwd: localPath }) || '').trim();
5839
+ if (now === main) {
5840
+ try {
5841
+ writeInboxAlert(`operator-checkout-restored-${project.name}`,
5842
+ `Operator checkout ${localPath} was hijacked onto '${cur}'${why} and restored to ${main}. ` +
5843
+ 'A fleet flow ran git checkout/merge in the LIVE operator checkout instead of an isolated worktree ' +
5844
+ '(playbooks/shared-rules.md: "Do NOT checkout branches in the main working tree — use worktrees"). ' +
5845
+ `The '${cur}' branch commits are preserved on its ref.`);
5846
+ } catch { /* alert is best-effort */ }
5847
+ } else {
5848
+ log('warn', `Operator checkout restore for ${project.name} did not reach ${main} (now '${now}') — manual intervention needed`);
5849
+ }
5850
+ } catch (e) { log('warn', `restoreHijackedOperatorCheckouts ${project?.name || '?'}: ${e?.message || e}`); }
5851
+ }
5852
+ }
5853
+
5795
5854
  function materializePlansAsWorkItems(config) {
5796
5855
  if (!fs.existsSync(PRD_DIR)) { try { fs.mkdirSync(PRD_DIR, { recursive: true }); } catch (e) { log('warn', 'create PRD directory: ' + e.message); } }
5797
5856
  const writePrdLocked = (fileName, data) => {
@@ -6027,6 +6086,19 @@ function materializePlansAsWorkItems(config) {
6027
6086
  // (W-mouptdh1000h9f39).
6028
6087
  let plan = safeJsonNoRestore(path.join(PRD_DIR, file));
6029
6088
  if (!plan?.missing_features) continue;
6089
+ // Resurrection-re-execution guard ("old PRDs came back"): never (re)materialize
6090
+ // a defunct PRD — archived, or completed with its source plan gone from plans/.
6091
+ // Such a PRD can linger in prd/ with stale `missing`-status items; without
6092
+ // this, the materializer re-creates and re-runs already-finished work (it only
6093
+ // filters per-item by PRD_MATERIALIZABLE, which a stale 'missing' item passes).
6094
+ if (shared.isDefunctPrd(plan, PLANS_DIR)) {
6095
+ if (!_defunctPrdSkipLogged.has(file)) {
6096
+ log('info', `Skipping defunct PRD ${file} (archived or completed with archived/removed source plan) — not re-materializing`);
6097
+ _defunctPrdSkipLogged.add(file);
6098
+ }
6099
+ continue;
6100
+ }
6101
+ _defunctPrdSkipLogged.delete(file);
6030
6102
  ({ fileName: file, plan } = enforceDeclaredPlanProject(file, plan));
6031
6103
 
6032
6104
  // ID collision prevention: remap sequential IDs (P-001, P-002) to globally unique P-<uid> IDs.
@@ -8833,7 +8905,7 @@ function discoverCentralWorkItems(config) {
8833
8905
  const prdFiles = safeReadDir(PRD_DIR).filter(f => f.endsWith('.json'));
8834
8906
  for (const pf of prdFiles) {
8835
8907
  const prd = safeJson(path.join(PRD_DIR, pf));
8836
- if (planKey && prd && path.basename(String(prd.source_plan || '')) === planKey) {
8908
+ if (planKey && prd && !shared.isPrdArchived(prd) && shared.prdMatchesSourcePlan(prd.source_plan, planKey)) {
8837
8909
  prdFilename = pf;
8838
8910
  try { vars.existing_prd_json = fs.readFileSync(path.join(PRD_DIR, pf), 'utf8'); } catch (_) { /* ignore */ }
8839
8911
  log('info', `plan-to-prd: reusing existing PRD "${pf}" for plan "${item.planFile}" (#884)`);
@@ -9129,21 +9201,25 @@ async function discoverWork(config) {
9129
9201
  // (W-mouptdh1000h9f39). Idempotent and fast — readdir + existsSync.
9130
9202
  sweepStaleArchivedPrdBackups(prdDir, prdArchiveDir);
9131
9203
 
9132
- // Pass 2 — Existing orphan ghost-PRD purge + completion sweep.
9204
+ // Pass 2 — completion sweep. (The ghost-PRD purge that used to live here
9205
+ // was a REDUNDANT + UNSAFE duplicate of the plan-completion scan's
9206
+ // version: it unlinked prd/<f> whenever prd/archive/<f> existed, with NO
9207
+ // `_isGhostPrdRestore` identity check — the exact footgun #7 (#496)
9208
+ // hazard of deleting a legitimately re-opened PRD that shares an archived
9209
+ // basename. The plan-completion scan does the same purge safely (identity-
9210
+ // checked + neutralize); here the isDefunctPrd guard below already skips
9211
+ // archived/defunct PRDs, so this loop never re-runs completion on a ghost.)
9133
9212
  for (const f of fs.readdirSync(prdDir).filter(f => f.endsWith('.json'))) {
9134
9213
  if (completedPlanCache.has(f)) continue;
9135
- if (fs.existsSync(path.join(prdArchiveDir, f))) {
9136
- // Orphaned backup restore — plan is already archived. Purge the ghost copy.
9137
- try { fs.unlinkSync(path.join(prdDir, f)); } catch { }
9138
- try { fs.unlinkSync(path.join(prdDir, f + '.backup')); } catch { }
9139
- completedPlanCache.add(f);
9140
- continue;
9141
- }
9142
9214
  // safeJsonNoRestore — defense in depth: if the file vanished between
9143
9215
  // readdir and read (e.g. concurrent archive), do not resurrect it
9144
9216
  // from a stale .backup sidecar (W-mouptdh1000h9f39).
9145
9217
  const plan = safeJsonNoRestore(path.join(prdDir, f));
9146
9218
  if (!plan?.missing_features) continue;
9219
+ // Resurrection guard (mirror of completion-scan-B / #523): never re-run
9220
+ // completion (which re-creates the verify gate) for a defunct PRD —
9221
+ // archived, or completed with its plan gone from plans/.
9222
+ if (shared.isDefunctPrd(plan, PLANS_DIR)) { completedPlanCache.add(f); continue; }
9147
9223
  // A completed PRD may still be missing its aggregate verify WI — e.g. it
9148
9224
  // landed on disk pre-completed via the plan-to-prd / pipeline path, so no
9149
9225
  // agent-completion event ever drove checkPlanCompletion (W-mqk5ld3p). The
@@ -9335,6 +9411,9 @@ let tickCount = 0;
9335
9411
  // In-memory cache of plan filenames confirmed completed — avoids redundant
9336
9412
  // checkPlanCompletion calls. Cleared automatically on engine restart.
9337
9413
  const completedPlanCache = new Set();
9414
+ // Dedupe the "Skipping defunct PRD" materializer log so a lingering defunct PRD
9415
+ // doesn't spam one line per discovery tick; cleared when the file stops being defunct.
9416
+ const _defunctPrdSkipLogged = new Set();
9338
9417
  // Filenames where a live PRD shares a basename with an archived PRD but diverges
9339
9418
  // (different source_plan / introduces new work). We refuse to auto-purge those
9340
9419
  // and warn once instead of every tick. (RC4 — footgun #7 collision protection.)
@@ -9795,6 +9874,12 @@ async function tickInner() {
9795
9874
  }
9796
9875
  }
9797
9876
  const plan = safeJson(path.join(PRD_DIR, file));
9877
+ // Resurrection-re-execution guard: a defunct PRD (archived, or completed
9878
+ // with its source plan gone from plans/) must NOT have its verify gate
9879
+ // re-created. This scan deliberately re-runs completion for completed
9880
+ // PRDs (to build the aggregate verify task), which is exactly how a
9881
+ // resurrected/lingering completed PRD spawned spurious verify WIs.
9882
+ if (shared.isDefunctPrd(plan, PLANS_DIR)) { completedPlanCache.add(file); continue; }
9798
9883
  if (plan && plan.missing_features) {
9799
9884
  // Run for completed PRDs too — a pre-completed plan (flag already set,
9800
9885
  // items pre-marked done) still needs its aggregate verify gate created.
@@ -10413,6 +10498,9 @@ async function tickInner() {
10413
10498
  const maxC = resolveMaxConcurrent(config);
10414
10499
  setTempBudget(Math.max(0, maxC - activeCountPre));
10415
10500
  }
10501
+ // Self-heal a hijacked operator checkout BEFORE discovery/materialization, so a
10502
+ // dead PRD restored by a stray `git checkout` can't be re-materialized this tick.
10503
+ try { restoreHijackedOperatorCheckouts(config); } catch (e) { log('warn', 'restoreHijackedOperatorCheckouts: ' + e.message); }
10416
10504
  let discoveryOk = true;
10417
10505
  try { await withTickTimeout(() => discoverWork(config), 'discoverWork', tickOpTimeoutMs); } catch (e) { log('warn', 'discoverWork: ' + e.message); discoveryOk = false; }
10418
10506
  if (_isTickStale(myGeneration)) return;
@@ -10514,6 +10602,7 @@ module.exports = {
10514
10602
  materializePlansAsWorkItems,
10515
10603
  materializeSpecsAsWorkItems, // exported for testing (P-f7-git-log)
10516
10604
  reservePrdFilename, // exported for testing (P-9b7e5d3c)
10605
+ restoreHijackedOperatorCheckouts, // exported for testing — operator-checkout self-heal
10517
10606
  sweepStaleArchivedPrdBackups, // exported for testing
10518
10607
  _isGhostPrdRestore, // exported for testing (RC4 — ghost-purge identity check)
10519
10608
 
@@ -10577,7 +10666,54 @@ module.exports = {
10577
10666
 
10578
10667
  // ─── Entrypoint ─────────────────────────────────────────────────────────────
10579
10668
 
10669
+ // Source-level defense against agents committing prd/*.json + plans/*.md
10670
+ // (gitignored engine work-plan state) as if they were deliverables — a tracked
10671
+ // PRD resurrects + re-executes on the next checkout ("old PRDs came back"). The
10672
+ // hook is GENERATED at runtime into the repo's git hooks dir (NOT committed —
10673
+ // opg push-protection blocks committed hooks, and committing one would itself be
10674
+ // the kind of supply-chain footgun we're guarding against). The git common hooks
10675
+ // dir is shared across all worktrees, so the operator checkout AND every agent
10676
+ // worktree are covered. Idempotent; never clobbers a foreign pre-commit hook.
10677
+ const PRD_PRECOMMIT_HOOK = [
10678
+ '#!/bin/sh',
10679
+ '# Minions guard (engine-generated; not committed). prd/*.json + plans/*.md are',
10680
+ '# gitignored runtime state and must NEVER be committed — a tracked PRD/plan is',
10681
+ '# resurrected by git checkout/reset/merge and re-runs finished work. See',
10682
+ '# engine.js ensureGitHooksInstalled. Bypass (rarely): git commit --no-verify.',
10683
+ "blocked=$(git diff --cached --name-only --diff-filter=AM | grep -E '^prd/.*\\.json$|^plans/.*\\.md$' || true)",
10684
+ 'if [ -n "$blocked" ]; then',
10685
+ ' echo "" 1>&2',
10686
+ ' echo " Refusing to commit gitignored Minions runtime state (prd/ + plans/ guard):" 1>&2',
10687
+ ' echo "$blocked" | sed \'s/^/ /\' 1>&2',
10688
+ ' echo " Internal work-plan artifacts, not deliverables. Unstage: git reset HEAD -- prd/ plans/" 1>&2',
10689
+ ' echo " (genuinely intentional? bypass with: git commit --no-verify)" 1>&2',
10690
+ ' exit 1',
10691
+ 'fi',
10692
+ 'exit 0',
10693
+ '',
10694
+ ].join('\n');
10695
+
10696
+ function ensureGitHooksInstalled() {
10697
+ try {
10698
+ const inWorkTree = String(shared.execSilent('git rev-parse --is-inside-work-tree', { cwd: MINIONS_DIR }) || '').trim();
10699
+ if (inWorkTree !== 'true') return;
10700
+ const hooksRel = String(shared.execSilent('git rev-parse --git-path hooks', { cwd: MINIONS_DIR }) || '').trim();
10701
+ if (!hooksRel) return;
10702
+ const hooksDir = path.isAbsolute(hooksRel) ? hooksRel : path.join(MINIONS_DIR, hooksRel);
10703
+ const hookFile = path.join(hooksDir, 'pre-commit');
10704
+ let existing = null;
10705
+ try { existing = fs.readFileSync(hookFile, 'utf8'); } catch { /* absent */ }
10706
+ if (existing === PRD_PRECOMMIT_HOOK) return; // already current
10707
+ if (existing && !existing.includes('Minions guard')) return; // respect a foreign hook
10708
+ fs.mkdirSync(hooksDir, { recursive: true });
10709
+ fs.writeFileSync(hookFile, PRD_PRECOMMIT_HOOK);
10710
+ try { fs.chmodSync(hookFile, 0o755); } catch { /* non-posix */ }
10711
+ log('info', `Installed git pre-commit guard (${hookFile}) — blocks committing prd/*.json + plans/*.md`);
10712
+ } catch { /* best-effort */ }
10713
+ }
10714
+
10580
10715
  if (require.main === module) {
10716
+ try { ensureGitHooksInstalled(); } catch { /* best-effort */ }
10581
10717
  // Every copilot this process spawns — agent dispatches AND internal
10582
10718
  // `llm.callLLM` calls (consolidation, classification, meetings, …) — inherits
10583
10719
  // this process's COPILOT_HOME, so point it at the seeded, MCP-filtered home.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2288",
3
+ "version": "0.1.2290",
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"
@@ -191,10 +191,11 @@ If the task description contains `mode: diff-aware-update`, you are updating an
191
191
  - Write ONLY the single `.json` PRD file to `{{team_root}}/prd/` — do NOT write any `.md` files there
192
192
  - Do NOT create a git branch, worktree, or PR — this playbook writes minions-internal state only
193
193
  - Do NOT modify any files in the project repo
194
+ - **NEVER run `git add prd/` or `git add -f prd/` — PRD files are gitignored runtime artifacts (`prd/*.json` is in `.gitignore`). Force-adding them exposes internal work plans in the public repo diff. A CI guard (`test/unit/prd-not-git-tracked.test.js`) will fail the PR if you do this, and you will be required to run `git rm --cached prd/<file>.json` to undo it.**
194
195
  - The engine will dispatch implementation agents automatically once the JSON file exists
195
196
  - For `shared-branch`: agents commit to a single branch — one PR is created automatically when all items are done
196
197
  - For `parallel`: each agent creates its own branch and PR
197
198
 
198
199
  ## When to Stop
199
200
 
200
- Your task is complete once you have written the PRD JSON file to `{{team_root}}/prd/{{prd_filename}}`. The engine will detect the file and begin dispatching work. Do NOT create branches, PRs, or modify project code. Stop after writing the JSON file.
201
+ Your task is complete once you have written the PRD JSON file to `{{team_root}}/prd/{{prd_filename}}`. The engine will detect the file and begin dispatching work. Do NOT run any git commands. Do NOT create branches, PRs, or modify project code. Stop after writing the JSON file.
@@ -86,6 +86,7 @@ This task is part of a **shared-branch plan** — every plan item shares the bra
86
86
  - Do NOT write to `agents/*/status.json` — the engine manages your status automatically.
87
87
  - Do NOT remove worktrees — the engine handles cleanup automatically.
88
88
  - Do NOT checkout branches in the main working tree — use worktrees or `git diff`/`git show`.
89
+ - **NEVER run `git checkout <branch>` / `git switch` / `git merge` / `git rebase` / `git cherry-pick` in a project's operator checkout (`project.localPath`, e.g. the configured repo root).** That tree must stay on its main branch. For ANY branch operation — backport, sync, cross-branch merge, "create a PR off branch X" — create an isolated worktree first: `git -C <localPath> worktree add <scratch-path> <base>`, do all checkout/merge/commit/push **inside that worktree**, then `git -C <localPath> worktree remove <scratch-path>`. Running these in the live checkout pollutes the operator's main, leaves stuck-merge conflicts, and resurrects gitignored runtime state (the "old PRDs came back" incident). The engine auto-restores a hijacked operator checkout and files an inbox alert, but do not rely on that — use a worktree.
89
90
  - Treat `notes/inbox/` writes as success artifacts only. If the task fails, is blocked, is cancelled, or ends partial, do **not** create an inbox note; report the failure in your final response, completion block, PR/work-item comment, or other task-specific failure channel instead.
90
91
  - Read `notes.md` for team rules and decisions before starting.
91
92
  - **Check team memory first, then look outside.** Before researching from scratch, check what the team already knows — in this order: