@yemi33/minions 0.1.2252 → 0.1.2253

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/dashboard.js CHANGED
@@ -53,6 +53,7 @@ const steering = require('./engine/steering');
53
53
  const steeringStore = require('./engine/steering-store');
54
54
  const projectDiscovery = require('./engine/project-discovery');
55
55
  const prAction = require('./engine/pr-action');
56
+ const createPrWorktree = require('./engine/create-pr-worktree');
56
57
  const prResolve = require('./engine/pr-resolve');
57
58
  const prFixTarget = require('./engine/pr-fix-target');
58
59
  const prTrack = require('./engine/pr-track');
@@ -13325,11 +13326,53 @@ What would you like to discuss or change? When you're happy, say "approve" and I
13325
13326
  const changedFileCount = porcelain.split('\n').map(s => s.trim()).filter(Boolean).length;
13326
13327
  const hasChanges = changedFileCount > 0;
13327
13328
  const branch = (typeof body?.branch === 'string' && body.branch.trim()) || currentBranch || '';
13329
+ // The Create-PR action follows the SAME checkout pattern as a dispatch
13330
+ // for this project: 'live' commits in the live checkout; 'worktree'
13331
+ // routes the changes through an isolated worktree (so nothing is ever
13332
+ // committed in the operator's live main). See engine/create-pr-worktree.js.
13333
+ const checkoutMode = shared.resolveCheckoutMode(project);
13328
13334
  const followups = hasChanges
13329
- ? prAction.buildCreatePrFollowups({ project: projectName, branch, contextOnly: !!body?.contextOnly })
13335
+ ? prAction.buildCreatePrFollowups({ project: projectName, branch, contextOnly: !!body?.contextOnly, checkoutMode })
13330
13336
  : [];
13331
13337
  recordCcTurnIfPresent(req, { kind: 'pr-action', id: '', title: `offer create-pr (${projectName})`, project: projectName, followups });
