@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.
- package/bin/minions.js +1 -0
- package/dashboard/js/refresh.js +2 -1
- package/dashboard/js/settings.js +1 -1
- package/dashboard.js +37 -19
- package/docs/completion-reports.md +20 -1
- package/docs/keep-processes.md +7 -2
- package/docs/live-checkout-mode.md +7 -7
- package/docs/managed-spawn.md +14 -1
- package/docs/runtime-adapters.md +61 -26
- package/docs/skills.md +2 -0
- package/docs/workspace-manifests.md +1 -1
- package/docs/worktree-lifecycle.md +36 -0
- package/engine/acp-transport.js +273 -58
- package/engine/ado-comment.js +3 -1
- package/engine/agent-worker-pool.js +278 -54
- package/engine/cc-worker-pool.js +12 -3
- package/engine/cleanup.js +15 -11
- package/engine/cli.js +56 -28
- package/engine/comment-format.js +51 -14
- package/engine/create-pr-worktree.js +57 -79
- package/engine/dispatch.js +7 -2
- package/engine/execution-model.js +68 -0
- package/engine/gh-comment.js +6 -3
- package/engine/github.js +6 -7
- package/engine/keep-process-sweep.js +131 -9
- package/engine/lifecycle.js +126 -45
- package/engine/live-checkout.js +4 -2
- package/engine/llm.js +59 -83
- package/engine/managed-spawn.js +112 -5
- package/engine/memory-store.js +3 -1
- package/engine/playbook.js +4 -6
- package/engine/pooled-agent-process.js +512 -45
- package/engine/pr-action.js +6 -8
- package/engine/process-utils.js +154 -18
- package/engine/runtimes/claude.js +94 -25
- package/engine/runtimes/codex.js +1 -0
- package/engine/runtimes/contract.js +239 -0
- package/engine/runtimes/copilot.js +97 -9
- package/engine/runtimes/index.js +37 -9
- package/engine/shared.js +349 -82
- package/engine/spawn-agent.js +85 -116
- package/engine/spawn-phase-watchdog.js +8 -37
- package/engine/timeout.js +14 -10
- package/engine/worktree-gc.js +55 -46
- package/engine.js +418 -241
- package/package.json +1 -1
- package/playbooks/fix.md +20 -0
- package/playbooks/review.md +2 -0
- package/playbooks/shared-rules.md +2 -2
- package/prompts/cc-system.md +11 -11
- package/skills/check-self-authored-review-comment/SKILL.md +34 -0
package/bin/minions.js
CHANGED
|
@@ -912,6 +912,7 @@ function init() {
|
|
|
912
912
|
|
|
913
913
|
// Copy with smart merge logic
|
|
914
914
|
copyDir(PKG_ROOT, MINIONS_HOME, excludeTop, alwaysUpdate, neverOverwrite, isUpgrade, actions);
|
|
915
|
+
shared.syncBundledPersonalSkills(path.join(MINIONS_HOME, 'skills'), { homeDir: os.homedir() });
|
|
915
916
|
|
|
916
917
|
// Create config from template if it doesn't exist
|
|
917
918
|
const configPath = path.join(MINIONS_HOME, 'config.json');
|
package/dashboard/js/refresh.js
CHANGED
|
@@ -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
|
-
|
|
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 = {};
|
package/dashboard/js/settings.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
1977
|
+
const worktreeRoots = new Map();
|
|
1978
1978
|
for (const p of projects) {
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1979
|
+
if (!p.localPath) continue;
|
|
1980
|
+
const wtRoot = path.resolve(
|
|
1981
|
+
p.localPath,
|
|
1982
|
+
config.engine?.worktreeRoot || shared.ENGINE_DEFAULTS.worktreeRoot,
|
|
1983
|
+
);
|
|
1984
|
+
const rootKey = shared._normalizeWorktreePath(wtRoot);
|
|
1985
|
+
if (!worktreeRoots.has(rootKey)) worktreeRoots.set(rootKey, wtRoot);
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
let count = 0;
|
|
1989
|
+
for (const wtRoot of worktreeRoots.values()) {
|
|
1982
1990
|
try {
|
|
1983
1991
|
for (const dir of fs.readdirSync(wtRoot)) {
|
|
1984
1992
|
const dirPath = path.join(wtRoot, dir);
|
|
@@ -4678,6 +4686,7 @@ function _invokeDocChatViaPool({ prompt, model, effort, engineConfig, systemProm
|
|
|
4678
4686
|
(async () => {
|
|
4679
4687
|
try {
|
|
4680
4688
|
sessionHandle = await ccWorkerPool.getSession({
|
|
4689
|
+
runtime: llm._resolveRuntimeFor({ engineConfig }),
|
|
4681
4690
|
tabId: tabKey,
|
|
4682
4691
|
model,
|
|
4683
4692
|
effort,
|
|
@@ -5345,13 +5354,15 @@ function _recoverPartialDocChatResponse(result, sessionKey) {
|
|
|
5345
5354
|
function _shouldSuppressDocChatPostPatchError(ccError, finalize) {
|
|
5346
5355
|
if (!finalize || finalize.edited !== true) return false;
|
|
5347
5356
|
if (!ccError) return false;
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5357
|
+
try {
|
|
5358
|
+
const runtime = resolveRuntime(ccError.runtime);
|
|
5359
|
+
return shouldSuppressPostMutationError(runtime, {
|
|
5360
|
+
mutationApplied: true,
|
|
5361
|
+
error: ccError,
|
|
5362
|
+
});
|
|
5363
|
+
} catch {
|
|
5364
|
+
return false;
|
|
5365
|
+
}
|
|
5355
5366
|
}
|
|
5356
5367
|
|
|
5357
5368
|
function _buildDocChatResponsePayload({
|
|
@@ -9447,6 +9458,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
9447
9458
|
if (!shared.resolveCcUseWorkerPool(CONFIG.engine)) return { skipped: 'pool-disabled' };
|
|
9448
9459
|
if (!tabId) throw new Error('tabId required');
|
|
9449
9460
|
const result = await ccWorkerPool.warmTab({
|
|
9461
|
+
runtime: resolveRuntime(shared.resolveCcCli(CONFIG.engine)),
|
|
9450
9462
|
tabId,
|
|
9451
9463
|
model: CONFIG.engine?.ccModel || shared.ENGINE_DEFAULTS.ccModel,
|
|
9452
9464
|
effort: CONFIG.engine?.ccEffort || shared.ENGINE_DEFAULTS.ccEffort,
|
|
@@ -9823,6 +9835,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
9823
9835
|
(async () => {
|
|
9824
9836
|
try {
|
|
9825
9837
|
sessionHandle = await ccWorkerPool.getSession({
|
|
9838
|
+
runtime: llm._resolveRuntimeFor({ engineConfig }),
|
|
9826
9839
|
tabId: resolvedTabId,
|
|
9827
9840
|
model,
|
|
9828
9841
|
effort,
|
|
@@ -13614,6 +13627,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13614
13627
|
status: 'enrolled',
|
|
13615
13628
|
prId: linkResult.id,
|
|
13616
13629
|
autoManaged,
|
|
13630
|
+
resurrected: linkResult.resurrected === true,
|
|
13617
13631
|
contextOnly: false,
|
|
13618
13632
|
project: linkResult.targetProject?.name || 'central',
|
|
13619
13633
|
projectName: plan.projectName || linkResult.targetProject?.name || null,
|
|
@@ -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
|
|
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'
|
|
13648
|
-
//
|
|
13649
|
-
|
|
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: '
|
|
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({
|
|
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,
|
|
227
|
+
A no-op completion declares that the agent correctly **declined** to do the work — the change was already shipped on master, the dispatch premise was wrong, or an exact review finding was proven resolved/invalid with current-code evidence.
|
|
227
228
|
|
|
228
229
|
To signal a no-op:
|
|
229
230
|
|
|
@@ -245,6 +246,24 @@ Engine behavior when `noop: true` (`engine/lifecycle.js` `parseCompletionNoop` +
|
|
|
245
246
|
|
|
246
247
|
Without `noop: true`, an empty PR field will be flagged as a missing-PR-attachment failure and auto-retried up to `ENGINE_DEFAULTS.maxRetries` times.
|
|
247
248
|
|
|
249
|
+
### Review-fix no-op evidence
|
|
250
|
+
|
|
251
|
+
Review-feedback fixes have a stricter no-op contract because all agents can share one platform credential. `viewerDidAuthor: true` does not identify the Minions agent that authored a finding, and reviewer/fixer agent equality is allowed.
|
|
252
|
+
|
|
253
|
+
If the live PR branch did not advance, the completion must set `noop: true` and include:
|
|
254
|
+
|
|
255
|
+
```json
|
|
256
|
+
{
|
|
257
|
+
"reviewFindingResolution": {
|
|
258
|
+
"findingId": "review-0123456789abcdef",
|
|
259
|
+
"disposition": "already-resolved",
|
|
260
|
+
"currentCodeEvidence": "engine/lifecycle.js:4600 already enforces the condition; test/unit/example.test.js covers it."
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The `findingId` is injected into the fix playbook from trusted `minionsReview` provenance. `disposition` is `already-resolved` or `invalid`, and `currentCodeEvidence` must cite a current `file:line` or commit. An identity-only no-op is retried and does not increment `_noOpFixes`.
|
|
266
|
+
|
|
248
267
|
## PR-comment follow-ups
|
|
249
268
|
|
|
250
269
|
Fix and review agents can spin off new Minions work items in response to PR
|
package/docs/keep-processes.md
CHANGED
|
@@ -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": "
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
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). |
|
package/docs/managed-spawn.md
CHANGED
|
@@ -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": "
|
|
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.
|
package/docs/runtime-adapters.md
CHANGED
|
@@ -8,7 +8,8 @@ behavior is hidden behind an adapter object resolved through `resolveRuntime()`.
|
|
|
8
8
|
|
|
9
9
|
| File | Role |
|
|
10
10
|
|------|------|
|
|
11
|
-
| `engine/runtimes/
|
|
11
|
+
| `engine/runtimes/contract.js` | Versioned adapter contract, capability gating, opaque invocation-option transport, and optional-hook facades. |
|
|
12
|
+
| `engine/runtimes/index.js` | Adapter registry and the only runtime facade imported by orchestration code. |
|
|
12
13
|
| `engine/runtimes/claude.js` | Claude Code adapter. Owns binary probe, `--system-prompt-file`, JSONL parser, model shorthands, budget cap, bare mode. |
|
|
13
14
|
| `engine/runtimes/copilot.js` | GitHub Copilot CLI adapter. Owns standalone-vs-`gh-copilot` resolution, stdin-only prompt delivery, `https://api.githubcopilot.com/models` discovery, effort `'max' → 'xhigh'` mapping. |
|
|
14
15
|
| `engine/runtimes/codex.js` | OpenAI Codex CLI adapter. Owns `@openai/codex`/native binary resolution, `codex exec --json -` prompt delivery, `codex debug models --bundled` discovery, `.agents/skills` roots, and Codex JSONL parsing. |
|
|
@@ -30,6 +31,7 @@ methods that genuinely differ.
|
|
|
30
31
|
|
|
31
32
|
| Member | Type / Returns | Purpose |
|
|
32
33
|
|--------|----------------|---------|
|
|
34
|
+
| `apiVersion` | number | Contract version. Bundled adapters use `1`; incompatible versions fail at registration. |
|
|
33
35
|
| `name` | string | Adapter identifier (matches the registry key). |
|
|
34
36
|
| `capabilities` | flag object (see below) | Feature flags consumed by engine code. |
|
|
35
37
|
| `resolveBinary({ env, config? })` | `{ bin, native, leadingArgs }` or null | Probe PATH, npm-globals, package overrides; cached to `engine/<name>-caps.json`. `leadingArgs` is `[]` for direct binaries and `['copilot']` for the `gh copilot` extension fallback. |
|
|
@@ -39,7 +41,8 @@ methods that genuinely differ.
|
|
|
39
41
|
| `modelsCache` | absolute path | Cache file for `listModels()` results. |
|
|
40
42
|
| `spawnScript` | absolute path | Path to the runtime-agnostic spawn wrapper (currently `engine/spawn-agent.js` for both runtimes). |
|
|
41
43
|
| `buildArgs(opts)` | `string[]` | CLI args excluding the binary itself. |
|
|
42
|
-
| `
|
|
44
|
+
| `resolveInvocationOptions({ options, engineConfig, config, failureClass })` | object | Optional; maps harness-specific config and retry policy into semantic invocation options. Core code never reads harness-specific invocation keys. |
|
|
45
|
+
| `buildSpawnFlags(opts)` | `string[]` | Optional compatibility flags for older spawn wrappers. New fields travel in the opaque `--runtime-options` bag automatically. |
|
|
43
46
|
| `buildPrompt(promptText, sysPromptText)` | string | Final prompt delivered to the CLI. Adapters that lack `--system-prompt-file` (Copilot) prepend a `<system>` block here. |
|
|
44
47
|
| `getUserAssetDirs({ homeDir })` | `string[]` | Runtime-native global asset roots passed to spawn as `--add-dir` so worktrees still see them. |
|
|
45
48
|
| `getSkillRoots({ homeDir, project? })` | `[{ scope, dir, projectName? }]` | Where `collectSkillFiles` looks for native + project skill markdown. |
|
|
@@ -52,11 +55,39 @@ methods that genuinely differ.
|
|
|
52
55
|
| `parseStreamChunk(line)` | event object or null | Single JSONL line → typed event. |
|
|
53
56
|
| `parseError(rawOutput)` | `{ message, code, retriable }` | Codes: `auth-failure`, `context-limit`, `budget-exceeded`, `model-unavailable` (retriable=true for upstream overload/503; retriable=false for invalid/typo'd model id — Copilot enriches the message via `_warmModelCache()` so it lists the available models), `crash`, null. |
|
|
54
57
|
| `createStreamConsumer(ctx)` | consumer object | Stream accumulator used by `engine/llm.js`. |
|
|
55
|
-
| `
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
(
|
|
59
|
-
|
|
58
|
+
| `prepareWorkspace(ctx)` | result object | Optional synchronous hook for harness-owned workspace state, such as trust configuration. |
|
|
59
|
+
| `isSpawnStartupEvent(event)` | boolean | Optional startup-event classifier used by the spawn-phase watchdog. |
|
|
60
|
+
| `shouldSuppressPostMutationError(ctx)` | boolean | Optional harness quirk policy for an error received after a confirmed mutation. |
|
|
61
|
+
| `buildWorkerArgs(opts)` | string[] | Required with `acpWorkerPool`; owns persistent-worker CLI flags. |
|
|
62
|
+
| `encodePooledOutput(event)` | string | Required with `acpWorkerPool`; translates semantic pooled events into the adapter's normal parser format. |
|
|
63
|
+
| `detectPermissionGate`, `getPromptDeliveryMode`, `usesSystemPromptFile`, `classifyFailure` | misc | Adapter-owned policy that orchestration reads through the facade instead of branching on `runtime.name`. |
|
|
64
|
+
|
|
65
|
+
`engine/runtimes/contract.js` is authoritative. Registration normalizes legacy
|
|
66
|
+
partial test/tooling adapters, validates bundled adapters strictly, and rejects
|
|
67
|
+
an explicit unsupported `apiVersion`.
|
|
68
|
+
|
|
69
|
+
### Stable invocation boundary
|
|
70
|
+
|
|
71
|
+
Minions passes semantic values such as `model`, `effort`, and `sessionId`; it
|
|
72
|
+
does not construct harness CLI argv. The selected adapter:
|
|
73
|
+
|
|
74
|
+
1. maps its config through `resolveInvocationOptions()`;
|
|
75
|
+
2. receives the full options object through an opaque base64url JSON
|
|
76
|
+
`--runtime-options` channel when the spawn wrapper is used; and
|
|
77
|
+
3. converts those options to real CLI flags only in `buildArgs()`.
|
|
78
|
+
|
|
79
|
+
Legacy named wrapper flags remain for backward compatibility, but they are no
|
|
80
|
+
longer the extension point. A new harness option can be added entirely inside
|
|
81
|
+
its adapter without teaching `engine.js`, `engine/llm.js`, or
|
|
82
|
+
`engine/spawn-agent.js` another CLI flag. Large image payloads are written to a
|
|
83
|
+
protected per-call sidecar; only its path enters the options argument, and the
|
|
84
|
+
wrapper hydrates then deletes it before invoking the adapter.
|
|
85
|
+
|
|
86
|
+
Retirement gates for the temporary compatibility surfaces are tracked in
|
|
87
|
+
`docs/deprecated.json`: `legacy-runtime-named-spawn-flags`,
|
|
88
|
+
`versionless-runtime-adapter-compat`, and
|
|
89
|
+
`preapprove-workspace-mcps-wrapper`. Calendar dates never override their
|
|
90
|
+
external-usage gates.
|
|
60
91
|
|
|
61
92
|
## Capability Flags
|
|
62
93
|
|
|
@@ -81,7 +112,7 @@ real behavioral split appears between adapters.
|
|
|
81
112
|
| `resumePromptCarryover` | ✗ | ✓ | ✓ | CC resume turns prepend recent visible Q&A in stdin when the runtime session store is opaque to Minions. |
|
|
82
113
|
| `resumeBookkeepingTurn` | ✓ | — | — | Claude CLI injects a synthetic "Continue from where you left off." meta turn on `--resume`; CC prompts must tell the model not to treat it as user intent. |
|
|
83
114
|
| `streamConsumer` | ✓ | ✓ | ✓ | Adapter implements `createStreamConsumer(ctx)` — required by `engine/llm.js` accumulator. |
|
|
84
|
-
| `imageInput` | ✓ | ✓ |
|
|
115
|
+
| `imageInput` | ✓ | ✓ | ✓ | Runtime accepts `images: [{mimeType, dataBase64}]` and delivers them to the CLI using adapter-owned transport. When false, `_resolveImageOpts` returns a typed `model-unavailable` error. |
|
|
85
116
|
| `acpWorkerPool` | ✗ | ✓ | ✗ | Runtime supports `<bin> --acp` (Agent Client Protocol), so it can be routed through a persistent worker pool instead of cold-spawning a fresh CLI process per turn/dispatch. Two independent consumers gate on this flag: `engine/cc-worker-pool.js` (CC/doc-chat tabs, `resolveCcUseWorkerPool`) and `engine/agent-worker-pool.js` (fleet agent dispatch, `resolveAgentUseWorkerPool`). Both resolvers hard-return `false` whenever this flag isn't `true` on the resolved runtime, regardless of any operator override, so a future non-ACP runtime can never be silently pooled. See [ACP Worker Pool](#acp-worker-pool-fleet-agent-dispatch) below. |
|
|
86
117
|
|
|
87
118
|
When a behavior is genuinely uniform across all current adapters, it still gets
|
|
@@ -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
|
|
132
|
-
via a fresh `session/new`, no
|
|
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
|
|
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
|
-
|
|
201
|
-
resolution. Engine code MUST go through
|
|
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: **
|
|
289
|
-
|
|
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`
|
|
300
|
-
|
|
301
|
-
|
|
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
|
|
307
|
-
|
|
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
|
-
|
|
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/
|
|
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.
|
|
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
|