@yemi33/minions 0.1.2382 → 0.1.2384

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.
Files changed (51) hide show
  1. package/bin/minions.js +1 -0
  2. package/dashboard/js/refresh.js +2 -1
  3. package/dashboard/js/settings.js +1 -1
  4. package/dashboard.js +37 -19
  5. package/docs/completion-reports.md +20 -1
  6. package/docs/keep-processes.md +7 -2
  7. package/docs/live-checkout-mode.md +7 -7
  8. package/docs/managed-spawn.md +14 -1
  9. package/docs/runtime-adapters.md +61 -26
  10. package/docs/skills.md +2 -0
  11. package/docs/workspace-manifests.md +1 -1
  12. package/docs/worktree-lifecycle.md +36 -0
  13. package/engine/acp-transport.js +273 -58
  14. package/engine/ado-comment.js +3 -1
  15. package/engine/agent-worker-pool.js +278 -54
  16. package/engine/cc-worker-pool.js +12 -3
  17. package/engine/cleanup.js +15 -11
  18. package/engine/cli.js +56 -28
  19. package/engine/comment-format.js +51 -14
  20. package/engine/create-pr-worktree.js +57 -79
  21. package/engine/dispatch.js +7 -2
  22. package/engine/execution-model.js +68 -0
  23. package/engine/gh-comment.js +6 -3
  24. package/engine/github.js +6 -7
  25. package/engine/keep-process-sweep.js +131 -9
  26. package/engine/lifecycle.js +126 -45
  27. package/engine/live-checkout.js +4 -2
  28. package/engine/llm.js +59 -83
  29. package/engine/managed-spawn.js +112 -5
  30. package/engine/memory-store.js +3 -1
  31. package/engine/playbook.js +4 -6
  32. package/engine/pooled-agent-process.js +512 -45
  33. package/engine/pr-action.js +6 -8
  34. package/engine/process-utils.js +154 -18
  35. package/engine/runtimes/claude.js +94 -25
  36. package/engine/runtimes/codex.js +1 -0
  37. package/engine/runtimes/contract.js +239 -0
  38. package/engine/runtimes/copilot.js +97 -9
  39. package/engine/runtimes/index.js +37 -9
  40. package/engine/shared.js +349 -82
  41. package/engine/spawn-agent.js +85 -116
  42. package/engine/spawn-phase-watchdog.js +8 -37
  43. package/engine/timeout.js +14 -10
  44. package/engine/worktree-gc.js +55 -46
  45. package/engine.js +418 -241
  46. package/package.json +1 -1
  47. package/playbooks/fix.md +20 -0
  48. package/playbooks/review.md +2 -0
  49. package/playbooks/shared-rules.md +2 -2
  50. package/prompts/cc-system.md +11 -11
  51. 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');
@@ -175,7 +175,8 @@ const RENDER_VERSIONS = {
175
175
  // Bumped to 3 by W-mqk2s8q1 (Advanced → Diagnostics: relocated the diag button).
176
176
  // Bumped to 4 by W-mqrdavob (Agents table: per-agent charter editor column).
177
177
  // Bumped to 5 by W-mr0qs0vw (Worker Pool: CC session idle-timeout field).
178
- settings: 5,
178
+ // Bumped to 6 for the fleet ACP pool opt-in default and warning copy.
179
+ settings: 6,
179
180
  };
180
181
  const _sectionCache = {};
181
182
  const _lastValueByKey = {};
@@ -448,7 +448,7 @@ async function openSettings() {
448
448
  '<div class="settings-pane-sub">Persistent CC worker pool and git worktree provisioning. Tune these only if you see worktree-create timeouts or slow CC cold-spawns.</div>' +
449
449
  '<div class="settings-stack" style="margin-bottom:12px">' +
450
450
  settingsToggle('CC Worker Pool', 'set-ccUseWorkerPool', (e.ccUseWorkerPool === undefined ? ((e.ccCli || e.defaultCli) === 'copilot') : !!e.ccUseWorkerPool), 'Route Command Center / doc-chat through a persistent copilot --acp worker per tab instead of spawning a fresh CLI per turn. Copilot-only (Agent Client Protocol transport); Claude does not implement ACP, so this toggle has no effect when CC runtime is Claude. Default ON for copilot (cold-spawn ~20s on Windows); forced OFF for non-copilot CC runtimes regardless of this toggle.') +
451
- settingsToggle('Agent Worker Pool', 'set-agentUseWorkerPool', e.agentUseWorkerPool !== false, 'Route fleet Copilot dispatches through persistent copilot --acp workers. Default ON; runtimes without ACP support ignore this setting.') +
451
+ settingsToggle('Agent Worker Pool', 'set-agentUseWorkerPool', e.agentUseWorkerPool === true, 'Route fleet Copilot dispatches through persistent copilot --acp workers. Default OFF while the newer fleet-pool lifecycle path stabilizes; runtimes without ACP support ignore this setting.') +
452
452
  // W-mq6f2fe0000557fa — opt-in: auto-kill orphan spawn-agent processes
453
453
  // holding a stuck worktree's cwd. Gated by safe-reap criteria (cmdline
454
454
  // matches spawn-agent.js + basename + age > agentTimeout * 2). Default
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,
@@ -13623,7 +13637,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
13623
13637
  }