13332
- return jsonReply(res, 200, { ok: true, hasChanges, branch, changedFileCount, followups }, req);
13338
+ return jsonReply(res, 200, { ok: true, hasChanges, branch, changedFileCount, checkoutMode, followups }, req);
13339
+ } catch (e) {
13340
+ return jsonReply(res, e.statusCode || 400, { error: e.message }, req);
13341
+ }
13342
+ }},
13343
+
13344
+ { 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) => {
13345
+ const body = await readBody(req);
13346
+ try {
13347
+ reloadConfig();
13348
+ const projectName = typeof body?.project === 'string' ? body.project.trim() : '';
13349
+ if (!projectName) return jsonReply(res, 400, { error: 'project is required' }, req);
13350
+ const project = shared.getProjects(CONFIG).find(p => p && p.name === projectName);
13351
+ if (!project) return jsonReply(res, 404, { error: `unknown project: ${projectName}` }, req);
13352
+ if (!project.localPath) return jsonReply(res, 400, { error: `project ${projectName} has no localPath` }, req);
13353
+ const result = await createPrWorktree.prepareCreatePrWorktree({
13354
+ project,
13355
+ branch: typeof body?.branch === 'string' ? body.branch : undefined,
13356
+ minionsDir: MINIONS_DIR,
13357
+ engine: CONFIG.engine,
13358
+ log: (lvl, msg) => console.log(msg),
13359
+ });
13360
+ return jsonReply(res, 200, result, req);
13361
+ } catch (e) {
13362
+ return jsonReply(res, e.statusCode || 400, { error: e.message }, req);
13363
+ }
13364
+ }},
13365
+
13366
+ { method: 'POST', path: '/api/pr-action/cleanup-create-pr-worktree', desc: 'Remove an isolated worktree created by /api/pr-action/prepare-create-pr-worktree once its PR has been pushed/opened (P-cc-createpr-checkout-mode). Refuses any path missing the minions ownership marker, so it can never delete an arbitrary directory.', params: 'project (configured project name), worktreePath (the path returned by prepare-create-pr-worktree)', handler: async (req, res) => {
13367
+ const body = await readBody(req);
13368
+ try {
13369
+ reloadConfig();
13370
+ const projectName = typeof body?.project === 'string' ? body.project.trim() : '';
13371
+ const project = projectName ? shared.getProjects(CONFIG).find(p => p && p.name === projectName) : null;
13372
+ const worktreePath = typeof body?.worktreePath === 'string' ? body.worktreePath.trim() : '';
13373
+ if (!worktreePath) return jsonReply(res, 400, { error: 'worktreePath is required' }, req);
13374
+ const result = await createPrWorktree.cleanupCreatePrWorktree({ worktreePath, project });
13375
+ return jsonReply(res, 200, result, req);
13333
13376
  } catch (e) {
13334
13377
  return jsonReply(res, e.statusCode || 400, { error: e.message }, req);
13335
13378
  }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * engine/create-pr-worktree.js — checkout-mode-aware Create-PR staging.
3
+ *
4
+ * Background (the bug this fixes): the CC "Create PR" flow has the LLM commit /
5
+ * branch / push directly in the project's LIVE operator checkout
6
+ * (`project.localPath`). For a `checkoutMode: "worktree"` project that is wrong —
7
+ * a misstep by the LLM (committing before branching, freelancing `git pull`)
8
+ * leaks commits / merges onto the operator's `main`. The Create-PR action must
9
+ * follow the SAME checkout pattern as a normal dispatch for that project:
10
+ * - `live` → operate in the live checkout (handled by the existing flow);
11
+ * - `worktree` → operate in an isolated git worktree, never the live checkout.
12
+ *
13
+ * This module implements the `worktree` path. `prepareCreatePrWorktree` moves the
14
+ * uncommitted edits CC made in the live checkout into a fresh worktree (created
15
+ * off the same base commit, on a new branch) and then restores the live checkout
16
+ * clean — DETERMINISTICALLY, server-side, so nothing relies on the LLM running
17
+ * git correctly. CC is then told to commit / push / open the PR inside the
18
+ * returned worktree path, and to call `cleanupCreatePrWorktree` when done.
19
+ *
20
+ * Safety ordering: the worktree is populated and verified BEFORE the live
21
+ * checkout is touched. If applying the diff fails, the worktree is removed and
22
+ * the live checkout is left exactly as it was — the operator never loses work.
23
+ */
24
+
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+ const shared = require('./shared');
28
+
29
+ const PATCH_APPLY_TIMEOUT_MS = 120000;
30
+
31
+ /**
32
+ * Move a worktree-mode project's uncommitted live-checkout changes into an
33
+ * isolated worktree and restore the live checkout clean.
34
+ *
35
+ * @returns {Promise<{ ok:true, worktreePath, branch, baseBranch, baseSha,
36
+ * trackedChanged:boolean, untrackedCount:number } | { ok:false, reason:string }>}
37
+ * @throws on a git/fs failure AFTER which the live checkout is guaranteed
38
+ * untouched (the operator keeps their work).
39
+ */
40
+ async function prepareCreatePrWorktree({
41
+ project, branch, minionsDir, engine, log = () => {}, _git, _fs,
42
+ } = {}) {
43
+ const git = _git || ((args, opts) => shared.shellSafeGit(args, opts));
44
+ const fsm = _fs || fs;
45
+ if (!project || typeof project !== 'object') {
46
+ throw new Error('prepareCreatePrWorktree: project object required');
47
+ }
48
+ const localPath = project.localPath;
49
+ if (!localPath) {
50
+ throw new Error('prepareCreatePrWorktree: project.localPath required');
51
+ }
52
+ const mainBranch = (project.mainBranch && String(project.mainBranch).trim()) || 'main';
53
+
54
+ // 1. Capture the uncommitted state from the live checkout.
55
+ const baseSha = (await git(['-C', localPath, 'rev-parse', 'HEAD'])).trim();
56
+ if (!/^[0-9a-f]{7,40}$/i.test(baseSha)) {
57
+ throw new Error(`prepareCreatePrWorktree: could not resolve HEAD in ${localPath} (got ${JSON.stringify(baseSha)})`);
58
+ }
59
+ // `git diff HEAD --binary` captures staged + unstaged tracked changes
60
+ // (including deletions and binary deltas) as a single patch.
61
+ const trackedDiff = await git(['-C', localPath, 'diff', '--binary', 'HEAD']);
62
+ const porcelain = await git(['-C', localPath, 'status', '--porcelain']);
63
+ const untracked = porcelain
64
+ .split('\n')
65
+ .filter(l => l.startsWith('?? '))
66
+ .map(l => l.slice(3).trim())
67
+ .filter(Boolean);
68
+ const hasTracked = !!trackedDiff.trim();
69
+ if (!hasTracked && untracked.length === 0) {
70
+ return { ok: false, reason: 'no-changes' };
71
+ }
72
+
73
+ // 2. Create an isolated worktree off the SAME base commit the edits were made
74
+ // against, on a NEW branch (so it never collides with `main` being checked
75
+ // out in the live tree, and never commits onto a shared branch).
76
+ const uid = shared.uid();
77
+ const sanitized = String(project.name || 'project').replace(/[^a-zA-Z0-9._-]/g, '-').slice(0, 40) || 'project';
78
+ const requested = branch && String(branch).trim();
79
+ const branchName = (requested && /^[\w./-]{1,80}$/.test(requested) && requested !== mainBranch)
80
+ ? requested
81
+ : `cc-pr/${sanitized}-${uid}`;
82
+
83
+ let projectRoot;
84
+ try { projectRoot = shared.resolveProjectRootDir(localPath, minionsDir); }
85
+ catch (e) { throw new Error(`prepareCreatePrWorktree: cannot resolve project root for ${localPath} — ${e.message}`); }
86
+ const wtRel = (engine && engine.worktreeRoot) || shared.ENGINE_DEFAULTS.worktreeRoot;
87
+ const worktreesBase = path.resolve(projectRoot, wtRel);
88
+ const wtPath = path.join(worktreesBase, `cc-createpr-${sanitized}-${uid}`);
89
+ try { fsm.mkdirSync(worktreesBase, { recursive: true }); } catch { /* best-effort; worktree add will surface a real failure */ }
90
+
91
+ await git(
92
+ ['-C', localPath, 'worktree', 'add', '-b', branchName, wtPath, baseSha],
93
+ { timeout: shared.ENGINE_DEFAULTS.worktreeCreateTimeout },
94
+ );
95
+ try { shared.writeWorktreeOwnerMarker(wtPath, { source: 'cc-create-pr', project: project.name }); } catch { /* marker is best-effort */ }
96
+
97
+ // 3. Reproduce the live-checkout changes inside the worktree. If anything here
98
+ // fails, tear the worktree down and leave the live checkout untouched.
99
+ try {
100
+ if (hasTracked) {
101
+ const patchFile = path.join(wtPath, `.cc-createpr-${uid}.patch`);
102
+ fsm.writeFileSync(patchFile, trackedDiff);
103
+ await git(['-C', wtPath, 'apply', '--whitespace=nowarn', patchFile], { timeout: PATCH_APPLY_TIMEOUT_MS });
104
+ try { fsm.unlinkSync(patchFile); } catch { /* ignore */ }
105
+ }
106
+ for (const rel of untracked) {
107
+ // `?? dir/` entries from porcelain are directories — copy their tree.
108
+ const src = path.join(localPath, rel);
109
+ const dst = path.join(wtPath, rel);
110
+ _copyRecursive(fsm, src, dst);
111
+ }
112
+ } catch (e) {
113
+ try { await git(['-C', localPath, 'worktree', 'remove', '--force', wtPath]); } catch { /* leak rather than throw a second error */ }
114
+ throw new Error(
115
+ `prepareCreatePrWorktree: failed to stage changes into the worktree — ${e.message}. ` +
116
+ 'The live checkout was left untouched.',
117
+ );
118
+ }
119
+
120
+ // 4. The worktree now holds the changes — restore the live checkout clean.
121
+ await git(['-C', localPath, 'checkout', '--', '.']);
122
+ for (const rel of untracked) {
123
+ try { fsm.rmSync(path.join(localPath, rel.replace(/\/$/, '')), { recursive: true, force: true }); } catch { /* ignore */ }
124
+ }
125
+
126
+ log('info', `[cc-create-pr] staged ${project.name} changes into isolated worktree ${wtPath} on branch ${branchName} (live checkout restored)`);
127
+ return {
128
+ ok: true,
129
+ worktreePath: wtPath,
130
+ branch: branchName,
131
+ baseBranch: mainBranch,
132
+ baseSha,
133
+ trackedChanged: hasTracked,
134
+ untrackedCount: untracked.length,
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Remove a worktree previously created by prepareCreatePrWorktree. Refuses to
140
+ * touch a path that doesn't carry the minions ownership marker (so a bad/forged
141
+ * path can never delete an arbitrary directory).
142
+ */
143
+ async function cleanupCreatePrWorktree({ worktreePath, project, _git, _fs } = {}) {
144
+ const git = _git || ((args, opts) => shared.shellSafeGit(args, opts));
145
+ const fsm = _fs || fs;
146
+ if (!worktreePath || typeof worktreePath !== 'string') {
147
+ throw new Error('cleanupCreatePrWorktree: worktreePath required');
148
+ }
149
+ if (!fsm.existsSync(worktreePath)) {
150
+ return { ok: true, alreadyGone: true };
151
+ }
152
+ // Guard: only ever remove a directory the engine stamped as its own worktree.
153
+ if (!shared.hasWorktreeOwnerMarker(worktreePath)) {
154
+ return { ok: false, reason: 'not-owned' };
155
+ }
156
+ const fromDir = (project && project.localPath) || worktreePath;
157
+ try {
158
+ await git(['-C', fromDir, 'worktree', 'remove', '--force', worktreePath]);
159
+ } catch (e) {
160
+ return { ok: false, reason: 'remove-failed', error: e.message };
161
+ }
162
+ return { ok: true };
163
+ }
164
+
165
+ // Copy a file or directory tree (untracked entries may be either).
166
+ function _copyRecursive(fsm, src, dst) {
167
+ const st = fsm.statSync(src);
168
+ if (st.isDirectory()) {
169
+ fsm.mkdirSync(dst, { recursive: true });
170
+ for (const entry of fsm.readdirSync(src)) {
171
+ _copyRecursive(fsm, path.join(src, entry), path.join(dst, entry));
172
+ }
173
+ } else {
174
+ fsm.mkdirSync(path.dirname(dst), { recursive: true });
175
+ fsm.copyFileSync(src, dst);
176
+ }
177
+ }
178
+
179
+ module.exports = { prepareCreatePrWorktree, cleanupCreatePrWorktree };
@@ -1966,6 +1966,89 @@ async function enforcePrAttachmentContract(type, meta, agentId, config, resultSu
1966
1966
  return { reason, itemId: meta.item.id, severity, phantom: isPhantom };
1967
1967
  }
1968
1968
 
1969
+ // W-mqsk1ip00006cbae — After a verify WI transitions to done, assert that a PR
1970
+ // exists: either a direct reference on the item (_pr/_prUrl) or a matching
1971
+ // entry in pull-requests.json for the plan (by sourcePlan). If no PR is found,
1972
+ // flip the WI to failed with failure_class: verify-missing-pr and write an
1973
+ // inbox alert so operators can investigate.
1974
+ //
1975
+ // Returns null on the happy path (PR found or type is not verify).
1976
+ // Returns { reason, severity: 'hard', failureClass: 'verify-missing-pr' } when
1977
+ // the guard fires so the caller can set skipDoneStatus = true.
1978
+ function enforceVerifyPrContract(type, meta, agentId, config, resultSummary) {
1979
+ if (type !== WORK_TYPE.VERIFY) return null;
1980
+ if (!meta?.item?.id) return null;
1981
+
1982
+ const item = meta.item;
1983
+
1984
+ // Fast path: direct PR reference stamped on the WI.
1985
+ if (item._pr || item._prUrl) return null;
1986
+
1987
+ // Canonical tracker check: does any PR record list this WI in prdItems?
1988
+ try {
1989
+ if (hasCanonicalPrAttachment(item.id, config)) return null;
1990
+ } catch (err) {
1991
+ // If the check throws (e.g. unreadable PR file) we cannot confirm a PR
1992
+ // exists, so fall through to the failure path.
1993
+ log('warn', `enforceVerifyPrContract: canonical PR check failed for ${item.id}: ${err.message}`);
1994
+ }
1995
+
1996
+ // Plan-level check: any PR with matching sourcePlan in project PR files.
1997
+ if (item.sourcePlan) {
1998
+ try {
1999
+ const projects = shared.getProjects(config);
2000
+ for (const p of projects) {
2001
+ const prs = readOptionalJsonStrict(shared.projectPrPath(p), 'project pull-requests', Array.isArray) || [];
2002
+ if (prs.some(pr => pr.sourcePlan === item.sourcePlan)) return null;
2003
+ }
2004
+ const centralPrs = readOptionalJsonStrict(path.join(MINIONS_DIR, 'pull-requests.json'), 'central pull-requests', Array.isArray) || [];
2005
+ if (centralPrs.some(pr => pr.sourcePlan === item.sourcePlan)) return null;
2006
+ } catch (err) {
2007
+ log('warn', `enforceVerifyPrContract: sourcePlan PR check failed for ${item.id}: ${err.message}`);
2008
+ }
2009
+ }
2010
+
2011
+ // No PR found — flip WI to failed and surface an alert.
2012
+ const failureClass = FAILURE_CLASS.VERIFY_MISSING_PR;
2013
+ const reason = `Verify WI ${item.id} completed done but no PR was found ` +
2014
+ `(no _prUrl/_pr on item, no canonical attachment, and no pull-requests.json entry for plan ` +
2015
+ `${item.sourcePlan || '(none)'}). Flipped to failed so the plan does not silently advance. ` +
2016
+ `(Branch: ${meta.branch || '(none)'}, agent: ${agentId})`;
2017
+
2018
+ const wiPath = resolveWorkItemPath(meta);
2019
+ if (wiPath) {
2020
+ mutateJsonFileLocked(wiPath, data => {
2021
+ if (!Array.isArray(data)) return data;
2022
+ const w = data.find(i => i.id === item.id);
2023
+ if (!w) return data;
2024
+ w.status = WI_STATUS.FAILED;
2025
+ w._failureClass = failureClass;
2026
+ w.failReason = reason;
2027
+ w.failedAt = ts();
2028
+ delete w.completedAt;
2029
+ return data;
2030
+ }, { skipWriteIfUnchanged: true });
2031
+ }
2032
+
2033
+ if (item.sourcePlan) {
2034
+ try { syncPrdItemStatus(item.id, WI_STATUS.FAILED, item.sourcePlan); } catch (e) { log('warn', `verify-missing-pr PRD sync: ${e.message}`); }
2035
+ }
2036
+
2037
+ shared.writeToInbox('engine', `verify-missing-pr-${item.id}`,
2038
+ `# Verify WI completed without a PR: ${item.id}\n\n` +
2039
+ `**Agent:** ${agentId}\n` +
2040
+ `**Work item:** \`${item.id}\` — ${item.title || ''}\n` +
2041
+ `**Plan:** ${item.sourcePlan || '(none)'}\n` +
2042
+ `**Branch:** ${meta.branch || '(none)'}\n\n` +
2043
+ `${reason}\n` +
2044
+ (resultSummary ? `\n## Agent summary\n${resultSummary}\n` : ''),
2045
+ null,
2046
+ { sourceItem: item.id, reason: 'verify-missing-pr' });
2047
+
2048
+ log('warn', reason);
2049
+ return { reason, itemId: item.id, severity: 'hard', failureClass };
2050
+ }
2051
+
1969
2052
  // ─── Post-Completion Hooks ──────────────────────────────────────────────────
1970
2053
 
1971
2054
  /**
@@ -5141,6 +5224,17 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
5141
5224
  }
5142
5225
  }
5143
5226
 
5227
+ // W-mqsk1ip00006cbae — verify WI PR guard. After enforcePrAttachmentContract
5228
+ // (which skips verify types), assert that a verify WI has a PR before
5229
+ // allowing the done transition.
5230
+ if (effectiveSuccess && meta?.item?.id && !skipDoneStatus && !noopRationale) {
5231
+ const verifyContractFailure = enforceVerifyPrContract(type, meta, agentId, config, resultSummary);
5232
+ if (verifyContractFailure?.severity === 'hard') {
5233
+ completionContractFailure = completionContractFailure || verifyContractFailure;
5234
+ skipDoneStatus = true;
5235
+ }
5236
+ }
5237
+
5144
5238
  if (effectiveSuccess && meta?.item?.id && !skipDoneStatus) {
5145
5239
  meta._agentId = agentId;
5146
5240
  if (noopRationale) {
@@ -5946,6 +6040,7 @@ module.exports = {
5946
6040
  deferNonTerminalCompletion,
5947
6041
  deferPhantomCompletion,
5948
6042
  enforcePrAttachmentContract,
6043
+ enforceVerifyPrContract,
5949
6044
  markMissingPrAttachment,
5950
6045
  parseCompletionReportFile,
5951
6046
  resolveHarnessPropagated,
@@ -146,36 +146,62 @@ const CREATE_PR_FOLLOWUP = Object.freeze({ kind: 'create-pr', label: 'Create PR'
146
146
  * well-named branch" wording when absent.
147
147
  * `contextOnly` — when true, the chip links the new PR as context-only instead
148
148
  * of auto-managed (the default is auto-managed / contextOnly:false).
149
+ * `checkoutMode` — the project's resolved checkout pattern
150
+ * (`shared.resolveCheckoutMode(project)`). The Create-PR action
151
+ * MUST follow the same pattern as a normal dispatch for that
152
+ * project: `'live'` operates in the live operator checkout
153
+ * (legacy behavior); `'worktree'` (the default) routes the
154
+ * changes through an isolated git worktree so nothing is ever
155
+ * committed in the operator's live `main`. Defaults to `'live'`
156
+ * for back-compat when a caller doesn't resolve it.
149
157
  *
150
158
  * Each chip: `{ kind:'create-pr', label, project, branch, message }` where
151
159
  * `message` is the Command Center turn the chip click should send.
152
160
  */
153
- function buildCreatePrFollowups({ project, branch, contextOnly = false } = {}) {
161
+ function buildCreatePrFollowups({ project, branch, contextOnly = false, checkoutMode = 'live' } = {}) {
154
162
  const proj = (project && String(project).trim()) || '';
155
163
  if (!proj) return [];
156
164
  const br = branch && String(branch).trim() ? String(branch).trim() : '';
157
- const branchClause = br
158
- ? `(the working tree is on branch ${br}; if ${br} is the project's default/main branch, create a new well-named branch off it and commit there rather than committing to ${br} directly)`
159
- : 'on a new well-named branch off the project main branch';
160
165
  const linkBody = contextOnly
161
166
  ? `{"url":"<new PR url>","project":"${proj}","contextOnly":true}`
162
167
  : `{"url":"<new PR url>","project":"${proj}","contextOnly":false}`;
163
168
  const trackNote = contextOnly
164
169
  ? 'so it is tracked but not auto-reviewed'
165
170
  : 'so the engine auto-manages it (review -> fix -> re-review -> auto-merge)';
166
- // The original branch CC must return to once the PR is open, so the new PR
167
- // branch is never left checked out in the shared operator working tree.
168
- const restoreClause = br
169
- ? `switch the working tree back to the ORIGINAL branch (${br})`
170
- : 'switch the working tree back to the original branch it started on';
171
- const message =
172
- `Create a PR from the local changes you just made in the ${proj} project. ` +
173
- (br ? `Remember the working tree's current branch (${br}) as the ORIGINAL branch to return to when done. ` : '') +
174
- `Steps: (1) in that project's working tree, stage and commit the modified files with a clear conventional-commit message ${branchClause}; ` +
175
- `(2) push the branch; (3) open a PR against the project's main branch using the right CLI for the repo host (gh for GitHub, az repos for ADO); ` +
176
- `(4) link it to the tracker by calling POST /api/pull-requests/link with ${linkBody} ${trackNote}; ` +
177
- `(5) finally, ${restoreClause} do not leave the new PR branch checked out. ` +
178
- `Then show me the PR URL.`;
171
+
172
+ let message;
173
+ if (checkoutMode === 'worktree') {
174
+ // Worktree-mode projects: the server moves the live-checkout changes into an
175
+ // isolated worktree and restores the live checkout clean, so the commit /
176
+ // push / PR all happen OFF the operator's tree. CC never runs git in the
177
+ // live checkout that is what kept leaking commits onto `main`.
178
+ message =
179
+ `Create a PR from the local changes you just made in the ${proj} project. ` +
180
+ `This project uses isolated worktrees, so do NOT commit, branch, or push in its live checkout. ` +
181
+ `Steps: (1) call POST /api/pr-action/prepare-create-pr-worktree with {"project":"${proj}"} — the server moves your uncommitted changes into a fresh isolated worktree, restores the live checkout clean, and returns {worktreePath, branch, baseBranch}; ` +
182
+ `(2) run all git from inside worktreePath (use \`git -C <worktreePath> ...\`): stage and commit the changes on the returned branch with a clear conventional-commit message; ` +
183
+ `(3) push that branch; (4) open a PR against baseBranch using the right CLI for the repo host (gh for GitHub, az repos for ADO); ` +
184
+ `(5) link it by calling POST /api/pull-requests/link with ${linkBody} ${trackNote}; ` +
185
+ `(6) finally call POST /api/pr-action/cleanup-create-pr-worktree with {"project":"${proj}","worktreePath":"<worktreePath>"} to remove the worktree. ` +
186
+ `Never git-commit/-push in the live checkout. Then show me the PR URL.`;
187
+ } else {
188
+ // Live-checkout projects: operate in place, but branch off main and restore
189
+ // the original branch so the PR branch is never left checked out.
190
+ const branchClause = br
191
+ ? `(the working tree is on branch ${br}; if ${br} is the project's default/main branch, create a new well-named branch off it and commit there rather than committing to ${br} directly)`
192
+ : 'on a new well-named branch off the project main branch';
193
+ const restoreClause = br
194
+ ? `switch the working tree back to the ORIGINAL branch (${br})`
195
+ : 'switch the working tree back to the original branch it started on';
196
+ message =
197
+ `Create a PR from the local changes you just made in the ${proj} project. ` +
198
+ (br ? `Remember the working tree's current branch (${br}) as the ORIGINAL branch to return to when done. ` : '') +
199
+ `Steps: (1) in that project's working tree, stage and commit the modified files with a clear conventional-commit message ${branchClause}; ` +
200
+ `(2) push the branch; (3) open a PR against the project's main branch using the right CLI for the repo host (gh for GitHub, az repos for ADO); ` +
201
+ `(4) link it to the tracker by calling POST /api/pull-requests/link with ${linkBody} ${trackNote}; ` +
202
+ `(5) finally, ${restoreClause} — do not leave the new PR branch checked out. ` +
203
+ `Then show me the PR URL.`;
204
+ }
179
205
  return [{ kind: CREATE_PR_FOLLOWUP.kind, label: CREATE_PR_FOLLOWUP.label, project: proj, branch: br || null, message }];
180
206
  }
181
207
 
package/engine/shared.js CHANGED
@@ -4359,6 +4359,7 @@ const FAILURE_CLASS = {
4359
4359
  WORKSPACE_MANIFEST_URL: 'workspace-manifest-url-forbidden', // W-mq07avbk000m5543: out-of-scope external URL fetch. Non-retryable as-is.
4360
4360
  SPAWN_PHASE_STALL: 'spawn-phase-stall', // W-mq0e2dae000a003d: process spawned and ran startup-only events (MCP init / hooks) but never emitted real task progress; CPU usage stayed below threshold past the grace window. Engine kills the wedged child and treats this as retryable (fresh-session) so a re-spawn can clear a transient MCP wedge.
4361
4361
  OUTPUT_TRUNCATED: 'output-truncated', // P-8e4c2a17: the agent streamed more stdout than the engine's hard capture cap (engine.js AGENT_OUTPUT_CAP_BYTES, 1MB) BEFORE the terminal `result` event arrived. The result (session id, completion block, final text) lives at the END of the stream, so it fell outside the captured window and parseOutput found nothing — the dispatch would otherwise fail as an opaque UNKNOWN and retry to death. Surfaced loudly with an actionable message; non-retryable (mechanical retry just reproduces the overflow — the agent must reduce output volume or the task must be split).
4362
+ VERIFY_MISSING_PR: 'verify-missing-pr', // W-mqsk1ip00006cbae: a verify WI exited done but no PR was attached (neither _prUrl/_pr on the item nor a matching pull-requests.json entry for the plan). Flipped to failed so the plan doesn't silently advance without an E2E PR. Retryable — agent may have phantom-crashed before pushing the branch.
4362
4363
  UNKNOWN: 'unknown', // Unclassified failure
4363
4364
  };
4364
4365
  const ESCALATION_POLICY = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2252",
3
+ "version": "0.1.2253",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "minions": "bin/minions.js"
@@ -96,7 +96,11 @@ curl -s -X POST http://localhost:{{dashboard_port}}/api/pr-action/offer-create-p
96
96
  -H 'Content-Type: application/json' -H 'X-CC-Turn-Id: {{cc_turn_id}}' \
97
97
  -d '{"project":"<project name>"}'
98
98
  ```
99
- This checks the project's working tree (`git status --porcelain`) and, when there are uncommitted changes, returns a **`[Create PR]`** follow-up chip to the user (same chip mechanism as the `pr-action` `[Comment]`/`[Fix once]`/`[Track for auto-fix]` chips). When the user clicks it, you'll receive a turn instructing you to commit → push → open the PR → link it to the tracker → switch the working tree back to the original branch (never leave the new PR branch checked out in the shared operator tree). Pass `"contextOnly":true` if the PR should be tracked-but-not-auto-reviewed; omit it to have the engine auto-manage the PR (review → fix → re-review → auto-merge). If `hasChanges` is `false`, there's nothing to PR — skip the offer. Don't commit/push on your own initiative; surface the chip and let the user decide.
99
+ This checks the project's working tree (`git status --porcelain`) and, when there are uncommitted changes, returns a **`[Create PR]`** follow-up chip to the user (same chip mechanism as the `pr-action` `[Comment]`/`[Fix once]`/`[Track for auto-fix]` chips). When the user clicks it, you'll receive a turn with **explicit step-by-step instructions follow them exactly**. The instructions adapt to the project's checkout pattern, so just do what the turn says:
100
+ - **Worktree-mode projects** (the default): the turn tells you to first `POST /api/pr-action/prepare-create-pr-worktree {"project":"…"}`. The server moves your uncommitted changes into an **isolated worktree** and restores the live checkout clean, returning `{worktreePath, branch, baseBranch}`. You then run **all git from inside `worktreePath`** (`git -C <worktreePath> …`) to commit/push/open the PR, then `POST /api/pr-action/cleanup-create-pr-worktree` to remove it. **Never commit, branch, or push in the live checkout** — that leaks commits onto the operator's `main`.
101
+ - **Live-mode projects**: the turn tells you to commit on a new branch off main in the live checkout and switch back to the original branch when done.
102
+
103
+ Pass `"contextOnly":true` if the PR should be tracked-but-not-auto-reviewed; omit it to have the engine auto-manage the PR (review → fix → re-review → auto-merge). If `hasChanges` is `false`, there's nothing to PR — skip the offer. Don't commit/push on your own initiative; surface the chip and let the user decide.
100
104
 
101
105
  ## When to dispatch vs answer inline
102
106