@yemi33/minions 0.1.2382 → 0.1.2383
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/bin/minions.js +1 -0
- package/dashboard.js +26 -12
- package/docs/completion-reports.md +20 -1
- package/docs/runtime-adapters.md +54 -22
- package/docs/skills.md +2 -0
- package/docs/workspace-manifests.md +1 -1
- package/docs/worktree-lifecycle.md +14 -0
- package/engine/acp-transport.js +63 -35
- package/engine/ado-comment.js +3 -1
- package/engine/agent-worker-pool.js +11 -4
- package/engine/cc-worker-pool.js +4 -2
- package/engine/cli.js +9 -1
- package/engine/comment-format.js +51 -14
- package/engine/dispatch.js +7 -2
- package/engine/execution-model.js +68 -0
- package/engine/gh-comment.js +6 -3
- package/engine/github.js +6 -7
- package/engine/lifecycle.js +125 -44
- package/engine/llm.js +59 -83
- package/engine/memory-store.js +3 -1
- package/engine/playbook.js +4 -6
- package/engine/pooled-agent-process.js +28 -26
- package/engine/runtimes/claude.js +94 -25
- package/engine/runtimes/codex.js +1 -0
- package/engine/runtimes/contract.js +239 -0
- package/engine/runtimes/copilot.js +78 -8
- package/engine/runtimes/index.js +37 -9
- package/engine/shared.js +95 -21
- package/engine/spawn-agent.js +79 -113
- package/engine/spawn-phase-watchdog.js +8 -37
- package/engine.js +183 -109
- package/package.json +1 -1
- package/playbooks/fix.md +20 -0
- package/playbooks/review.md +2 -0
- package/skills/check-self-authored-review-comment/SKILL.md +34 -0
package/bin/minions.js
CHANGED
|
@@ -912,6 +912,7 @@ function init() {
|
|
|
912
912
|
|
|
913
913
|
// Copy with smart merge logic
|
|
914
914
|
copyDir(PKG_ROOT, MINIONS_HOME, excludeTop, alwaysUpdate, neverOverwrite, isUpgrade, actions);
|
|
915
|
+
shared.syncBundledPersonalSkills(path.join(MINIONS_HOME, 'skills'), { homeDir: os.homedir() });
|
|
915
916
|
|
|
916
917
|
// Create config from template if it doesn't exist
|
|
917
918
|
const configPath = path.join(MINIONS_HOME, 'config.json');
|
package/dashboard.js
CHANGED
|
@@ -26,7 +26,7 @@ const fs = require('fs');
|
|
|
26
26
|
const path = require('path');
|
|
27
27
|
const v8 = require('v8');
|
|
28
28
|
const llm = require('./engine/llm');
|
|
29
|
-
const { resolveRuntime } = require('./engine/runtimes');
|
|
29
|
+
const { resolveRuntime, shouldSuppressPostMutationError } = require('./engine/runtimes');
|
|
30
30
|
|
|
31
31
|
// Dashboard version stamp — captured at module load so it reflects the code actually running.
|
|
32
32
|
// codeCommit is read via _readGitHeadShort (defined below — function declarations
|
|
@@ -1974,11 +1974,19 @@ function _countWorktrees() {
|
|
|
1974
1974
|
try {
|
|
1975
1975
|
const config = queries.getConfig();
|
|
1976
1976
|
const projects = shared.getProjects(config);
|
|
1977
|
-
|
|
1977
|
+
const worktreeRoots = new Map();
|
|
1978
1978
|
for (const p of projects) {
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1979
|
+
if (!p.localPath) continue;
|
|
1980
|
+
const wtRoot = path.resolve(
|
|
1981
|
+
p.localPath,
|
|
1982
|
+
config.engine?.worktreeRoot || shared.ENGINE_DEFAULTS.worktreeRoot,
|
|
1983
|
+
);
|
|
1984
|
+
const rootKey = shared._normalizeWorktreePath(wtRoot);
|
|
1985
|
+
if (!worktreeRoots.has(rootKey)) worktreeRoots.set(rootKey, wtRoot);
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
let count = 0;
|
|
1989
|
+
for (const wtRoot of worktreeRoots.values()) {
|
|
1982
1990
|
try {
|
|
1983
1991
|
for (const dir of fs.readdirSync(wtRoot)) {
|
|
1984
1992
|
const dirPath = path.join(wtRoot, dir);
|
|
@@ -4678,6 +4686,7 @@ function _invokeDocChatViaPool({ prompt, model, effort, engineConfig, systemProm
|
|
|
4678
4686
|
(async () => {
|
|
4679
4687
|
try {
|
|
4680
4688
|
sessionHandle = await ccWorkerPool.getSession({
|
|
4689
|
+
runtime: llm._resolveRuntimeFor({ engineConfig }),
|
|
4681
4690
|
tabId: tabKey,
|
|
4682
4691
|
model,
|
|
4683
4692
|
effort,
|
|
@@ -5345,13 +5354,15 @@ function _recoverPartialDocChatResponse(result, sessionKey) {
|
|
|
5345
5354
|
function _shouldSuppressDocChatPostPatchError(ccError, finalize) {
|
|
5346
5355
|
if (!finalize || finalize.edited !== true) return false;
|
|
5347
5356
|
if (!ccError) return false;
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5357
|
+
try {
|
|
5358
|
+
const runtime = resolveRuntime(ccError.runtime);
|
|
5359
|
+
return shouldSuppressPostMutationError(runtime, {
|
|
5360
|
+
mutationApplied: true,
|
|
5361
|
+
error: ccError,
|
|
5362
|
+
});
|
|
5363
|
+
} catch {
|
|
5364
|
+
return false;
|
|
5365
|
+
}
|
|
5355
5366
|
}
|
|
5356
5367
|
|
|
5357
5368
|
function _buildDocChatResponsePayload({
|
|
@@ -9447,6 +9458,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
9447
9458
|
if (!shared.resolveCcUseWorkerPool(CONFIG.engine)) return { skipped: 'pool-disabled' };
|
|
9448
9459
|
if (!tabId) throw new Error('tabId required');
|
|
9449
9460
|
const result = await ccWorkerPool.warmTab({
|
|
9461
|
+
runtime: resolveRuntime(shared.resolveCcCli(CONFIG.engine)),
|
|
9450
9462
|
tabId,
|
|
9451
9463
|
model: CONFIG.engine?.ccModel || shared.ENGINE_DEFAULTS.ccModel,
|
|
9452
9464
|
effort: CONFIG.engine?.ccEffort || shared.ENGINE_DEFAULTS.ccEffort,
|
|
@@ -9823,6 +9835,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
9823
9835
|
(async () => {
|
|
9824
9836
|
try {
|
|
9825
9837
|
sessionHandle = await ccWorkerPool.getSession({
|
|
9838
|
+
runtime: llm._resolveRuntimeFor({ engineConfig }),
|
|
9826
9839
|
tabId: resolvedTabId,
|
|
9827
9840
|
model,
|
|
9828
9841
|
effort,
|
|
@@ -13614,6 +13627,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13614
13627
|
status: 'enrolled',
|
|
13615
13628
|
prId: linkResult.id,
|
|
13616
13629
|
autoManaged,
|
|
13630
|
+
resurrected: linkResult.resurrected === true,
|
|
13617
13631
|
contextOnly: false,
|
|
13618
13632
|
project: linkResult.targetProject?.name || 'central',
|
|
13619
13633
|
projectName: plan.projectName || linkResult.targetProject?.name || null,
|
|
@@ -90,6 +90,7 @@ Do **not** invent, regenerate, or share the nonce across dispatches — each spa
|
|
|
90
90
|
|---|---|---|
|
|
91
91
|
| `noop` | boolean | Canonical no-op signal. See [No-op semantics](#no-op-semantics). |
|
|
92
92
|
| `noopReason` | string | Human-readable rationale shown when `noop: true`. Falls back to `summary` if absent. |
|
|
93
|
+
| `reviewFindingResolution` | object | Required when a review-feedback fix leaves the PR branch unchanged. Shape: `{findingId, disposition, currentCodeEvidence}`; see [Review-fix no-op evidence](#review-fix-no-op-evidence). |
|
|
93
94
|
| `files_changed` | string \| array | Comma-separated list (or array) of key files changed. |
|
|
94
95
|
| `affected_files` | string[] | Optional array of relative file paths this dispatch touched or plans to touch. Used by the dispatcher to emit a conflict warning (`WI <new-id> may conflict with in-progress <existing-id> on files: [list]`) when a new WI's `affected_files` overlaps with an in-progress WI's `affected_files`. Logging-only — dispatch is never blocked. Stored back onto the work item after completion so re-dispatches can also participate in overlap detection. |
|
|
95
96
|
| `tests` | string | `pass`, `fail`, `skipped`, `N/A`, or a free-form note like `skipped — relying on PR pipeline`. |
|
|
@@ -223,7 +224,7 @@ All three classes are **never retryable** — they signal a structural/environme
|
|
|
223
224
|
|
|
224
225
|
## No-op semantics
|
|
225
226
|
|
|
226
|
-
A no-op completion declares that the agent correctly **declined** to do the work — the change was already shipped on master, the dispatch premise was wrong,
|
|
227
|
+
A no-op completion declares that the agent correctly **declined** to do the work — the change was already shipped on master, the dispatch premise was wrong, or an exact review finding was proven resolved/invalid with current-code evidence.
|
|
227
228
|
|
|
228
229
|
To signal a no-op:
|
|
229
230
|
|
|
@@ -245,6 +246,24 @@ Engine behavior when `noop: true` (`engine/lifecycle.js` `parseCompletionNoop` +
|
|
|
245
246
|
|
|
246
247
|
Without `noop: true`, an empty PR field will be flagged as a missing-PR-attachment failure and auto-retried up to `ENGINE_DEFAULTS.maxRetries` times.
|
|
247
248
|
|
|
249
|
+
### Review-fix no-op evidence
|
|
250
|
+
|
|
251
|
+
Review-feedback fixes have a stricter no-op contract because all agents can share one platform credential. `viewerDidAuthor: true` does not identify the Minions agent that authored a finding, and reviewer/fixer agent equality is allowed.
|
|
252
|
+
|
|
253
|
+
If the live PR branch did not advance, the completion must set `noop: true` and include:
|
|
254
|
+
|
|
255
|
+
```json
|
|
256
|
+
{
|
|
257
|
+
"reviewFindingResolution": {
|
|
258
|
+
"findingId": "review-0123456789abcdef",
|
|
259
|
+
"disposition": "already-resolved",
|
|
260
|
+
"currentCodeEvidence": "engine/lifecycle.js:4600 already enforces the condition; test/unit/example.test.js covers it."
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The `findingId` is injected into the fix playbook from trusted `minionsReview` provenance. `disposition` is `already-resolved` or `invalid`, and `currentCodeEvidence` must cite a current `file:line` or commit. An identity-only no-op is retried and does not increment `_noOpFixes`.
|
|
266
|
+
|
|
248
267
|
## PR-comment follow-ups
|
|
249
268
|
|
|
250
269
|
Fix and review agents can spin off new Minions work items in response to PR
|
package/docs/runtime-adapters.md
CHANGED
|
@@ -8,7 +8,8 @@ behavior is hidden behind an adapter object resolved through `resolveRuntime()`.
|
|
|
8
8
|
|
|
9
9
|
| File | Role |
|
|
10
10
|
|------|------|
|
|
11
|
-
| `engine/runtimes/
|
|
11
|
+
| `engine/runtimes/contract.js` | Versioned adapter contract, capability gating, opaque invocation-option transport, and optional-hook facades. |
|
|
12
|
+
| `engine/runtimes/index.js` | Adapter registry and the only runtime facade imported by orchestration code. |
|
|
12
13
|
| `engine/runtimes/claude.js` | Claude Code adapter. Owns binary probe, `--system-prompt-file`, JSONL parser, model shorthands, budget cap, bare mode. |
|
|
13
14
|
| `engine/runtimes/copilot.js` | GitHub Copilot CLI adapter. Owns standalone-vs-`gh-copilot` resolution, stdin-only prompt delivery, `https://api.githubcopilot.com/models` discovery, effort `'max' → 'xhigh'` mapping. |
|
|
14
15
|
| `engine/runtimes/codex.js` | OpenAI Codex CLI adapter. Owns `@openai/codex`/native binary resolution, `codex exec --json -` prompt delivery, `codex debug models --bundled` discovery, `.agents/skills` roots, and Codex JSONL parsing. |
|
|
@@ -30,6 +31,7 @@ methods that genuinely differ.
|
|
|
30
31
|
|
|
31
32
|
| Member | Type / Returns | Purpose |
|
|
32
33
|
|--------|----------------|---------|
|
|
34
|
+
| `apiVersion` | number | Contract version. Bundled adapters use `1`; incompatible versions fail at registration. |
|
|
33
35
|
| `name` | string | Adapter identifier (matches the registry key). |
|
|
34
36
|
| `capabilities` | flag object (see below) | Feature flags consumed by engine code. |
|
|
35
37
|
| `resolveBinary({ env, config? })` | `{ bin, native, leadingArgs }` or null | Probe PATH, npm-globals, package overrides; cached to `engine/<name>-caps.json`. `leadingArgs` is `[]` for direct binaries and `['copilot']` for the `gh copilot` extension fallback. |
|
|
@@ -39,7 +41,8 @@ methods that genuinely differ.
|
|
|
39
41
|
| `modelsCache` | absolute path | Cache file for `listModels()` results. |
|
|
40
42
|
| `spawnScript` | absolute path | Path to the runtime-agnostic spawn wrapper (currently `engine/spawn-agent.js` for both runtimes). |
|
|
41
43
|
| `buildArgs(opts)` | `string[]` | CLI args excluding the binary itself. |
|
|
42
|
-
| `
|
|
44
|
+
| `resolveInvocationOptions({ options, engineConfig, config, failureClass })` | object | Optional; maps harness-specific config and retry policy into semantic invocation options. Core code never reads harness-specific invocation keys. |
|
|
45
|
+
| `buildSpawnFlags(opts)` | `string[]` | Optional compatibility flags for older spawn wrappers. New fields travel in the opaque `--runtime-options` bag automatically. |
|
|
43
46
|
| `buildPrompt(promptText, sysPromptText)` | string | Final prompt delivered to the CLI. Adapters that lack `--system-prompt-file` (Copilot) prepend a `<system>` block here. |
|
|
44
47
|
| `getUserAssetDirs({ homeDir })` | `string[]` | Runtime-native global asset roots passed to spawn as `--add-dir` so worktrees still see them. |
|
|
45
48
|
| `getSkillRoots({ homeDir, project? })` | `[{ scope, dir, projectName? }]` | Where `collectSkillFiles` looks for native + project skill markdown. |
|
|
@@ -52,11 +55,39 @@ methods that genuinely differ.
|
|
|
52
55
|
| `parseStreamChunk(line)` | event object or null | Single JSONL line → typed event. |
|
|
53
56
|
| `parseError(rawOutput)` | `{ message, code, retriable }` | Codes: `auth-failure`, `context-limit`, `budget-exceeded`, `model-unavailable` (retriable=true for upstream overload/503; retriable=false for invalid/typo'd model id — Copilot enriches the message via `_warmModelCache()` so it lists the available models), `crash`, null. |
|
|
54
57
|
| `createStreamConsumer(ctx)` | consumer object | Stream accumulator used by `engine/llm.js`. |
|
|
55
|
-
| `
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
(
|
|
59
|
-
|
|
58
|
+
| `prepareWorkspace(ctx)` | result object | Optional synchronous hook for harness-owned workspace state, such as trust configuration. |
|
|
59
|
+
| `isSpawnStartupEvent(event)` | boolean | Optional startup-event classifier used by the spawn-phase watchdog. |
|
|
60
|
+
| `shouldSuppressPostMutationError(ctx)` | boolean | Optional harness quirk policy for an error received after a confirmed mutation. |
|
|
61
|
+
| `buildWorkerArgs(opts)` | string[] | Required with `acpWorkerPool`; owns persistent-worker CLI flags. |
|
|
62
|
+
| `encodePooledOutput(event)` | string | Required with `acpWorkerPool`; translates semantic pooled events into the adapter's normal parser format. |
|
|
63
|
+
| `detectPermissionGate`, `getPromptDeliveryMode`, `usesSystemPromptFile`, `classifyFailure` | misc | Adapter-owned policy that orchestration reads through the facade instead of branching on `runtime.name`. |
|
|
64
|
+
|
|
65
|
+
`engine/runtimes/contract.js` is authoritative. Registration normalizes legacy
|
|
66
|
+
partial test/tooling adapters, validates bundled adapters strictly, and rejects
|
|
67
|
+
an explicit unsupported `apiVersion`.
|
|
68
|
+
|
|
69
|
+
### Stable invocation boundary
|
|
70
|
+
|
|
71
|
+
Minions passes semantic values such as `model`, `effort`, and `sessionId`; it
|
|
72
|
+
does not construct harness CLI argv. The selected adapter:
|
|
73
|
+
|
|
74
|
+
1. maps its config through `resolveInvocationOptions()`;
|
|
75
|
+
2. receives the full options object through an opaque base64url JSON
|
|
76
|
+
`--runtime-options` channel when the spawn wrapper is used; and
|
|
77
|
+
3. converts those options to real CLI flags only in `buildArgs()`.
|
|
78
|
+
|
|
79
|
+
Legacy named wrapper flags remain for backward compatibility, but they are no
|
|
80
|
+
longer the extension point. A new harness option can be added entirely inside
|
|
81
|
+
its adapter without teaching `engine.js`, `engine/llm.js`, or
|
|
82
|
+
`engine/spawn-agent.js` another CLI flag. Large image payloads are written to a
|
|
83
|
+
protected per-call sidecar; only its path enters the options argument, and the
|
|
84
|
+
wrapper hydrates then deletes it before invoking the adapter.
|
|
85
|
+
|
|
86
|
+
Retirement gates for the temporary compatibility surfaces are tracked in
|
|
87
|
+
`docs/deprecated.json`: `legacy-runtime-named-spawn-flags`,
|
|
88
|
+
`versionless-runtime-adapter-compat`, and
|
|
89
|
+
`preapprove-workspace-mcps-wrapper`. Calendar dates never override their
|
|
90
|
+
external-usage gates.
|
|
60
91
|
|
|
61
92
|
## Capability Flags
|
|
62
93
|
|
|
@@ -81,7 +112,7 @@ real behavioral split appears between adapters.
|
|
|
81
112
|
| `resumePromptCarryover` | ✗ | ✓ | ✓ | CC resume turns prepend recent visible Q&A in stdin when the runtime session store is opaque to Minions. |
|
|
82
113
|
| `resumeBookkeepingTurn` | ✓ | — | — | Claude CLI injects a synthetic "Continue from where you left off." meta turn on `--resume`; CC prompts must tell the model not to treat it as user intent. |
|
|
83
114
|
| `streamConsumer` | ✓ | ✓ | ✓ | Adapter implements `createStreamConsumer(ctx)` — required by `engine/llm.js` accumulator. |
|
|
84
|
-
| `imageInput` | ✓ | ✓ |
|
|
115
|
+
| `imageInput` | ✓ | ✓ | ✓ | Runtime accepts `images: [{mimeType, dataBase64}]` and delivers them to the CLI using adapter-owned transport. When false, `_resolveImageOpts` returns a typed `model-unavailable` error. |
|
|
85
116
|
| `acpWorkerPool` | ✗ | ✓ | ✗ | Runtime supports `<bin> --acp` (Agent Client Protocol), so it can be routed through a persistent worker pool instead of cold-spawning a fresh CLI process per turn/dispatch. Two independent consumers gate on this flag: `engine/cc-worker-pool.js` (CC/doc-chat tabs, `resolveCcUseWorkerPool`) and `engine/agent-worker-pool.js` (fleet agent dispatch, `resolveAgentUseWorkerPool`). Both resolvers hard-return `false` whenever this flag isn't `true` on the resolved runtime, regardless of any operator override, so a future non-ACP runtime can never be silently pooled. See [ACP Worker Pool](#acp-worker-pool-fleet-agent-dispatch) below. |
|
|
86
117
|
|
|
87
118
|
When a behavior is genuinely uniform across all current adapters, it still gets
|
|
@@ -197,9 +228,9 @@ agent fleet to Copilot, and vice versa. Both paths fall through to
|
|
|
197
228
|
`engine.defaultCli` (the fleet-wide default), but they never see each other's
|
|
198
229
|
overrides.
|
|
199
230
|
|
|
200
|
-
|
|
201
|
-
resolution. Engine code MUST go through
|
|
202
|
-
directly.
|
|
231
|
+
The helpers in `engine/shared.js` are the single source of truth for
|
|
232
|
+
runtime/model and cross-runtime policy resolution. Engine code MUST go through
|
|
233
|
+
them instead of reading config keys directly.
|
|
203
234
|
|
|
204
235
|
| Helper | Chain |
|
|
205
236
|
|--------|-------|
|
|
@@ -207,6 +238,7 @@ directly.
|
|
|
207
238
|
| `resolveCcCli(engine)` | `engine.ccCli` → `engine.defaultCli` → `'copilot'` |
|
|
208
239
|
| `resolveAgentModel(agent, engine)` | `agent.model` → `engine.defaultModel` → undefined |
|
|
209
240
|
| `resolveCcModel(engine)` | `engine.ccModel` → `engine.defaultModel` → undefined |
|
|
241
|
+
| `resolveAgentAllowedTools(config)` | Legacy `config.claude.allowedTools` fleet ceiling → undefined. Despite its legacy namespace, it applies to every selected runtime. |
|
|
210
242
|
| `resolveAgentMaxBudget(agent, engine)` | `agent.maxBudgetUsd` → `engine.maxBudgetUsd`. Honors literal `0`. |
|
|
211
243
|
| `resolveAgentBareMode(agent, engine)` | `agent.bareMode` → `engine.claudeBareMode` → false. Strict null check so per-agent `false` overrides engine `true`. |
|
|
212
244
|
|
|
@@ -285,8 +317,9 @@ codifies are documented in [`docs/harness-propagation.md`](./harness-propagation
|
|
|
285
317
|
|
|
286
318
|
## The "No Runtime-Name Branching" Rule
|
|
287
319
|
|
|
288
|
-
The whole point of this layer: **
|
|
289
|
-
|
|
320
|
+
The whole point of this layer: **orchestration code MUST use the runtime facade,
|
|
321
|
+
capabilities, and adapter hooks rather than concrete imports, harness config
|
|
322
|
+
keys, or `runtime.name === 'claude'` comparisons.**
|
|
290
323
|
|
|
291
324
|
If you find yourself wanting to write `if (runtime.name === 'copilot')` in
|
|
292
325
|
engine code, the correct response is one of:
|
|
@@ -296,19 +329,18 @@ engine code, the correct response is one of:
|
|
|
296
329
|
3. Move the special case into the adapter's `buildArgs` / `buildPrompt` /
|
|
297
330
|
`parseOutput` where it belongs.
|
|
298
331
|
|
|
299
|
-
Tests in `test/unit/runtime-adapters.test.js`
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
code.
|
|
332
|
+
Tests in `test/unit/runtime-adapters.test.js` enforce the contract.
|
|
333
|
+
`test/unit/runtime-boundary.test.js` rejects concrete adapter imports,
|
|
334
|
+
runtime-name branches, and harness-specific invocation config in core paths.
|
|
303
335
|
|
|
304
336
|
## Adding a New Runtime
|
|
305
337
|
|
|
306
|
-
1. Create `engine/runtimes/<name>.js` implementing
|
|
307
|
-
|
|
308
|
-
Set a useful `installHint`.
|
|
338
|
+
1. Create `engine/runtimes/<name>.js` implementing adapter API version `1`.
|
|
339
|
+
Keep all CLI flags, output schemas, workspace quirks, and worker behavior in
|
|
340
|
+
that file. Set a useful `installHint`.
|
|
309
341
|
2. Register it in `engine/runtimes/index.js`:
|
|
310
342
|
```js
|
|
311
|
-
|
|
343
|
+
registerRuntime('<name>', require('./<name>'), { strict: true });
|
|
312
344
|
```
|
|
313
345
|
3. Add unit coverage modeled on `test/unit/runtime-adapters.test.js`.
|
|
314
346
|
|
|
@@ -324,6 +356,6 @@ flags — never special-case in engine code.
|
|
|
324
356
|
- `docs/copilot-cli-schema.md` — empirical Copilot CLI behavior captured during
|
|
325
357
|
the spike that produced the Copilot adapter; cite it when changing
|
|
326
358
|
Copilot-specific capability flags.
|
|
327
|
-
- `engine/runtimes/
|
|
359
|
+
- `engine/runtimes/contract.js` — authoritative adapter contract and facade.
|
|
328
360
|
- `engine/shared.js` `resolveAgentCli` / `resolveCcCli` etc. — the only
|
|
329
361
|
supported way to read runtime / model selection from config.
|
package/docs/skills.md
CHANGED
|
@@ -4,6 +4,8 @@ Agents emit a fenced `skill` block when they discover a durable, reusable workfl
|
|
|
4
4
|
|
|
5
5
|
The engine extracts valid blocks from agent stdout and from inbox findings (`notes/inbox/*.md`) and writes them into the selected runtime's native personal skills directory (e.g. `~/.claude/skills/<name>/SKILL.md` for Claude, the equivalent personal skills directory for Copilot). Project-scoped skills are landed via PR into the matching project's playbook tree.
|
|
6
6
|
|
|
7
|
+
Packaged `scope: minions` skills are synchronized into every registered runtime's personal skill directory during `minions init` and upgrades. A packaged skill replaces an existing same-name personal copy so contract fixes take effect immediately.
|
|
8
|
+
|
|
7
9
|
## Block format
|
|
8
10
|
|
|
9
11
|
````
|
|
@@ -54,7 +54,7 @@ A config that never mentions `workspace_manifest` produces the exact same dispat
|
|
|
54
54
|
| Phase | Location | What it does |
|
|
55
55
|
| ----- | -------- | ------------ |
|
|
56
56
|
| **Dispatch — repo gate** | `engine.js spawnAgent()` (right after project resolution) | Calls `shared.agentCanUseRepo(agent, project)`. Mismatch → `completeDispatch(... FAILURE_CLASS.WORKSPACE_MANIFEST_REPO, agentRetryable: false)` and `cleanupTempAgent` runs. Non-retryable because the structural answer is "widen the manifest or pick a different agent". |
|
|
57
|
-
| **Spawn — tool merge** | `engine.js spawnAgent()` → `_buildAgentSpawnFlags(..., allowedTools)` | `shared.
|
|
57
|
+
| **Spawn — tool merge** | `engine.js spawnAgent()` → `_buildAgentSpawnFlags(..., allowedTools)` | `shared.resolveAgentAllowedTools(config)` preserves the legacy fleet baseline for every selected runtime, then `shared.mergeManifestAllowedTools(baseline, manifest.allowed_tools)` produces the intersection. Result is passed through the Claude / Copilot / Codex adapter option path. Empty manifest list (`[]`) = deny-all. Same resolved option bag is reused on the steering-resume codepath. |
|
|
58
58
|
| **Agent context** | Future work (playbook.js) | Manifest can be surfaced into the agent prompt so the agent sees its declared scope. Today, `shared.resolveAgentManifest(agent)` returns the resolved struct any caller can read. |
|
|
59
59
|
| **URL fetch** | Advisory today | `allowed_external_urls` is validated at config load. No runtime helper function — a future runtime adapter hook can wire URL-fetch intercepts against the manifest. |
|
|
60
60
|
| **Memory scope** | Advisory today | `memory_scope` field is stored in the manifest (`private` = agent only sees its own `knowledge/agents/<id>.md`; `shared` = full team knowledge (current default); `read-only-shared` = reads shared memory but should not write inbox/notes). No runtime enforcement helper — full enforcement at the consolidation layer is future work. |
|
|
@@ -47,6 +47,20 @@ recycle worktree dirs across branches.
|
|
|
47
47
|
→ `git checkout --detach origin/<main>` → mark IDLE.
|
|
48
48
|
- **State** in SQLite (`worktree_pool`); git ops outside the transaction.
|
|
49
49
|
|
|
50
|
+
## Fresh ordinary branch bases
|
|
51
|
+
|
|
52
|
+
When the pool is disabled or has no usable entry, a brand-new ordinary mutating
|
|
53
|
+
branch is created only after `git fetch origin <mainBranch>` succeeds, and its
|
|
54
|
+
base is always `origin/<mainBranch>`. A network or authentication failure stops
|
|
55
|
+
before `git worktree add` or agent spawn and leaves the work item retryable; the
|
|
56
|
+
engine never falls back to a potentially stale local `<mainBranch>`.
|
|
57
|
+
|
|
58
|
+
This fail-closed rule applies only when the requested branch is confirmed absent
|
|
59
|
+
from origin. PR-targeted / `useExistingBranch`, shared-branch, and retry paths
|
|
60
|
+
that intentionally preserve an existing branch continue using that branch
|
|
61
|
+
unchanged. Warm-pool borrowing keeps its existing fetch-and-checkout of
|
|
62
|
+
`origin/<mainBranch>`.
|
|
63
|
+
|
|
50
64
|
## Ownership marker is never "dirty" (#284)
|
|
51
65
|
|
|
52
66
|
The reused-worktree preflight (`assertCleanSharedWorktree`) builds its
|
package/engine/acp-transport.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* engine/acp-transport.js — Shared ACP (Agent Client Protocol) transport layer
|
|
3
3
|
* (P-4c6e8a72, extracted from engine/cc-worker-pool.js).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* module owns
|
|
7
|
-
*
|
|
5
|
+
* ACP-capable harnesses expose a long-lived JSON-RPC server over stdin/stdout.
|
|
6
|
+
* This module owns protocol plumbing that is identical regardless of which
|
|
7
|
+
* adapter or consumer is using the worker:
|
|
8
8
|
*
|
|
9
|
-
* - process spawn (`spawnAcp`) —
|
|
10
|
-
* runtime adapter
|
|
9
|
+
* - process spawn (`spawnAcp`) — delegates binary and argv construction to
|
|
10
|
+
* the selected runtime adapter.
|
|
11
11
|
* - newline-delimited JSON-RPC framing, request/response correlation, and
|
|
12
12
|
* `session/update` notification dispatch (the `Worker` class).
|
|
13
13
|
* - typed error envelope (`ERROR_CODES` / `typedError`).
|
|
@@ -45,6 +45,12 @@
|
|
|
45
45
|
|
|
46
46
|
const { spawn } = require('child_process');
|
|
47
47
|
const crypto = require('crypto');
|
|
48
|
+
const { normalizeExecutionModel } = require('./execution-model');
|
|
49
|
+
const {
|
|
50
|
+
resolveRuntime,
|
|
51
|
+
resolveRuntimeByCapability,
|
|
52
|
+
buildWorkerArgs,
|
|
53
|
+
} = require('./runtimes');
|
|
48
54
|
|
|
49
55
|
// W-mpmwxni2000c25c7-c — typed error codes the transport emits through every
|
|
50
56
|
// failure exit so a consumer (CC streaming handler / doc-chat pool wrapper /
|
|
@@ -56,8 +62,7 @@ const ERROR_CODES = Object.freeze({
|
|
|
56
62
|
// because a transient PATH / fs glitch may recover.
|
|
57
63
|
WORKER_SPAWN_FAILED: 'worker-spawn-failed',
|
|
58
64
|
// The worker process exited DURING the ACP handshake (initialize or
|
|
59
|
-
// session/new)
|
|
60
|
-
// version is too old. Also fires when session/new returns no
|
|
65
|
+
// session/new), or session/new returned no
|
|
61
66
|
// sessionId. Retriable: the caller swaps to a fallback model / a re-auth
|
|
62
67
|
// may unblock the next attempt.
|
|
63
68
|
ACP_HANDSHAKE_FAILED: 'acp-handshake-failed',
|
|
@@ -89,25 +94,29 @@ function typedError(message, code, retriable = true) {
|
|
|
89
94
|
// same knob instead of hardcoding their own copy.
|
|
90
95
|
const WARM_MAX_CONCURRENT = 3;
|
|
91
96
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
function resolveWorkerRuntime(runtime) {
|
|
98
|
+
const adapter = typeof runtime === 'string'
|
|
99
|
+
? resolveRuntime(runtime)
|
|
100
|
+
: (runtime || resolveRuntimeByCapability('acpWorkerPool'));
|
|
101
|
+
if (adapter?.capabilities?.acpWorkerPool !== true) {
|
|
102
|
+
throw new Error(`Runtime "${adapter?.name || '<unknown>'}" does not support ACP workers`);
|
|
103
|
+
}
|
|
104
|
+
return adapter;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Real spawn implementation. Binary resolution and worker argv are both
|
|
108
|
+
// adapter-owned so harness CLI flag changes do not leak into the transport.
|
|
109
|
+
function spawnAcp({ runtime, cwd, env } = {}) {
|
|
110
|
+
const adapter = resolveWorkerRuntime(runtime);
|
|
102
111
|
const workerEnv = env || process.env;
|
|
103
|
-
const resolved =
|
|
112
|
+
const resolved = adapter.resolveBinary({ env: workerEnv });
|
|
104
113
|
if (!resolved || !resolved.bin) {
|
|
105
114
|
throw new Error(
|
|
106
|
-
|
|
115
|
+
`${adapter.name} binary not resolvable -- ${adapter.installHint || `install the ${adapter.name} CLI`}`
|
|
107
116
|
);
|
|
108
117
|
}
|
|
109
118
|
const { bin, native, leadingArgs = [] } = resolved;
|
|
110
|
-
const acpArgs = [...leadingArgs,
|
|
119
|
+
const acpArgs = [...leadingArgs, ...buildWorkerArgs(adapter, { cwd, env: workerEnv })];
|
|
111
120
|
// Mirror engine/spawn-agent.js: native binaries run directly; a non-native
|
|
112
121
|
// (JS entry / npm shim) runs under process.execPath as `node <bin> ...`.
|
|
113
122
|
const execBin = native ? bin : process.execPath;
|
|
@@ -181,6 +190,19 @@ function buildSessionNewParams({ cwd, mcpServers, model, effort }) {
|
|
|
181
190
|
return params;
|
|
182
191
|
}
|
|
183
192
|
|
|
193
|
+
function extractSessionModel(result) {
|
|
194
|
+
for (const value of [
|
|
195
|
+
result?.models?.currentModelId,
|
|
196
|
+
result?.currentModelId,
|
|
197
|
+
result?.modelId,
|
|
198
|
+
result?.model,
|
|
199
|
+
]) {
|
|
200
|
+
const model = normalizeExecutionModel(value);
|
|
201
|
+
if (model) return model;
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
184
206
|
function extractChunkText(content) {
|
|
185
207
|
if (content == null) return '';
|
|
186
208
|
if (typeof content === 'string') return content;
|
|
@@ -246,9 +268,11 @@ function mapAcpToolCallToToolUse(update) {
|
|
|
246
268
|
*/
|
|
247
269
|
class Worker {
|
|
248
270
|
constructor({
|
|
271
|
+
runtime,
|
|
249
272
|
id, model, effort, mcpServers, mcpServersHash, processEnv, processContextHash,
|
|
250
273
|
systemPromptHash, cwd, internals, trace,
|
|
251
274
|
}) {
|
|
275
|
+
this.runtime = resolveWorkerRuntime(runtime);
|
|
252
276
|
this.id = id;
|
|
253
277
|
this.model = model;
|
|
254
278
|
this.effort = effort;
|
|
@@ -283,13 +307,11 @@ class Worker {
|
|
|
283
307
|
async _spawnAndInit() {
|
|
284
308
|
let proc;
|
|
285
309
|
try {
|
|
286
|
-
proc = this._internals.spawnAcp({ cwd: this.cwd, env: this.processEnv });
|
|
310
|
+
proc = this._internals.spawnAcp({ runtime: this.runtime, cwd: this.cwd, env: this.processEnv });
|
|
287
311
|
} catch (err) {
|
|
288
|
-
|
|
289
|
-
// on PATH) or EACCES. Surface as worker-spawn-failed so the consumer
|
|
290
|
-
// can show "install the CLI / fix PATH" guidance.
|
|
312
|
+
const hint = this.runtime.workerErrorHint || `${this.runtime.name} persistent worker failed`;
|
|
291
313
|
throw typedError(
|
|
292
|
-
|
|
314
|
+
`${hint} (${err.message})`,
|
|
293
315
|
ERROR_CODES.WORKER_SPAWN_FAILED,
|
|
294
316
|
true
|
|
295
317
|
);
|
|
@@ -307,11 +329,9 @@ class Worker {
|
|
|
307
329
|
const earlyExitPromise = new Promise((_, reject) => {
|
|
308
330
|
earlyExitReject = (code) => {
|
|
309
331
|
this.killed = true;
|
|
310
|
-
|
|
311
|
-
// always missing `copilot login`, stale CLI, or daemon crash on
|
|
312
|
-
// boot). Retriable so re-auth or a CLI upgrade can recover.
|
|
332
|
+
const hint = this.runtime.workerErrorHint || `${this.runtime.name} persistent worker failed`;
|
|
313
333
|
const err = typedError(
|
|
314
|
-
|
|
334
|
+
`${hint} (exit ${code})`,
|
|
315
335
|
ERROR_CODES.ACP_HANDSHAKE_FAILED,
|
|
316
336
|
true
|
|
317
337
|
);
|
|
@@ -327,8 +347,9 @@ class Worker {
|
|
|
327
347
|
// proc 'error' event fires when the OS can't actually start the child
|
|
328
348
|
// (ENOENT after a successful spawn() call, etc.). Treat as a spawn
|
|
329
349
|
// failure even though we made it past the synchronous spawn() above.
|
|
350
|
+
const hint = this.runtime.workerErrorHint || `${this.runtime.name} persistent worker failed`;
|
|
330
351
|
const wrapped = typedError(
|
|
331
|
-
|
|
352
|
+
`${hint} (${err.message})`,
|
|
332
353
|
ERROR_CODES.WORKER_SPAWN_FAILED,
|
|
333
354
|
true
|
|
334
355
|
);
|
|
@@ -350,11 +371,13 @@ class Worker {
|
|
|
350
371
|
earlyExitPromise,
|
|
351
372
|
]);
|
|
352
373
|
this.sessionId = result && result.sessionId;
|
|
374
|
+
this.currentModel = extractSessionModel(result) || normalizeExecutionModel(this.model);
|
|
353
375
|
if (!this.sessionId) {
|
|
354
376
|
// Handshake completed without an error but the daemon didn't hand
|
|
355
377
|
// back a sessionId — protocol violation or partial init failure.
|
|
378
|
+
const hint = this.runtime.workerErrorHint || `${this.runtime.name} persistent worker failed`;
|
|
356
379
|
throw typedError(
|
|
357
|
-
|
|
380
|
+
`${hint} (session/new returned no sessionId)`,
|
|
358
381
|
ERROR_CODES.ACP_HANDSHAKE_FAILED,
|
|
359
382
|
true
|
|
360
383
|
);
|
|
@@ -369,7 +392,7 @@ class Worker {
|
|
|
369
392
|
// Post-handshake exit = the daemon died mid-conversation. Retriable
|
|
370
393
|
// because the next call will cold-spawn a fresh worker.
|
|
371
394
|
const err = typedError(
|
|
372
|
-
|
|
395
|
+
`${this.runtime.name} persistent worker process exited`,
|
|
373
396
|
ERROR_CODES.WORKER_DIED,
|
|
374
397
|
true
|
|
375
398
|
);
|
|
@@ -563,7 +586,8 @@ class Worker {
|
|
|
563
586
|
this._notify('session/cancel', { sessionId: this.inflight.sessionId });
|
|
564
587
|
}
|
|
565
588
|
|
|
566
|
-
async newSession(
|
|
589
|
+
async newSession(options = {}) {
|
|
590
|
+
const { mcpServers, systemPromptHash, model, effort, cwd } = options;
|
|
567
591
|
// Cancel any inflight before swapping the underlying session.
|
|
568
592
|
if (this.inflight) {
|
|
569
593
|
this.cancel();
|
|
@@ -582,8 +606,9 @@ class Worker {
|
|
|
582
606
|
// Bug B (issue #2479): if the caller is rotating the session because the
|
|
583
607
|
// system prompt changed, they may also be passing a fresh model/effort —
|
|
584
608
|
// update bookkeeping BEFORE session/new so the new fields land on the
|
|
585
|
-
// daemon.
|
|
586
|
-
|
|
609
|
+
// daemon. An explicit `model: undefined` clears a prior pooled override and
|
|
610
|
+
// lets the new session select its own default.
|
|
611
|
+
if (Object.prototype.hasOwnProperty.call(options, 'model')) this.model = model;
|
|
587
612
|
if (effort !== undefined) this.effort = effort;
|
|
588
613
|
// agent-worker-pool.js reuses a warm worker across dispatches with
|
|
589
614
|
// different cwds (unlike cc-worker-pool.js, whose tabs never change
|
|
@@ -593,6 +618,7 @@ class Worker {
|
|
|
593
618
|
cwd: this.cwd, mcpServers: mcpServers || [], model: this.model, effort: this.effort,
|
|
594
619
|
}));
|
|
595
620
|
this.sessionId = result && result.sessionId;
|
|
621
|
+
this.currentModel = extractSessionModel(result) || normalizeExecutionModel(this.model);
|
|
596
622
|
this.systemPromptHash = systemPromptHash;
|
|
597
623
|
this.firstSystemPromptSent = false;
|
|
598
624
|
}
|
|
@@ -623,11 +649,13 @@ module.exports = {
|
|
|
623
649
|
ERROR_CODES,
|
|
624
650
|
typedError,
|
|
625
651
|
spawnAcp,
|
|
652
|
+
resolveWorkerRuntime,
|
|
626
653
|
killImmediate,
|
|
627
654
|
hashMcpServers,
|
|
628
655
|
hashProcessContext,
|
|
629
656
|
buildTrustedSessionContext,
|
|
630
657
|
buildSessionNewParams,
|
|
658
|
+
extractSessionModel,
|
|
631
659
|
extractChunkText,
|
|
632
660
|
mapAcpToolCallToToolUse,
|
|
633
661
|
Worker,
|
package/engine/ado-comment.js
CHANGED
|
@@ -106,6 +106,7 @@ async function _defaultAcquireToken() {
|
|
|
106
106
|
* @param {string} args.agentId minions agent id (marker)
|
|
107
107
|
* @param {string} args.kind comment kind (marker)
|
|
108
108
|
* @param {string} [args.workItemId] originating work-item id (marker)
|
|
109
|
+
* @param {string} [args.model] concrete runtime execution model
|
|
109
110
|
* @param {number} [args.timeoutMs=30000]
|
|
110
111
|
* @param {Function} [args.acquireToken] () => Promise<string> — injectable token source
|
|
111
112
|
* @param {Function} [args.fetchImpl=fetch] injectable fetch
|
|
@@ -120,6 +121,7 @@ async function postAdoPrComment({
|
|
|
120
121
|
agentId,
|
|
121
122
|
kind,
|
|
122
123
|
workItemId,
|
|
124
|
+
model,
|
|
123
125
|
resolved = false,
|
|
124
126
|
timeoutMs = 30000,
|
|
125
127
|
acquireToken = _defaultAcquireToken,
|
|
@@ -131,7 +133,7 @@ async function postAdoPrComment({
|
|
|
131
133
|
_validatePrNumber(prNumber);
|
|
132
134
|
|
|
133
135
|
// buildMinionsCommentBody validates marker fields and throws on bad input.
|
|
134
|
-
const finalBody = buildMinionsCommentBody({ agentId, kind, workItemId, body });
|
|
136
|
+
const finalBody = buildMinionsCommentBody({ agentId, kind, workItemId, model, body });
|
|
135
137
|
|
|
136
138
|
const token = await acquireToken();
|
|
137
139
|
if (!token || typeof token !== 'string') {
|