13624
13638
  }},
13625
13639
 
13626
- { method: 'POST', path: '/api/pr-action/offer-create-pr', desc: 'Offer a "Create PR" follow-up chip after Command Center makes a LOCAL code edit itself in a configured project (P-cc-createpr). Validates the project has uncommitted working-tree changes (git status --porcelain in project.localPath) and returns a create-pr follow-up chip — same CC-mediated shape as the pr-action chips, so the existing dashboard chip renderer/click handler light up unchanged. Returns {ok, hasChanges, branch, changedFileCount, followups}; followups is [] when there is nothing to PR. READ-ONLY: never stages/commits/pushes — clicking the chip routes a fresh CC turn that does the commit/push/PR-create/link.', params: 'project (configured project name), branch (optional — defaults to the working-tree current branch), contextOnly (optional bool — link the new PR as context-only instead of auto-managed)', handler: async (req, res) => {
13640
+ { method: 'POST', path: '/api/pr-action/offer-create-pr', desc: 'Offer a "Create PR" follow-up chip for pre-existing operator changes in a configured project. Validates the project has uncommitted working-tree changes (git status --porcelain in project.localPath) and returns a create-pr follow-up chip. Returns {ok, hasChanges, branch, changedFileCount, followups}; followups is [] when there is nothing to PR. READ-ONLY: never stages/commits/pushes.', params: 'project (configured project name), branch (optional — defaults to the working-tree current branch), contextOnly (optional bool — link the new PR as context-only instead of auto-managed)', handler: async (req, res) => {
13627
13641
  const body = await readBody(req);
13628
13642
  try {
13629
13643
  reloadConfig();
@@ -13644,10 +13658,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
13644
13658
  const hasChanges = changedFileCount > 0;
13645
13659
  const branch = (typeof body?.branch === 'string' && body.branch.trim()) || currentBranch || '';
13646
13660
  // The Create-PR action follows the SAME checkout pattern as a dispatch
13647
- // for this project: 'live' commits in the live checkout; 'worktree'
13648
- // routes the changes through an isolated worktree (so nothing is ever
13649
- // committed in the operator's live main). See engine/create-pr-worktree.js.
13650
- const checkoutMode = shared.resolveCheckoutMode(project);
13661
+ // for this project: 'live' uses the live-checkout flow; 'worktree'
13662
+ // copies changes into an isolated worktree and preserves the source.
13663
+ const checkoutMode = shared.resolveCheckoutMode(project, shared.WORK_TYPE.IMPLEMENT);
13651
13664
  // Resolve the project's actual main/base branch instead of trusting
13652
13665
  // whatever `git rev-parse --abbrev-ref HEAD` returned as a safe base —
13653
13666
  // the live checkout may be sitting on a stale/topic branch whose
@@ -13663,7 +13676,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
13663
13676
  }
13664
13677
  }},
13665
13678
 
13666
- { method: 'POST', path: '/api/pr-action/prepare-create-pr-worktree', desc: 'Stage a worktree-mode project\'s uncommitted live-checkout changes into an ISOLATED git worktree and restore the live checkout clean (P-cc-createpr-checkout-mode). The Create-PR action calls this for projects whose checkoutMode is "worktree" so the subsequent commit/push/PR happen off the operator tree — never committing to the live main. Safe ordering: the worktree is populated and verified BEFORE the live checkout is touched; on apply failure the worktree is removed and the live checkout is left untouched. Returns {ok, worktreePath, branch, baseBranch, baseSha, trackedChanged, untrackedCount}; {ok:false, reason:"no-changes"} when the tree is clean.', params: 'project (configured project name), branch (optional preferred branch name)', handler: async (req, res) => {
13679
+ { method: 'POST', path: '/api/pr-action/prepare-create-pr-worktree', desc: 'Copy a worktree-mode project\'s pre-existing operator changes into an ISOLATED git worktree without modifying the source checkout. Subsequent commit/push/PR operations happen only in the returned worktree. Returns {ok, worktreePath, branch, baseBranch, baseSha, trackedChanged, untrackedCount, sourceCheckoutPreserved}; {ok:false, reason:"no-changes"} when the tree is clean.', params: 'project (configured project name), branch (optional preferred branch name)', handler: async (req, res) => {
13667
13680
  const body = await readBody(req);
13668
13681
  try {
13669
13682
  reloadConfig();
@@ -13674,6 +13687,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
13674
13687
  if (!project.localPath) return jsonReply(res, 400, { error: `project ${projectName} has no localPath` }, req);
13675
13688
  const result = await createPrWorktree.prepareCreatePrWorktree({
13676
13689
  project,
13690
+ projects: shared.getProjects(CONFIG),
13677
13691
  branch: typeof body?.branch === 'string' ? body.branch : undefined,
13678
13692
  minionsDir: MINIONS_DIR,
13679
13693
  engine: CONFIG.engine,
@@ -13693,7 +13707,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
13693
13707
  const project = projectName ? shared.getProjects(CONFIG).find(p => p && p.name === projectName) : null;
13694
13708
  const worktreePath = typeof body?.worktreePath === 'string' ? body.worktreePath.trim() : '';
13695
13709
  if (!worktreePath) return jsonReply(res, 400, { error: 'worktreePath is required' }, req);
13696
- const result = await createPrWorktree.cleanupCreatePrWorktree({ worktreePath, project });
13710
+ const result = await createPrWorktree.cleanupCreatePrWorktree({
13711
+ worktreePath,
13712
+ project,
13713
+ projects: shared.getProjects(CONFIG),
13714
+ });
13697
13715
  return jsonReply(res, 200, result, req);
13698
13716
  } catch (e) {
13699
13717
  return jsonReply(res, e.statusCode || 400, { error: e.message }, req);
@@ -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
@@ -22,7 +22,7 @@ Set `meta.keep_processes: true` on the WI; agent writes `agents/<id>/keep-pids.j
22
22
  {
23
23
  "pids": [12345], // ≤5 entries
24
24
  "purpose": "gradle daemon for follow-up test WI",
25
- "cwd": "D:/repos/my-android-app", // MUST be a real git worktree
25
+ "cwd": "<MINIONS_AGENT_CWD>", // assigned dispatch root or a subdirectory
26
26
  "ports": [8080], // advisory, engine doesn't bind
27
27
  "expires_at": "2026-04-29T00:00:00Z", // TTL ≤1440 min; default 60
28
28
  "written_by": "ripley",
@@ -42,6 +42,11 @@ Set `meta.keep_processes: true` on the WI; agent writes `agents/<id>/keep-pids.j
42
42
 
43
43
  - **PIDs:** ≤5 per sidecar.
44
44
  - **TTL:** ≤1440 min (24h). Default 60.
45
- - **Cwd:** must exist and resolve to inside a git worktree (`requireGitWorkdir: true`). Monorepo subdirs walk up to `gitWorktreeMaxParentDepth` (default 6).
45
+ - **Cwd:** must exist, resolve inside a git worktree, and stay at or below the current dispatch root from `MINIONS_AGENT_CWD`. Monorepo subdirs are allowed. In worktree mode, pointing at `project.localPath` is rejected before the wrapper decides which descendants survive. `keep_processes` dispatches use the cold spawn wrapper rather than the ACP pool so descendant ownership remains enforceable.
46
+
47
+ After acceptance, the engine adds project/work-type/checkout-mode/dispatch-root
48
+ provenance to the sidecar. Boot sweep uses it to kill and unlink any retained
49
+ process whose cwd now overlaps a worktree-mode operator checkout (including
50
+ legacy sidecars from the old read-only-in-`localPath` behavior).
46
51
 
47
52
  See also: [`CLAUDE.md`](../CLAUDE.md) → **Agent Spawn**, [managed-spawn.md](managed-spawn.md).
@@ -78,7 +78,7 @@ This is **DESTRUCTIVE** — tracked WIP and untracked files can be permanently u
78
78
 
79
79
  #### 2c. Stale `.git/index.lock` self-heal (W-mr3tayu4)
80
80
 
81
- Before any preflight git command runs, `prepareLiveCheckout` calls the shared, age-gated `shared.removeStaleIndexLock(localPath)` to clear a leftover `.git/index.lock` from a crashed/killed git process. Live mode never resets/cleans the operator tree, so nothing else would clear it, and — since `LIVE_CHECKOUT_FAILED` is retryable — every automatic retry used to hit the same stale lock and fail identically forever until an operator deleted it by hand. The remover only touches locks older than 5 minutes, so a lock held by a currently-running git process is never removed out from under it. `engine.js`'s own worktree-mode lock cleanup now delegates to the same shared helper.
81
+ Before any preflight git command runs, `prepareLiveCheckout` calls the shared, age-gated `shared.removeStaleIndexLock(localPath)` to clear a leftover `.git/index.lock` from a crashed/killed git process. Live mode never resets/cleans the operator tree, so nothing else would clear it, and — since `LIVE_CHECKOUT_FAILED` is retryable — every automatic retry used to hit the same stale lock and fail identically forever until an operator deleted it by hand. The remover only touches locks older than 5 minutes, so a lock held by a currently-running git process is never removed out from under it. Worktree-mode setup never calls this helper against `project.localPath`.
82
82
 
83
83
  ### 3. No implicit remote sync or force checkout
84
84
 
@@ -214,7 +214,7 @@ Enable it fleet-wide via **Settings → "Live-checkout auto-base-repair (fleet-w
214
214
 
215
215
  ## Dispatch-end auto-restore
216
216
 
217
- Live-mode agents run **in-place** in the operator's checkout, so when a dispatch ends the engine switches the tree back to the ref it was on before the agent ran. This runs on **every** terminal result — success, failure, timeout, crash — and also on **every restart-recovery reaping path**: the engine-restart re-attach orphan-completion path (`engine/cli.js`) AND the `engine/timeout.js` paths that reap a dispatch the engine lost the process handle for (`completeFromOutput` + the orphan sweep). All three route through the shared `maybeRestoreLiveCheckoutFromRecord(item, …)` helper (which fires from the *persisted* dispatch record — `originalRef`'s presence is the live-mode signal), so a live agent that spans an engine restart and finishes afterward never strands the checkout on the agent's branch. (Previously the timeout.js reaping paths had **no** restore wiring, which is exactly how a restart-spanning live dispatch left the operator stuck on `work/W-…`.)
217
+ Live-mode agents run **in-place** in the operator's checkout, so when a dispatch ends the engine switches the tree back to the ref it was on before the agent ran. This runs on terminal results and restart-recovery reaping paths through `maybeRestoreLiveCheckoutFromRecord`. Restore now requires both persisted `checkoutMode: "live"` provenance (legacy records may use `originalRef`) and a current configured mode that still resolves live for the dispatch type. A worktree marker or current worktree mode fails closed before any Git probe.
218
218
 
219
219
  - **Original-ref capture.** `prepareLiveCheckout` records the operator's starting ref *before* the first checkout: `git symbolic-ref --short HEAD` → `{ originalRef:<branch>, originalRefType:'branch' }`, falling back to `git rev-parse HEAD` → `{ originalRef:<sha>, originalRefType:'detached' }`. `spawnAgent` persists `originalRef` / `originalRefType` onto the dispatch record via `mutateDispatch`, so the restore survives an engine restart, where the in-memory spawn closure is gone and only the persisted record remains.
220
220
  - **Self-healing dirty recovery (PL-live-checkout-reliability-hardening).** Before the plain checkout, if the tree is **dirty AND HEAD is on the agent branch** (and that branch differs from `originalRef`), the dirt is provably **agent-authored** — the engine created that branch and verified the tree clean before switching to it. The leftovers are committed onto the **agent branch** (`git add -A` + `git commit --no-verify -m "minions: auto-save agent WIP (dispatch …)"`), so the plain `git checkout <originalRef>` then succeeds and the operator tree returns clean. This is the fix for the *"no recovery from dirty checkout"* deadlock, where an agent's crash-leftover WIP refused the restore, stranded the tree dirty on the agent branch, and then made **every future WI** for that project fail `LIVE_CHECKOUT_DIRTY` until a human cleaned it. It only ever mutates the engine-created branch, never the operator's branch, never discards (the WIP lands as a visible, revertable commit / on its PR), and gitignored artifacts are never staged (so a tree dirty only with ignored build output never reaches here). Best-effort: a failed auto-commit falls through to the manual-recovery alert below.
@@ -229,7 +229,7 @@ The core invariant holds end-to-end through restore: **the engine only ever swit
229
229
 
230
230
  ## Create-PR chip safety (CC-initiated, separate from the dispatch path)
231
231
 
232
- The Command Center **"Create PR"** chip (`dashboard.js` `POST /api/pr-action/offer-create-pr` `engine/pr-action.js#buildCreatePrFollowups`) is a **separate, ad-hoc path** that opens a PR for a local edit Command Center just made in a live-checkout project. It does **not** go through `prepareLiveCheckout`, so the dirty-tree / orphaned-branch / mid-operation defenses above (Guarantees 2 & 5, dispatch-start self-heal) do **not** apply to it. This path needs its own equivalent safety contract, because it shares the same failure mode: the live checkout may be sitting on a stale/topic branch left over from a previous run, a manually-checked-out branch, or any other non-main state.
232
+ Command Center never creates edits inside a configured project's `localPath`. The **"Create PR"** chip is only for pre-existing operator changes. In worktree mode, `prepareCreatePrWorktree` creates a fresh branch from `origin/<mainBranch>` (falling back to local main when no remote exists), applies only the uncommitted diff/untracked files, and leaves the source checkout unchanged. Topic commits from the operator's current branch cannot leak into the PR. Live-mode Create-PR remains a deliberate in-place flow and retains the base-branch protections below.
233
233
 
234
234
  **The bug it fixes (W-mr3jbpru).** The old live-mode chip message keyed its branch instruction off `git rev-parse --abbrev-ref HEAD` (the *current* branch) and only told the agent to branch off cleanly **if** that current branch already happened to be the project's main branch. If the checkout was on any other branch, the message told the agent to commit the new changes **directly onto that branch** — silently carrying all of that branch's unrelated commits into the new PR. There was no engine-side verification that the current branch was clean or at parity with main; it was trusted blindly. A user hit exactly this: a Create-PR chip PR ended up built on top of a leftover topic branch and carried unrelated diffs.
235
235
 
@@ -240,7 +240,7 @@ The Command Center **"Create PR"** chip (`dashboard.js` → `POST /api/pr-action
240
240
  3. If `git stash pop` reports a merge conflict (the stashed diff doesn't cleanly apply onto the fresh main tip), the message instructs the agent to **stop and surface the conflict** to the user rather than force-resolving it silently.
241
241
  4. The message **names the resolved main branch explicitly** (e.g. `origin/android`) instead of leaving "the project main branch" vague, so the always-branch-off-resolved-main rule is stated unambiguously.
242
242
 
243
- This mirrors, in CC-instruction form, what `prepareLiveCheckout` enforces engine-side for the dispatch path: never reuse whatever branch the live checkout happened to be on as the base for new work. The **worktree-mode** branch of `buildCreatePrFollowups` is unaffected — that path already routes the changes through an isolated worktree (`engine/create-pr-worktree.js`) so nothing is ever committed on the operator's `main`.
243
+ This mirrors, in CC-instruction form, what `prepareLiveCheckout` enforces engine-side for the dispatch path: never reuse whatever branch the live checkout happened to be on as the base for new work. The **worktree-mode** branch copies existing changes to an isolated worktree without modifying the operator checkout.
244
244
 
245
245
  ## Operator workflow
246
246
 
@@ -362,11 +362,11 @@ Live-checkout mode is deliberately small. These are NOT supported and will not b
362
362
  |---|---|
363
363
  | `engine/shared.js` — `CHECKOUT_MODES`, `validateCheckoutMode`, `resolveCheckoutMode`, `isLiveCheckoutProject` | Enum + validator + back-compat resolver (P-a3f9b201; consolidated W-mqiaw974). |
364
364
  | `engine/shared.js` — `resolveLiveCheckoutAutoReset` + `ENGINE_DEFAULTS.liveCheckoutAutoReset` | Pure precedence resolver (per-project boolean > fleet-wide engine default > false) + the fleet-wide default (OFF as of W-mrawgw4q000a6bff — was ON as of W-mqzbbhn2). Gates the dirty-tree auto-reset in `prepareLiveCheckout` (W-mqvejug6000eeb20); in `engine.js spawnAgent` only fires as a fallback after auto-stash fails/is disabled (W-mrawgw4q000a6bff). |
365
- | `engine/shared.js` — `resolveSpawnPaths` | Returns `{ cwd: localPath, worktreeRootDir: null, liveMode: true }` for live projects (P-a3f9b202). |
365
+ | `engine/shared.js` — `resolveSpawnPaths` | Returns `{ cwd: localPath, worktreeRootDir: null, liveMode: true }` for live dispatches; project-bound worktree-mode read-only types request detached worktree placement. |
366
366
  | `engine/live-checkout.js` — `prepareLiveCheckout` | Pure helper by default: pre-mutation mid-operation / detached-HEAD preflight, original-ref capture, dirty check, **already-on-branch fast path**, `refs/heads/<branch>` existence check, branch resolution from HEAD (no fetch — issue #226), **no-half-switch + `blob-fetch`/`worktree-conflict` classification**, and opt-in destructive WIP cleanup (`reset --hard HEAD` + `clean -fd` + re-check + audit note) that preserves the branch and committed history. Git probes use a **50 MB maxBuffer**. |
367
367
  | `engine/live-checkout.js` — `restoreLiveCheckoutAtDispatchEnd` | Dispatch-end auto-restore (plain `git checkout <originalRef>`, never `--force`/reset/clean/stash, best-effort) + **self-healing dirty recovery** (auto-commit agent WIP onto the agent branch) + `live-checkout-failed-<dispatchId>` terminal-failure alert + `live-checkout-branch-<dispatchId>` fallback notify (now also on unexpected restore errors) (P-d9e6b2c4; self-heal PL-live-checkout-reliability-hardening). |
368
368
  | `engine/live-checkout.js` — `resolveLiveCheckoutAutoStash`, `performLiveCheckoutAutoStash`, `applyLiveCheckoutAutoStash` | Auto-stash resolver (per-project boolean, then fleet setting, then default true) + `git stash push --include-untracked` runner + stash/re-preflight orchestration. Engine never auto-pops (W-mqtvnnj1000357fa). |
369
- | `engine/live-checkout.js` — `maybeRestoreLiveCheckoutFromRecord` | Shared wrapper that fires the dispatch-end restore from a persisted dispatch record; used by `cli.js` + both `timeout.js` reaping paths so a restart-spanning live dispatch is never stranded (PL-live-checkout-reliability-hardening). |
369
+ | `engine/live-checkout.js` — `maybeRestoreLiveCheckoutFromRecord` | Shared persisted-record restore wrapper; requires explicit live provenance plus current live configuration and fails closed for worktree mode. |
370
370
  | `engine.js` — `spawnAgent` live-mode block | Calls `prepareLiveCheckout`, delegates opt-in auto-stash to `applyLiveCheckoutAutoStash` on a dirty tree (W-mqtvnnj1000357fa), handles dirty / throw branches, gates `git worktree add` on `!liveMode` (P-a3f9b204). |
371
371
  | `engine.js` — `spawnAgent` mid-op / detached-HEAD refusal block | Emits `LIVE_CHECKOUT_MID_OPERATION`, writes `live-checkout-blocked-<wi-id>` alert, stamps `_pendingReason: 'live_checkout_mid_operation'` / `'live_checkout_detached_head'` (P-c5a1f3b8). |
372
372
  | `engine.js` — `spawnAgent` originalRef persistence | Persists `originalRef` / `originalRefType` onto the dispatch record via `mutateDispatch` so restore survives an engine restart (P-c5a1f3b8). |
@@ -376,7 +376,7 @@ Live-checkout mode is deliberately small. These are NOT supported and will not b
376
376
  | `engine.js` — dispatcher `liveProjectsInUse` set | Per-project mutating-concurrency cap (P-a3f9b205). |
377
377
  | `engine.js` — worktree-pool / orphan-GC short-circuits | `worktreePath===null` no-ops in live mode (P-a3f9b206). |
378
378
  | `engine/cleanup.js` — `runPeriodicWorktreeSweep` live filter | Excludes live-checkout projects from the registry-derived periodic worktree GC so the operator's primary checkout never enters the GC decision surface (PL-live-checkout-reliability-hardening). |
379
- | `engine/create-pr-worktree.js` — `prepareCreatePrWorktree` step-4 restore | `reset --hard HEAD` (not the index-leaking `checkout -- .`) + retried untracked removal + `liveTreeDirty` surfaced + `shared.removeWorktree` teardown (PL-live-checkout-reliability-hardening). |
379
+ | `engine/create-pr-worktree.js` — `prepareCreatePrWorktree` | Copies pre-existing operator changes into an isolated worktree and returns `sourceCheckoutPreserved: true`; never resets, cleans, checks out, or deletes source files. |
380
380
  | `engine/pr-action.js` — `buildCreatePrFollowups({ …, mainBranch })` live-mode message | CC "Create PR" chip instruction. In live mode the PR is ALWAYS built on a fresh branch off `origin/<mainBranch>` (stash → fetch → `checkout -B` → pop → commit → push → PR → restore), never reusing the current branch as the base; stop-on-stash-pop-conflict. `dashboard.js` `POST /api/pr-action/offer-create-pr` resolves `mainBranch` via `shared.resolveMainBranch` and threads it in (W-mr3jbpru). |
381
381
  | `dashboard/js/settings.js` — checkoutMode dropdown + chip + `set-liveCheckoutAutoReset` fleet toggle | Operator-facing UI; the fleet-wide auto-reset toggle persists to `engine.liveCheckoutAutoReset` (per-project UI deferred — config.json only) (P-a3f9b207; auto-reset toggle W-mqvejug6000eeb20). |
382
382
  | `test/unit/{resolve-spawn-paths-live-mode,prepare-live-checkout,spawn-agent-live-mode-wiring,live-checkout-auto-stash,live-checkout-stash-before-reset}.test.js` | Wiring and contract tests (P-a3f9b208; auto-stash helpers W-mqtvnnj1000357fa; stash-before-reset ordering W-mrawgw4q000a6bff). |
@@ -35,7 +35,7 @@ The sidecar lives at `<MINIONS_DIR>/agents/<agentId>/managed-spawn.json` and is
35
35
  "name": "constellation-host", // kebab-case, ≤64 chars, unique within file
36
36
  "cmd": "bun", // must be on engine.managedSpawn.executableAllowlist
37
37
  "args": ["run", "dev"], // ≤64 entries
38
- "cwd": "D:/repos/constellation", // must be inside a real git worktree (requireGitWorkdir: true) — monorepo subdirs ok, ancestor walked up to gitWorktreeMaxParentDepth parents
38
+ "cwd": "<MINIONS_AGENT_CWD>", // assigned dispatch root or a subdirectory
39
39
  "env": { "CONSTELLATION_SERVER": "http://localhost:3000" }, // ≤32 keys; POSIX-shape + denylist enforced
40
40
  "ports": [3001], // 1024-65535; ≤20 per spec; advisory only (engine doesn't bind)
41
41
  "ttl_minutes": 240, // ≤1440 (24h hard cap); defaults to 720 (12h)
@@ -59,6 +59,19 @@ The sidecar lives at `<MINIONS_DIR>/agents/<agentId>/managed-spawn.json` and is
59
59
 
60
60
  The renderer in [`buildManagedSpawnHint`](../engine/managed-spawn.js) at `engine/managed-spawn.js:419` emits this exact shape (with allowlist + cap reminders) into the agent's prompt whenever the work item has `meta.managed_spawn: true`. Treat the rendered hint as the source of truth — if this doc and the hint drift, the hint wins.
61
61
 
62
+ The engine also confines every spec to the dispatch root: `cwd` must resolve at
63
+ or below `MINIONS_AGENT_CWD`. In worktree mode, `project.localPath` is rejected
64
+ even though it is a valid Git checkout. Project-specific env deny patterns are
65
+ resolved from the dispatch's configured project, not inferred from the sibling
66
+ worktree path.
67
+
68
+ Persisted managed-process rows carry the canonical dispatch root and originating
69
+ work type. Manual restart and boot reconciliation revalidate that provenance
70
+ against current project checkout mode; a legacy or live-to-worktree record that
71
+ would restart inside `project.localPath` is killed and removed instead. The
72
+ periodic sweep applies the same check, so mode drift is enforced immediately
73
+ rather than waiting for TTL or another engine restart.
74
+
62
75
  ### Smoke-test before writing the sidecar (mandatory, W-mpbpexrg00110661)
63
76
 
64
77
  The hint mandates that agents run each `cmd args` in the declared `cwd` for at least 5 seconds AND confirm the healthcheck endpoint returns the expected status BEFORE writing `managed-spawn.json`. Sidecars written from guessed-at commands are how dispatches silently lose specs: the engine spawns what the agent declared; if the command crashes on first launch, the engine kills the spec, removes it from state, and (for partial failures) marks the WI with `_managedSpawnPartial` rather than demoting it — the agent's primary work succeeded by validator standards. The cost of guessing wrong is the operator finding out hours later that half the stack is down.
@@ -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
@@ -128,12 +159,15 @@ handlers, PID-file writing, live-output.log, timeout/steering cancel-vs-kill,
128
159
  restart-reattach) runs unmodified regardless of whether `proc` is a real OS
129
160
  process or a pooled lease. Assignment strategy, in order:
130
161
 
131
- 1. A **free** worker whose MCP-server set already matches wins first — reused
132
- via a fresh `session/new`, no respawn.
162
+ 1. A **free** worker whose MCP-server set, process environment, and canonical
163
+ process cwd already match wins first — reused via a fresh `session/new`, no
164
+ respawn.
133
165
  2. Otherwise another free worker is **evicted** (closed) and a replacement is
134
166
  spawned fresh with the requested MCP servers/cwd (MCP servers are resolved
135
- at `copilot --acp` process boot, so a changed server set requires a full
136
- respawn, not just a new session).
167
+ at `copilot --acp` process boot, so a changed server set or process cwd
168
+ requires a full respawn, not just a new session). This prevents a worker OS
169
+ process rooted in a live operator checkout from carrying that cwd into an
170
+ isolated worktree dispatch.
137
171
  3. Otherwise, if the pool has room (fewer than `agentAcpPoolSize` workers
138
172
  allocated across free + busy + in-flight-spawn), a brand-new worker spawns.
139
173
  4. Otherwise the request queues FIFO until a `release()` or worker crash frees
@@ -197,9 +231,9 @@ agent fleet to Copilot, and vice versa. Both paths fall through to
197
231
  `engine.defaultCli` (the fleet-wide default), but they never see each other's
198
232
  overrides.
199
233
 
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.
234
+ The helpers in `engine/shared.js` are the single source of truth for
235
+ runtime/model and cross-runtime policy resolution. Engine code MUST go through
236
+ them instead of reading config keys directly.
203
237
 
204
238
  | Helper | Chain |
205
239
  |--------|-------|
@@ -207,6 +241,7 @@ directly.
207
241
  | `resolveCcCli(engine)` | `engine.ccCli` → `engine.defaultCli` → `'copilot'` |
208
242
  | `resolveAgentModel(agent, engine)` | `agent.model` → `engine.defaultModel` → undefined |
209
243
  | `resolveCcModel(engine)` | `engine.ccModel` → `engine.defaultModel` → undefined |
244
+ | `resolveAgentAllowedTools(config)` | Legacy `config.claude.allowedTools` fleet ceiling → undefined. Despite its legacy namespace, it applies to every selected runtime. |
210
245
  | `resolveAgentMaxBudget(agent, engine)` | `agent.maxBudgetUsd` → `engine.maxBudgetUsd`. Honors literal `0`. |
211
246
  | `resolveAgentBareMode(agent, engine)` | `agent.bareMode` → `engine.claudeBareMode` → false. Strict null check so per-agent `false` overrides engine `true`. |
212
247
 
@@ -285,8 +320,9 @@ codifies are documented in [`docs/harness-propagation.md`](./harness-propagation
285
320
 
286
321
  ## The "No Runtime-Name Branching" Rule
287
322
 
288
- The whole point of this layer: **engine code MUST gate behavior on
289
- `runtime.capabilities.*` flags, not on `runtime.name === 'claude'` comparisons.**
323
+ The whole point of this layer: **orchestration code MUST use the runtime facade,
324
+ capabilities, and adapter hooks rather than concrete imports, harness config
325
+ keys, or `runtime.name === 'claude'` comparisons.**
290
326
 
291
327
  If you find yourself wanting to write `if (runtime.name === 'copilot')` in
292
328
  engine code, the correct response is one of:
@@ -296,19 +332,18 @@ engine code, the correct response is one of:
296
332
  3. Move the special case into the adapter's `buildArgs` / `buildPrompt` /
297
333
  `parseOutput` where it belongs.
298
334
 
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.
335
+ Tests in `test/unit/runtime-adapters.test.js` enforce the contract.
336
+ `test/unit/runtime-boundary.test.js` rejects concrete adapter imports,
337
+ runtime-name branches, and harness-specific invocation config in core paths.
303
338
 
304
339
  ## Adding a New Runtime
305
340
 
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`.
341
+ 1. Create `engine/runtimes/<name>.js` implementing adapter API version `1`.
342
+ Keep all CLI flags, output schemas, workspace quirks, and worker behavior in
343
+ that file. Set a useful `installHint`.
309
344
  2. Register it in `engine/runtimes/index.js`:
310
345
  ```js
311
- registry.set('<name>', require('./<name>'));
346
+ registerRuntime('<name>', require('./<name>'), { strict: true });
312
347
  ```
313
348
  3. Add unit coverage modeled on `test/unit/runtime-adapters.test.js`.
314
349
 
@@ -324,6 +359,6 @@ flags — never special-case in engine code.
324
359
  - `docs/copilot-cli-schema.md` — empirical Copilot CLI behavior captured during
325
360
  the spike that produced the Copilot adapter; cite it when changing
326
361
  Copilot-specific capability flags.
327
- - `engine/runtimes/claude.js` JSDoc header — authoritative adapter contract.
362
+ - `engine/runtimes/contract.js` — authoritative adapter contract and facade.
328
363
  - `engine/shared.js` `resolveAgentCli` / `resolveCcCli` etc. — the only
329
364
  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. |
@@ -11,6 +11,28 @@ Lifecycle keeps the cross-cutting invariants; the detail lives here.
11
11
  > `_killGitDescendantsForWorktree`, `pruneOrphanWorktrees*`, `gcDispatchWorktreeIfOrphan`),
12
12
  > `engine/cleanup.js`. Last verified: 2026-06-09.
13
13
 
14
+ ## Operator-checkout boundary
15
+
16
+ For `checkoutMode: "worktree"`, `project.localPath` is an operator-owned source
17
+ checkout, not an execution directory. Project-bound read-only and mutating
18
+ dispatches both run in engine-owned worktrees; Command Center delegates writes;
19
+ Create-PR staging copies existing operator changes without resetting or deleting
20
+ the source; restart restore requires explicit live dispatch provenance plus
21
+ current live configuration. Branchless project dispatches receive detached
22
+ worktrees, and path containment resolves symlinks/junctions before accepting a
23
+ worktree or `meta.workdir`. Placement and removal are checked against **every**
24
+ configured `localPath`, so a nested project's worktree cannot land inside a
25
+ parent project's operator checkout. Restart reattachment requires either a
26
+ validated persisted worktree path or current live-mode provenance; unsafe
27
+ legacy processes are terminated and retried. Worktree setup may update shared Git refs/metadata,
28
+ but it never checks out, resets, cleans, writes files, or removes
29
+ `.git/index.lock` in the operator checkout.
30
+
31
+ Worktrees that host live `managed_spawn` or `keep_processes` cwd records are
32
+ anchored across dispatch-end, periodic, boot, in-root, and out-of-root GC.
33
+ Anchor comparisons use canonical filesystem paths so junction/case aliases do
34
+ not make a live service's worktree look orphaned.
35
+
14
36
  ## Live-worktree guard (W-mq5rwwss000f30a7)
15
37
 
16
38
  **Invariant:** every code path that wipes, renames, or recycles a worktree
@@ -47,6 +69,20 @@ recycle worktree dirs across branches.
47
69
  → `git checkout --detach origin/<main>` → mark IDLE.
48
70
  - **State** in SQLite (`worktree_pool`); git ops outside the transaction.
49
71
 
72
+ ## Fresh ordinary branch bases
73
+
74
+ When the pool is disabled or has no usable entry, a brand-new ordinary mutating
75
+ branch is created only after `git fetch origin <mainBranch>` succeeds, and its
76
+ base is always `origin/<mainBranch>`. A network or authentication failure stops
77
+ before `git worktree add` or agent spawn and leaves the work item retryable; the
78
+ engine never falls back to a potentially stale local `<mainBranch>`.
79
+
80
+ This fail-closed rule applies only when the requested branch is confirmed absent
81
+ from origin. PR-targeted / `useExistingBranch`, shared-branch, and retry paths
82
+ that intentionally preserve an existing branch continue using that branch
83
+ unchanged. Warm-pool borrowing keeps its existing fetch-and-checkout of
84
+ `origin/<mainBranch>`.
85
+
50
86
  ## Ownership marker is never "dirty" (#284)
51
87
 
52
88
  The reused-worktree preflight (`assertCleanSharedWorktree`) builds its