@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 +2 -4
- package/dashboard.js +76 -16
- package/docs/distribution.md +1 -1
- package/docs/onboarding.md +1 -1
- package/docs/worktree-lifecycle.md +18 -0
- package/engine/playbook.js +9 -9
- package/engine/shared.js +46 -0
- package/engine/worktree-gc.js +23 -0
- package/engine.js +4 -0
- package/package.json +1 -4
- package/playbooks/shared-rules.md +1 -1
- package/engine/ado-mcp-wrapper.js +0 -63
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.
|
|
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
|
|
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
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
|
|
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
|
-
|
|
8143
|
-
//
|
|
8144
|
-
//
|
|
8145
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/docs/distribution.md
CHANGED
|
@@ -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
|
|
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
|
package/docs/onboarding.md
CHANGED
|
@@ -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)
|
|
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)`
|
package/engine/playbook.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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 \`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
package/engine/worktree-gc.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|