@yemi33/minions 0.1.2186 → 0.1.2188

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/README.md CHANGED
@@ -334,7 +334,7 @@ For GitHub repos, install and authenticate the [GitHub CLI](https://cli.github.c
334
334
 
335
335
  ### Azure DevOps Users
336
336
 
337
- For the best experience with ADO repos, install the [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) with the Azure DevOps extension. Agents should use the `az` CLI first for Azure DevOps operations such as PR creation, PR lookup, comments, reviewers, work items, and pipelines. Use the Azure DevOps MCP fallback only when `az` is unavailable in the environment or insufficient for a specific action.
337
+ For the best experience with ADO repos, install the [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) with the Azure DevOps extension. Agents should use the `az` CLI first for Azure DevOps operations such as PR creation, PR lookup, comments, reviewers, work items, and pipelines. If `az` is unavailable or insufficient for a specific action (e.g. PR comment threads on an older extension), agents fall back to the Azure DevOps REST API (`_apis/git/...`) directly, using a token from `az account get-access-token` no MCP server required.
338
338
 
339
339
  ```bash
340
340
  # Install Azure CLI
@@ -348,8 +348,6 @@ az login
348
348
  az devops configure --defaults organization=https://dev.azure.com/YOUR_ORG project=YOUR_PROJECT
349
349
  ```
350
350
 
351
- Optionally add the [Azure DevOps MCP server](https://github.com/microsoft/azure-devops-mcp) to your Claude Code settings (`~/.claude.json`) as a fallback. Agents inherit it on next spawn — no sync step needed.
352
-
353
351
  ## Work Items
354
352
 
355
353
  All work items use the shared `playbooks/work-item.md` template, which provides consistent branch naming, worktree workflow, PR creation steps, and status tracking.
@@ -676,7 +674,7 @@ To move to a new machine: `npm install -g @yemi33/minions && minions init --forc
676
674
  watches.js watch-actions.js
677
675
  # Repo-host integrations
678
676
  github.js issues.js
679
- ado.js ado-token.js ado-mcp-wrapper.js
677
+ ado.js ado-token.js
680
678
  ado-status.js check-status.js
681
679
  # Runtime state (generated, gitignored)
682
680
  control.json <- running/paused/stopped
package/dashboard.js CHANGED
@@ -8130,25 +8130,69 @@ What would you like to discuss or change? When you're happy, say "approve" and I
8130
8130
  let _docAbort = null;
8131
8131
  let _docStreamEnded = false;
8132
8132
  let _docHeartbeatTimer = null;
8133
+ // W-mpetru71000re5de — per-stream backpressure clock + approximate queued
8134
+ // bytes, mirroring writeCcEvent so a runtime error frame can't be swallowed
8135
+ // forever behind a stuck consumer (the "spins endlessly" failure mode).
8136
+ // _docBpStartedAt timestamps the first write that returned false (reset on
8137
+ // 'drain'); _docQueuedBytes accumulates bytes pushed past res.writable's
8138
+ // highWaterMark. The heartbeat tick force-closes the stream once the clock
8139
+ // exceeds SSE_STUCK_KILL_MS, which fires req.on('close') → abort + cleanup
8140
+ // so the client's reader gets `done` and renders a real error instead of
8141
+ // spinning. Doc-chat has no reconnect-replay, so we only shed intermediate
8142
+ // frames (chunk/tool/progress/heartbeat) — terminal `done` and `error`
8143
+ // frames are NEVER shed so the final answer / typed error always lands.
8144
+ let _docBpStartedAt = null;
8145
+ let _docQueuedBytes = 0;
8146
+ try {
8147
+ res.on('drain', () => { _docBpStartedAt = null; _docQueuedBytes = 0; });
8148
+ } catch { /* listener registration is best-effort */ }
8133
8149
  const writeDocEvent = (payload) => {
8134
- // TODO(W-mpetru71000re5de): doc-chat SSE has the same unbounded-queue
8135
- // failure mode as CC's writeCcEvent res.write() returning false from
8136
- // backpressure silently queues bytes in Node's WritableState.buffered[].
8137
- // Out of scope for this fix (task is scoped to CC only). When this is
8138
- // addressed, mirror the SSE_MAX_QUEUE_BYTES shed + SSE_STUCK_KILL_MS
8139
- // heartbeat force-close pattern from the writeCcEvent closure
8140
- // (dashboard.js, search for SSE_MAX_QUEUE_BYTES).
8150
+ const type = payload && payload.type;
8151
+ const isTerminal = type === 'done' || type === 'error';
8152
+ const _logFail = (reason) => {
8153
+ try {
8154
+ shared.log('warn', `[doc-sse-fail] ${JSON.stringify({ doc: docKey || 'unknown', type, reason, destroyed: !!res.destroyed, writableEnded: !!res.writableEnded, streamEnded: _docStreamEnded })}`);
8155
+ } catch { /* telemetry is best-effort */ }
8156
+ };
8157
+ if (res.destroyed || res.writableEnded) {
8158
+ _logFail(res.destroyed ? 'res-destroyed' : 'res-writable-ended');
8159
+ return false;
8160
+ }
8161
+ let wire;
8141
8162
  try {
8142
- const type = payload && payload.type;
8143
- // W-mpmwxni2000c25c7-d mirror the writeCcEvent change so doc-chat
8144
- // also emits `event: error` for terminal errors. Same back-compat:
8145
- // the JSON still carries `type: 'error'` for data-line parsers.
8163
+ // W-mpmwxni2000c25c7-d terminal error frames go out as `event: error`
8164
+ // so SSE consumers using addEventListener('error', …) can target them.
8165
+ // The JSON payload still carries `type: 'error'` for the data-line
8166
+ // parser in modal-qa.js.
8146
8167
  const eventLine = (type === 'error') ? 'event: error\n' : '';
8147
- res.write(eventLine + 'data: ' + JSON.stringify(payload) + '\n\n');
8148
- return true;
8168
+ wire = eventLine + 'data: ' + JSON.stringify(payload) + '\n\n';
8149
8169
  } catch {
8170
+ _logFail('json-serialize-failed');
8150
8171
  return false;
8151
8172
  }
8173
+ // Shed intermediate frames once the queue exceeds the cap, but ALWAYS
8174
+ // attempt terminal frames — the `done` frame carries the full final
8175
+ // answer and the `error` frame is the whole point of this fix.
8176
+ if (!isTerminal && _docQueuedBytes > SSE_MAX_QUEUE_BYTES) {
8177
+ try {
8178
+ shared.log('warn', `[doc-sse-shed] doc=${docKey || 'unknown'} type=${type} queuedBytes=${_docQueuedBytes} wireBytes=${wire.length}`);
8179
+ } catch { /* telemetry is best-effort */ }
8180
+ return true;
8181
+ }
8182
+ let writeOk;
8183
+ try { writeOk = res.write(wire); }
8184
+ catch { _logFail('res-write-threw'); return false; }
8185
+ if (writeOk === false) {
8186
+ // Backpressure — the write is still queued. Start (or extend) the
8187
+ // backpressure clock so the heartbeat tick can force-close a stuck
8188
+ // stream, and accumulate approximate queued bytes for the shed gate.
8189
+ if (_docBpStartedAt == null) _docBpStartedAt = Date.now();
8190
+ _docQueuedBytes += wire.length;
8191
+ try {
8192
+ shared.log('warn', `[doc-sse-backpressure] doc=${docKey || 'unknown'} type=${type} bytes=${wire.length} queuedBytes=${_docQueuedBytes} bpMs=${Date.now() - _docBpStartedAt}`);
8193
+ } catch { /* telemetry is best-effort */ }
8194
+ }
8195
+ return true;
8152
8196
  };
8153
8197
  const stopDocHeartbeat = () => {
8154
8198
  if (_docHeartbeatTimer) {
@@ -8196,6 +8240,20 @@ What would you like to discuss or change? When you're happy, say "approve" and I
8196
8240
  stopDocHeartbeat();
8197
8241
  return;
8198
8242
  }
8243
+ // W-mpetru71000re5de — force-close streams stuck on backpressure.
8244
+ // res.destroy() fires req.on('close'), which runs the teardown path
8245
+ // below (stop heartbeat → drop the in-flight guard → abort the LLM),
8246
+ // so a runtime that finished (or errored) but whose frame can't reach
8247
+ // a wedged consumer no longer leaves the client spinning forever.
8248
+ if (_docBpStartedAt && Date.now() - _docBpStartedAt > SSE_STUCK_KILL_MS) {
8249
+ const stuckMs = Date.now() - _docBpStartedAt;
8250
+ try {
8251
+ shared.log('warn', `[doc-sse-stuck-close] doc=${docKey || 'unknown'} stuckMs=${stuckMs} queuedBytes=${_docQueuedBytes}`);
8252
+ } catch { /* telemetry is best-effort */ }
8253
+ stopDocHeartbeat();
8254
+ try { res.destroy(); } catch { /* swallow — req.on('close') will still fire */ }
8255
+ return;
8256
+ }
8199
8257
  if (!writeDocEvent({ type: 'heartbeat' })) stopDocHeartbeat();
8200
8258
  }, CC_STREAM_HEARTBEAT_MS);
8201
8259
 
@@ -8256,12 +8314,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
8256
8314
  llm.trackEngineError('doc-chat', errCode);
8257
8315
  const isHardFailure = !partial && !(finalize && finalize.edited);
8258
8316
  if (isHardFailure) {
8259
- const errPayload = {
8317
+ // Route through writeDocEvent so the terminal error frame inherits
8318
+ // the destroyed/ended guards and is never shed under backpressure.
8319
+ writeDocEvent({
8320
+ type: 'error',
8260
8321
  message: ccError.typedMessage || ccError.errorMessage || 'Document chat failed',
8261
8322
  code: errCode,
8262
8323
  retriable: ccError.retriable !== false,
8263
- };
8264
- try { res.write(`event: error\ndata: ${JSON.stringify(errPayload)}\n\n`); } catch {}
8324
+ });
8265
8325
  }
8266
8326
  }
8267
8327
  const { answer: finalAnswer, ...donePayload } = payload;
@@ -60,7 +60,7 @@ These files are excluded from published packages:
60
60
  Controlled by the `files` field in `package.json`:
61
61
  - `bin/minions.js` — CLI entry point
62
62
  - `engine.js`, `dashboard.js`, `dashboard/` (fragments), `minions.js` — core scripts
63
- - `engine/spawn-agent.js`, `engine/ado-mcp-wrapper.js` — engine helpers
63
+ - `engine/spawn-agent.js` — engine helper
64
64
  - `agents/*/charter.md` — agent role definitions
65
65
  - `playbooks/*.md` — task templates
66
66
  - `config.template.json` — starter config
@@ -15,7 +15,7 @@ You only need to clone if you intend to modify Minions itself. End users normall
15
15
  ```bash
16
16
  git clone https://github.com/yemi33/minions.git ~/minions-dev
17
17
  cd ~/minions-dev
18
- npm install # installs dev tooling (Playwright) + the lone runtime dep (@azure-devops/mcp); engine is otherwise built on Node built-ins
18
+ npm install # installs dev tooling (Playwright, ESLint) only; the engine itself has zero runtime deps (Node built-ins)
19
19
  ```
20
20
 
21
21
  You should now have:
@@ -142,6 +142,24 @@ EPERM/EBUSY stragglers that the dispatch-end GC couldn't reap and sweeps
142
142
  the `git worktree list` registry for OUT-of-root entries the in-root
143
143
  scanner is blind to.
144
144
 
145
+ ### Ownership marker — out-of-root GC only touches engine worktrees (W-mqecdoot)
146
+
147
+ A project repo's `git worktree list` includes EVERY worktree registered
148
+ against it — including ones a developer created by hand
149
+ (`git worktree add /tmp/my-work <branch>`) for their own work. The
150
+ out-of-root sweep used to evict any such entry that wasn't pinned to a
151
+ live dispatch / pool / managed-spawn, **deleting human worktrees
152
+ mid-edit**. The fix: the engine stamps a `.minions-worktree` ownership
153
+ marker (`shared.WORKTREE_OWNER_MARKER`) into every worktree it creates
154
+ (`shared.writeWorktreeOwnerMarker`, called right after `git worktree add`
155
+ in `engine.js#runWorktreeAdd`). `pruneOrphanWorktreesFromGitRegistry`
156
+ now requires `shared.hasWorktreeOwnerMarker(path)` before evicting an
157
+ out-of-root worktree — **no marker → foreign → kept and never
158
+ escalated** (fail-open: leak a dir rather than nuke unpushed work). The
159
+ trailing `git worktree prune --expire=now` (drops registry entries whose
160
+ dirs are already gone) still runs regardless of ownership. The marker is
161
+ gitignored so it never shows up in any worktree's `git status`.
162
+
145
163
  ## Holder identification + opt-in auto-reap (W-mq6f2fe0000557fa)
146
164
 
147
165
  Orphan-sweep escalations also run `shared.findProcessesWithCwdInside(wt)`
@@ -40,7 +40,7 @@ function getPrCreateInstructions(project) {
40
40
  `- Use --head to specify your feature branch name\n` +
41
41
  `- Include a meaningful --title and body file describing the changes`;
42
42
  }
43
- // Default: Azure DevOps — prefer `az` CLI first, ADO MCP only as fallback
43
+ // Default: Azure DevOps — prefer `az` CLI first, ADO REST API as fallback
44
44
  const adoOrg = project?.adoOrg || '';
45
45
  const adoProject = project?.adoProject || '';
46
46
  const repoName = project?.repoName || '';
@@ -50,7 +50,7 @@ function getPrCreateInstructions(project) {
50
50
  `- Then: \`az repos pr create --repository "${repoName}" --source-branch <your-branch> --target-branch ${mainBranch} --title "PR title" --description @<body-file.md>\`\n` +
51
51
  `- Use \`@<file>\` syntax for \`--description\` so Markdown, quotes, and newlines pass safely\n` +
52
52
  `- Always set --target-branch to \`${mainBranch}\` (the main branch)\n\n` +
53
- `If \`az\` is unavailable or insufficient for this operation, fall back to \`mcp__azure-ado__repo_create_pull_request\` with repositoryId \`${repoId}\`. Do not use \`gh\` for Azure DevOps repositories.`;
53
+ `If \`az\` is unavailable, fall back to the ADO REST API: get a token with \`az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv\` and \`POST .../_apis/git/repositories/${repoId}/pullrequests?api-version=7.1\` (Bearer auth). Do not use \`gh\` for Azure DevOps repositories.`;
54
54
  }
55
55
 
56
56
  function getPrCommentInstructions(project) {
@@ -72,12 +72,12 @@ function getPrCommentInstructions(project) {
72
72
  "```\n" +
73
73
  `Then run: \`gh pr comment <number> --body-file <body-file.md> --repo ${org}/${repo}\`. Without the marker, the engine cannot tell your post from a real human comment and will queue redundant fix-dispatches.`;
74
74
  }
75
- // Azure DevOps — prefer `az` CLI first, ADO MCP only as fallback
75
+ // Azure DevOps — prefer `az` CLI first, ADO REST API as fallback
76
76
  const repoName = project?.repoName || '';
77
77
  return `For Azure DevOps, use the \`az\` CLI first to post a comment on the PR:\n` +
78
78
  `- Write the Markdown comment to a temporary file, then run: \`az repos pr comment create --pull-request-id <number> --content @<body-file.md>\` (substitute your project's repo \`${repoName}\` if not using \`az devops configure\` defaults)\n` +
79
79
  `- Use \`@<file>\` syntax for \`--content\` so Markdown, quotes, and newlines pass safely\n\n` +
80
- `If \`az repos pr comment\` is unavailable or insufficient (e.g. older az-devops extension, thread/status semantics needed), fall back to \`mcp__azure-ado__repo_create_pull_request_thread\` with repositoryId \`${repoId}\`. Do not use \`gh\` for Azure DevOps repositories.`;
80
+ `If \`az repos pr comment\` is unavailable or insufficient (e.g. older az-devops extension, thread/status semantics needed), fall back to the ADO REST API: get a token with \`az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv\` and \`POST .../_apis/git/repositories/${repoId}/pullrequests/<number>/threads?api-version=7.1\` with body \`{"comments":[{"content":"..."}],"status":"active"}\` (Bearer auth). Do not use \`gh\` for Azure DevOps repositories.`;
81
81
  }
82
82
 
83
83
  function getPrFetchInstructions(project) {
@@ -95,13 +95,13 @@ function getPrFetchInstructions(project) {
95
95
  `- Or use \`gh pr checkout <number> --repo ${org}/${repo}\` to fetch and checkout in one step\n` +
96
96
  `- The base branch is \`${mainBranch}\``;
97
97
  }
98
- // Azure DevOps — prefer `az` CLI first, ADO MCP only as fallback
98
+ // Azure DevOps — prefer `az` CLI first, ADO REST API as fallback
99
99
  const mainBranch = project?.localPath ? shared.resolveMainBranch(project.localPath, project.mainBranch) : (project?.mainBranch || 'main');
100
100
  return `For Azure DevOps, use the \`az\` CLI first to fetch PR status:\n` +
101
101
  `- \`az repos pr show --id <number>\` returns PR state, mergeStatus, source/target branches, vote summary, and policy/build evaluations\n` +
102
102
  `- For the local branch: \`git fetch origin <branch-name>\` then inspect via \`git show\`/\`git diff\` (do NOT checkout in your main working tree)\n` +
103
103
  `- The base branch is \`${mainBranch}\`\n\n` +
104
- `If \`az\` is unavailable or insufficient, fall back to \`mcp__azure-ado__repo_get_pull_request_by_id\`. Do not use \`gh\` for Azure DevOps repositories.`;
104
+ `If \`az\` is unavailable, fall back to the ADO REST API: get a token with \`az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv\` and \`GET .../_apis/git/repositories/<repoId>/pullrequests/<number>?api-version=7.1\` (Bearer auth). Do not use \`gh\` for Azure DevOps repositories.`;
105
105
  }
106
106
 
107
107
  function getPrVoteInstructions(project) {
@@ -123,11 +123,11 @@ function getPrVoteInstructions(project) {
123
123
  "```\n" +
124
124
  `Then run: \`gh pr review <number> --comment --body-file <verdict.md> --repo ${org}/${repo}\`. Do NOT use \`--approve\` or \`--request-changes\` flags — they will fail.`;
125
125
  }
126
- // Azure DevOps — prefer `az` CLI first, ADO MCP only as fallback
126
+ // Azure DevOps — prefer `az` CLI first, ADO REST API as fallback
127
127
  return `For Azure DevOps, use the \`az\` CLI first to set your reviewer vote:\n` +
128
128
  `- \`az repos pr set-vote --id <number> --vote {approve | approve-with-suggestions | reject | reset | wait-for-author}\`\n` +
129
129
  `- Pair the vote with \`az repos pr comment create --pull-request-id <number> --content @<verdict.md>\` so the verdict body is recorded as a thread comment\n\n` +
130
- `If \`az\` is unavailable or insufficient, fall back to \`mcp__azure-ado__repo_update_pull_request_reviewers\` with repositoryId \`${repoId}\` (vote integers: 10=approve, 5=approve-with-suggestions, 0=no-vote, -5=wait-for-author, -10=reject). Do not use \`gh\` for Azure DevOps repositories.`;
130
+ `If \`az\` is unavailable, fall back to the ADO REST API: get a token with \`az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv\` and \`PUT .../_apis/git/repositories/${repoId}/pullrequests/<number>/reviewers/<reviewerId>?api-version=7.1\` with body \`{"vote":10}\` (10=approve, 5=approve-with-suggestions, 0=no-vote, -5=wait-for-author, -10=reject; Bearer auth). Do not use \`gh\` for Azure DevOps repositories.`;
131
131
  }
132
132
 
133
133
  function getRepoHostLabel(project) {
@@ -139,7 +139,7 @@ function getRepoHostLabel(project) {
139
139
  function getRepoHostToolRule(project) {
140
140
  const host = getRepoHost(project);
141
141
  if (host === 'github') return 'Use GitHub MCP tools or `gh` CLI for PR operations';
142
- return 'For Azure DevOps, use the `az` CLI first for PR operations (e.g. `az repos pr create`, `az repos pr show`, `az repos pr comment`, `az repos pr set-vote`); use ADO MCP tools (`mcp__azure-ado__*`) only as a fallback when `az` is unavailable or insufficient. Do not use `gh` for Azure DevOps repositories.';
142
+ return 'For Azure DevOps, use the `az` CLI for PR operations (e.g. `az repos pr create`, `az repos pr show`, `az repos pr comment`, `az repos pr set-vote`); if `az` is unavailable or insufficient, fall back to the ADO REST API (`_apis/git/...`) with a token from `az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798`. Do not use `gh` for Azure DevOps repositories.';
143
143
  }
144
144
 
145
145
  // ─── Task Context Resolution ────────────────────────────────────────────────
package/engine/shared.js CHANGED
@@ -7790,6 +7790,49 @@ function removeWorktree(wtPath, gitRoot, worktreeRoot, opts = {}) {
7790
7790
  }
7791
7791
  }
7792
7792
 
7793
+ // W-mqecdoot — engine worktree ownership marker. The out-of-root worktree GC
7794
+ // (worktree-gc.pruneOrphanWorktreesFromGitRegistry) reaps worktrees registered
7795
+ // in a project's git that live OUTSIDE the configured worktreeRoot. Without a
7796
+ // positive ownership signal it cannot tell an engine-created orphan from a
7797
+ // worktree a human developer created by hand (`git worktree add /tmp/foo …`)
7798
+ // for their own work — and was deleting the latter mid-edit. We stamp this
7799
+ // marker into every worktree the engine creates so the GC can require proof of
7800
+ // ownership before removal; a worktree with NO marker is treated as foreign
7801
+ // and left untouched (fail-open: leak a dir rather than nuke someone's work).
7802
+ const WORKTREE_OWNER_MARKER = '.minions-worktree';
7803
+
7804
+ // Best-effort stamp dropped right after a successful `git worktree add`. Never
7805
+ // throws — a missing marker only costs us the ability to auto-GC that dir.
7806
+ function writeWorktreeOwnerMarker(worktreePath, info = {}) {
7807
+ if (!worktreePath) return false;
7808
+ try {
7809
+ const target = path.join(path.resolve(worktreePath), WORKTREE_OWNER_MARKER);
7810
+ const payload = {
7811
+ engine: true,
7812
+ createdAt: new Date().toISOString(),
7813
+ pid: process.pid,
7814
+ ...info,
7815
+ };
7816
+ fs.writeFileSync(target, JSON.stringify(payload, null, 2) + '\n');
7817
+ return true;
7818
+ } catch (e) {
7819
+ log('debug', `writeWorktreeOwnerMarker: could not stamp ${worktreePath}: ${e.message}`);
7820
+ return false;
7821
+ }
7822
+ }
7823
+
7824
+ // True only when the worktree carries the engine ownership marker. Fail-closed
7825
+ // (returns false) on any read error so an unreadable dir is treated as foreign
7826
+ // and kept, never blindly removed.
7827
+ function hasWorktreeOwnerMarker(worktreePath) {
7828
+ if (!worktreePath) return false;
7829
+ try {
7830
+ return fs.existsSync(path.join(path.resolve(worktreePath), WORKTREE_OWNER_MARKER));
7831
+ } catch {
7832
+ return false;
7833
+ }
7834
+ }
7835
+
7793
7836
  function slugify(text, maxLen = 50) {
7794
7837
  return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, maxLen);
7795
7838
  }
@@ -8195,6 +8238,9 @@ module.exports = {
8195
8238
  listProcessReachable,
8196
8239
  removeWorktree,
8197
8240
  isWorktreePathLive,
8241
+ WORKTREE_OWNER_MARKER,
8242
+ writeWorktreeOwnerMarker,
8243
+ hasWorktreeOwnerMarker,
8198
8244
  _normalizeWorktreePath, // exported for testing
8199
8245
  _writeWorktreeSkipLiveInboxNote, // exported for testing
8200
8246
  _retryFsOp, // exported for testing (W-mq5o6bvy000x7191)
@@ -771,6 +771,14 @@ function pruneOrphanWorktreesFromGitRegistry(opts) {
771
771
  const _parseWorktreePorcelain = typeof opts.parseWorktreePorcelain === 'function'
772
772
  ? opts.parseWorktreePorcelain
773
773
  : shared.parseWorktreePorcelain;
774
+ // W-mqecdoot — positive ownership gate. An out-of-root worktree is only
775
+ // engine-managed if it carries the ownership marker we stamp at creation
776
+ // (shared.writeWorktreeOwnerMarker). Worktrees a human created by hand
777
+ // (`git worktree add /tmp/foo …`) have no marker and must be left alone —
778
+ // this sweep was deleting them mid-edit.
779
+ const _hasOwnerMarker = typeof opts.hasOwnerMarker === 'function'
780
+ ? opts.hasOwnerMarker
781
+ : shared.hasWorktreeOwnerMarker;
774
782
 
775
783
  // Branches of active/pending dispatches (with and without `refs/heads/`
776
784
  // normalization). Matched case-insensitively to mirror git's behavior on
@@ -875,6 +883,21 @@ function pruneOrphanWorktreesFromGitRegistry(opts) {
875
883
  if (anchored) { projStats.kept++; result.kept++; continue; }
876
884
  }
877
885
 
886
+ // W-mqecdoot — ownership gate. Only ever evict a worktree the engine
887
+ // positively created (carries the ownership marker stamped at
888
+ // `git worktree add` time). No marker → it's a human's hand-made worktree
889
+ // (or a pre-marker legacy dir); KEEP it and do NOT escalate. The
890
+ // `git worktree prune --expire=now` below still drops registry entries
891
+ // whose dirs are already gone, which is safe regardless of ownership.
892
+ let owned = false;
893
+ try { owned = !!_hasOwnerMarker(wtAbs); }
894
+ catch (_e) { owned = false; }
895
+ if (!owned) {
896
+ projStats.kept++; result.kept++;
897
+ log('debug', `worktree-gc: keeping out-of-root ${wtAbs} — no engine ownership marker (foreign/hand-made worktree)`);
898
+ continue;
899
+ }
900
+
878
901
  // Slow-cadence gate for already-escalated stuck paths
879
902
  const slowRetryMs = (opts.config?.engine?.worktreeStuckSlowRetryMs)
880
903
  ?? shared.ENGINE_DEFAULTS.worktreeStuckSlowRetryMs
package/engine.js CHANGED
@@ -916,6 +916,10 @@ async function runWorktreeAdd(rootDir, worktreePath, addArgs, gitOpts, worktreeC
916
916
  log('warn', `Retrying git worktree add (attempt ${attempt + 1}/${retries + 1}) for ${path.basename(worktreePath)}`);
917
917
  }
918
918
  await shared.shellSafeGit(['worktree', 'add', worktreePath, ...addArgs], { ...gitOpts, cwd: rootDir });
919
+ // W-mqecdoot — stamp the engine ownership marker so the out-of-root
920
+ // worktree GC can distinguish a worktree we created from one a human
921
+ // developer made by hand. Best-effort; never blocks the spawn.
922
+ shared.writeWorktreeOwnerMarker(worktreePath, { rootDir });
919
923
  return;
920
924
  } catch (err) {
921
925
  lastErr = err;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2186",
3
+ "version": "0.1.2188",
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"
@@ -67,9 +67,6 @@
67
67
  "engines": {
68
68
  "node": ">=22.5"
69
69
  },
70
- "dependencies": {
71
- "@azure-devops/mcp": "2.7.0"
72
- },
73
70
  "devDependencies": {
74
71
  "@playwright/test": "^1.58.2",
75
72
  "eslint": "^9.39.4",
@@ -274,5 +274,5 @@ Output is JSON with the same fields. Exit 0 on success, 1 if not found.
274
274
 
275
275
  For Azure DevOps repo operations, use the `az` CLI first. Prefer commands such as `az repos pr create`, `az repos pr show`, `az repos pr list`, `az repos pr comment`, `az repos pr reviewer`, `az boards work-item`, and `az pipelines` after setting defaults with `az devops configure`.
276
276
 
277
- Use ADO MCP fallback tools (`mcp__azure-ado__*`) only when `az` is unavailable in the environment or insufficient for a specific operation. Do not choose MCP first just because it exists, and do not use `gh` for Azure DevOps repositories.
277
+ If `az` is unavailable or insufficient for a specific operation (e.g. PR comment threads on an older az-devops extension), fall back to the ADO REST API directly: acquire a token with `az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv` and call the `_apis/git/...` endpoints with Bearer auth. Do not use `gh` for Azure DevOps repositories.
278
278
  {{/ado_shared_rules}}
@@ -1,63 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Wrapper for @azure-devops/mcp that fetches an ADO token via the shared
4
- * az-first provider chain and sets AZURE_DEVOPS_EXT_PAT before launching the
5
- * MCP server.
6
- *
7
- * P-b3f8e1a5: @azure-devops/mcp is pinned in package.json and resolved from
8
- * local node_modules. We spawn process.execPath against the resolved bin file
9
- * instead of going through npx/npx.cmd. This (a) eliminates the per-cold-start
10
- * network fetch that ran with AZURE_DEVOPS_EXT_PAT in env, and (b) avoids the
11
- * Windows .cmd shim chain that crashes the runtime under spawn.
12
- */
13
- const { spawn } = require('child_process');
14
- const fs = require('fs');
15
- const path = require('path');
16
- const { acquireAdoTokenSync } = require('./ado-token');
17
-
18
- const PKG_NAME = '@azure-devops/mcp';
19
- const BIN_NAME = 'mcp-server-azuredevops';
20
-
21
- function resolveMcpBin() {
22
- const pkgJsonPath = require.resolve(`${PKG_NAME}/package.json`);
23
- const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
24
- const binField = pkgJson.bin;
25
- const relBin = typeof binField === 'string' ? binField : binField && binField[BIN_NAME];
26
- if (!relBin) {
27
- throw new Error(`${PKG_NAME} package.json is missing bin entry "${BIN_NAME}"`);
28
- }
29
- return path.resolve(path.dirname(pkgJsonPath), relBin);
30
- }
31
-
32
- let token;
33
- try {
34
- token = acquireAdoTokenSync().token;
35
- } catch (e) {
36
- process.stderr.write('ado-mcp-wrapper: ADO auth failed: ' + e.message + '\n');
37
- process.stderr.write('ado-mcp-wrapper: Run "az login" or refresh azureauth manually, then retry\n');
38
- process.exit(1);
39
- }
40
-
41
- let binPath;
42
- try {
43
- binPath = resolveMcpBin();
44
- } catch (e) {
45
- process.stderr.write('ado-mcp-wrapper: failed to resolve ' + PKG_NAME + ': ' + e.message + '\n');
46
- process.stderr.write('ado-mcp-wrapper: run "npm install" in the minions checkout to restore the pinned dependency\n');
47
- process.exit(1);
48
- }
49
-
50
- const args = process.argv.slice(2);
51
- const child = spawn(process.execPath, [binPath, ...args], {
52
- stdio: 'inherit',
53
- env: { ...process.env, AZURE_DEVOPS_EXT_PAT: token, AZURE_DEVOPS_EXT_AZURE_RM_PAT: token },
54
- windowsHide: true,
55
- shell: false,
56
- });
57
-
58
- child.on('exit', (code) => process.exit(code || 0));
59
- child.on('error', (err) => {
60
- process.stderr.write('ado-mcp-wrapper: ' + err.message + '\n');
61
- process.exit(1);
62
- });
63
-