@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 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
- let count = 0;
1977
+ const worktreeRoots = new Map();
1978
1978
  for (const p of projects) {
1979
- const root = p.localPath ? path.resolve(p.localPath) : null;
1980
- if (!root) continue;
1981
- const wtRoot = path.resolve(root, config.engine?.worktreeRoot || shared.ENGINE_DEFAULTS.worktreeRoot);
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
- // W-mpmwxni2000c25c7-a — accept both the legacy 'unknown-model' errorClass
5349
- // (still emitted by the dashboard preflight at _preflightModelCheck) and
5350
- // the new 'model-unavailable' code returned by copilot.parseError for
5351
- // invalid-model responses. Suppress in either case — the edit already
5352
- // landed, so the stale model error shouldn't surface to the user.
5353
- if (ccError.errorClass !== 'unknown-model' && ccError.errorClass !== 'model-unavailable') return false;
5354
- return String(ccError.runtime || '').toLowerCase() === 'copilot';
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, the flagged review comment was an author-note, etc.
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
@@ -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/index.js` | Adapter registry. `resolveRuntime(name)`, `listRuntimes()`, `registerRuntime()`. Engine code MUST go through these. |
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
- | `buildSpawnFlags(opts)` | `string[]` | Optional; engine-side flags consumed by `engine/spawn-agent.js` (overrides the default `_buildAgentSpawnFlags`). |
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
- | `detectPermissionGate`, `getPromptDeliveryMode`, `usesSystemPromptFile`, `classifyFailure` | misc | Adapter-owned policy that engine code reads through accessors instead of branching on `runtime.name`. |
56
-
57
- The contract surface is documented inline at the top of `engine/runtimes/claude.js`
58
- (see the JSDoc block at the start of that file). Keep that block authoritative —
59
- this table is the human-readable mirror.
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` | ✓ | ✓ | | Runtime accepts `images: [{mimeType, dataBase64}]` and delivers them to the CLI. Claude: Anthropic Messages base64 content-block envelope via `--input-format stream-json`. Copilot: base64 payloads materialized to tmp files and passed as `--attachment <path>` (W-mqv7324u0021db5d). When false, `_resolveImageOpts` returns a typed `model-unavailable` error. |
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
- Six helpers in `engine/shared.js` are the single source of truth for runtime/model
201
- resolution. Engine code MUST go through them instead of reading config keys
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: **engine code MUST gate behavior on
289
- `runtime.capabilities.*` flags, not on `runtime.name === 'claude'` comparisons.**
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` and `test/unit/copilot-adapter.test.js`
300
- enforce the contract; the source-inspection tests in `test/unit.test.js` look
301
- for `resolveRuntime(...)` call sites and reject runtime-name comparisons in new
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 the full adapter contract.
307
- Copy `claude.js` as the template and override only the members that differ.
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
- registry.set('<name>', require('./<name>'));
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/claude.js` JSDoc header — authoritative adapter contract.
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.mergeManifestAllowedTools(claudeConfig.allowedTools, manifest.allowed_tools)` produces the intersection of the runtime baseline and the manifest list. Result is passed as `--allowedTools <csv>` to Claude / Copilot / Codex, so the CLI itself enforces the narrowed surface. Empty manifest list (`[]`) = deny-all. Same merge runs on the steering-resume codepath. |
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
@@ -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
- * `copilot --acp` is a long-lived JSON-RPC server over stdin/stdout. This
6
- * module owns the protocol-level plumbing that is identical regardless of
7
- * WHO is pooling the worker (a CC/doc-chat tab vs. a fleet agent dispatch):
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`) — resolves the `copilot` binary via the
10
- * runtime adapter and launches it with `--acp`.
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) usually `copilot login` is incomplete or the CLI
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
- // Real spawn implementation — resolves `copilot` through the runtime adapter
93
- // so every ACP transport consumer gets the SAME native/leadingArgs contract
94
- // as the only other spawn site (engine/spawn-agent.js ~line 595). A bare
95
- // `spawn('copilot', {shell:false})` cannot exec a `.cmd`/`.ps1`/extensionless
96
- // npm shim on Windows — it ENOENTs instantly (copilot issue #2370 /
97
- // W-mqid8uev). resolveBinary() resolves the real JS entry
98
- // (node_modules/@github/copilot/index.js) and reports whether to spawn it
99
- // directly (native) or under the current Node process (shim).
100
- function spawnAcp({ cwd, env } = {}) {
101
- const copilot = require('./runtimes/copilot');
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 = copilot.resolveBinary({ env: workerEnv });
112
+ const resolved = adapter.resolveBinary({ env: workerEnv });
104
113
  if (!resolved || !resolved.bin) {
105
114
  throw new Error(
106
- `copilot binary not resolvable -- ${copilot.installHint || 'install the GitHub Copilot CLI'}`
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, '--acp', '--allow-all', '--max-autopilot-continues', '1'];
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
- // spawn() threw synchronously typically ENOENT (copilot binary not
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
- `copilot --acp failed -- ensure copilot CLI >=1.0.46 and copilot login is complete (${err.message})`,
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
- // Early exit DURING the handshake = acp-handshake-failed (almost
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
- `copilot --acp failed -- ensure copilot CLI >=1.0.46 and copilot login is complete (exit ${code})`,
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
- `copilot --acp failed -- ensure copilot CLI >=1.0.46 and copilot login is complete (${err.message})`,
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
- 'copilot --acp failed -- session/new returned no sessionId',
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
- 'copilot --acp process exited',
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({ mcpServers, systemPromptHash, model, effort, cwd }) {
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. Falls through to whatever we already had when callers omit them.
586
- if (model !== undefined) this.model = model;
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,
@@ -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') {