@yemi33/minions 0.1.2319 → 0.1.2321
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 +7 -2
- package/docs/live-checkout-mode.md +16 -0
- package/engine/pr-action.js +28 -10
- package/engine/runtimes/copilot.js +51 -9
- package/package.json +1 -1
package/dashboard.js
CHANGED
|
@@ -13767,11 +13767,16 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13767
13767
|
// routes the changes through an isolated worktree (so nothing is ever
|
|
13768
13768
|
// committed in the operator's live main). See engine/create-pr-worktree.js.
|
|
13769
13769
|
const checkoutMode = shared.resolveCheckoutMode(project);
|
|
13770
|
+
// Resolve the project's actual main/base branch instead of trusting
|
|
13771
|
+
// whatever `git rev-parse --abbrev-ref HEAD` returned as a safe base —
|
|
13772
|
+
// the live checkout may be sitting on a stale/topic branch whose
|
|
13773
|
+
// unrelated commits would otherwise leak into the new PR (W-mr3jbpru).
|
|
13774
|
+
const mainBranch = shared.resolveMainBranch(project.localPath, project.mainBranch);
|
|
13770
13775
|
const followups = hasChanges
|
|
13771
|
-
? prAction.buildCreatePrFollowups({ project: projectName, branch, contextOnly: !!body?.contextOnly, checkoutMode })
|
|
13776
|
+
? prAction.buildCreatePrFollowups({ project: projectName, branch, contextOnly: !!body?.contextOnly, checkoutMode, mainBranch })
|
|
13772
13777
|
: [];
|
|
13773
13778
|
recordCcTurnIfPresent(req, { kind: 'pr-action', id: '', title: `offer create-pr (${projectName})`, project: projectName, followups });
|
|
13774
|
-
return jsonReply(res, 200, { ok: true, hasChanges, branch, changedFileCount, checkoutMode, followups }, req);
|
|
13779
|
+
return jsonReply(res, 200, { ok: true, hasChanges, branch, changedFileCount, checkoutMode, mainBranch, followups }, req);
|
|
13775
13780
|
} catch (e) {
|
|
13776
13781
|
return jsonReply(res, e.statusCode || 400, { error: e.message }, req);
|
|
13777
13782
|
}
|
|
@@ -136,6 +136,21 @@ Live-mode agents run **in-place** in the operator's checkout, so when a dispatch
|
|
|
136
136
|
|
|
137
137
|
The core invariant holds end-to-end through restore: **the engine only ever switches branches — it never `git reset`s, `git clean`s, or `git stash`es the operator's tree, and never passes `--force`.** The one addition — the self-healing auto-commit — only ever runs `git add`/`git commit` on the engine's *own* agent branch, never on operator refs, and never discards.
|
|
138
138
|
|
|
139
|
+
## Create-PR chip safety (CC-initiated, separate from the dispatch path)
|
|
140
|
+
|
|
141
|
+
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.
|
|
142
|
+
|
|
143
|
+
**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.
|
|
144
|
+
|
|
145
|
+
**The contract now (unconditional, current-branch never trusted as base).**
|
|
146
|
+
|
|
147
|
+
1. The handler resolves the project's **actual** main/base branch with `shared.resolveMainBranch(project.localPath, project.mainBranch)` — it does **not** trust `git rev-parse --abbrev-ref HEAD` as a safe base — and threads it to `buildCreatePrFollowups({ …, mainBranch })`.
|
|
148
|
+
2. The CC-facing message is **unconditional**: regardless of what is currently checked out, it instructs the agent to build the PR on a **fresh branch off the up-to-date `origin/<mainBranch>` tip**, via `git stash push --include-untracked` → `git fetch origin <mainBranch>` → `git checkout -B <new-well-named-branch> origin/<mainBranch>` → `git stash pop` → commit → push → open the PR against `<mainBranch>` → link via `/api/pull-requests/link` → **switch the working tree back to the original branch** it started on (the same restore step the old message had). The old `if <current-branch> is the project's default/main branch` conditional is gone entirely.
|
|
149
|
+
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.
|
|
150
|
+
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.
|
|
151
|
+
|
|
152
|
+
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`.
|
|
153
|
+
|
|
139
154
|
## Operator workflow
|
|
140
155
|
|
|
141
156
|
### Enabling live mode (dashboard)
|
|
@@ -253,6 +268,7 @@ Live-checkout mode is deliberately small. These are NOT supported and will not b
|
|
|
253
268
|
| `engine.js` — worktree-pool / orphan-GC short-circuits | `worktreePath===null` no-ops in live mode (P-a3f9b206). |
|
|
254
269
|
| `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). |
|
|
255
270
|
| `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). |
|
|
271
|
+
| `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). |
|
|
256
272
|
| `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). |
|
|
257
273
|
| `test/unit/{resolve-spawn-paths-live-mode,prepare-live-checkout,spawn-agent-live-mode-wiring,live-checkout-auto-stash}.test.js` | Wiring and contract tests (P-a3f9b208; auto-stash helpers W-mqtvnnj1000357fa). |
|
|
258
274
|
| `engine/shared.js` — `FAILURE_CLASS.LIVE_CHECKOUT_DIRTY` | Non-retryable refusal class. |
|
package/engine/pr-action.js
CHANGED
|
@@ -154,14 +154,23 @@ const CREATE_PR_FOLLOWUP = Object.freeze({ kind: 'create-pr', label: 'Create PR'
|
|
|
154
154
|
* changes through an isolated git worktree so nothing is ever
|
|
155
155
|
* committed in the operator's live `main`. Defaults to `'live'`
|
|
156
156
|
* for back-compat when a caller doesn't resolve it.
|
|
157
|
+
* `mainBranch` — the project's resolved main/base branch
|
|
158
|
+
* (`shared.resolveMainBranch(project.localPath, project.mainBranch)`).
|
|
159
|
+
* In `'live'` mode the PR is ALWAYS built on a fresh branch off
|
|
160
|
+
* the up-to-date `origin/<mainBranch>` tip — never off whatever
|
|
161
|
+
* branch the live checkout happened to have checked out — so a
|
|
162
|
+
* stale/topic branch's unrelated commits can't leak into the new
|
|
163
|
+
* PR (W-mr3jbpru: "Create-PR reused stale topic branch as base").
|
|
164
|
+
* Falls back to `'main'` when a caller doesn't resolve it.
|
|
157
165
|
*
|
|
158
166
|
* Each chip: `{ kind:'create-pr', label, project, branch, message }` where
|
|
159
167
|
* `message` is the Command Center turn the chip click should send.
|
|
160
168
|
*/
|
|
161
|
-
function buildCreatePrFollowups({ project, branch, contextOnly = false, checkoutMode = 'live' } = {}) {
|
|
169
|
+
function buildCreatePrFollowups({ project, branch, contextOnly = false, checkoutMode = 'live', mainBranch = '' } = {}) {
|
|
162
170
|
const proj = (project && String(project).trim()) || '';
|
|
163
171
|
if (!proj) return [];
|
|
164
172
|
const br = branch && String(branch).trim() ? String(branch).trim() : '';
|
|
173
|
+
const mainRef = mainBranch && String(mainBranch).trim() ? String(mainBranch).trim() : 'main';
|
|
165
174
|
const linkBody = contextOnly
|
|
166
175
|
? `{"url":"<new PR url>","project":"${proj}","contextOnly":true}`
|
|
167
176
|
: `{"url":"<new PR url>","project":"${proj}","contextOnly":false}`;
|
|
@@ -185,21 +194,30 @@ function buildCreatePrFollowups({ project, branch, contextOnly = false, checkout
|
|
|
185
194
|
`(6) finally call POST /api/pr-action/cleanup-create-pr-worktree with {"project":"${proj}","worktreePath":"<worktreePath>"} to remove the worktree. ` +
|
|
186
195
|
`Never git-commit/-push in the live checkout. Then show me the PR URL.`;
|
|
187
196
|
} else {
|
|
188
|
-
// Live-checkout projects: operate in place, but
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
197
|
+
// Live-checkout projects: operate in place, but ALWAYS build the PR on a
|
|
198
|
+
// fresh branch off the up-to-date origin/<mainBranch> tip — never off
|
|
199
|
+
// whatever branch the live checkout happened to have checked out. Trusting
|
|
200
|
+
// the current branch let a stale/topic branch's unrelated commits leak into
|
|
201
|
+
// the new PR (W-mr3jbpru). The stash → fetch → checkout -B → pop sequence
|
|
202
|
+
// captures the intended changes and re-applies them on a clean base; the
|
|
203
|
+
// original branch is restored at the end so the PR branch is never left
|
|
204
|
+
// checked out in the shared operator tree.
|
|
193
205
|
const restoreClause = br
|
|
194
206
|
? `switch the working tree back to the ORIGINAL branch (${br})`
|
|
195
207
|
: 'switch the working tree back to the original branch it started on';
|
|
196
208
|
message =
|
|
197
209
|
`Create a PR from the local changes you just made in the ${proj} project. ` +
|
|
198
210
|
(br ? `Remember the working tree's current branch (${br}) as the ORIGINAL branch to return to when done. ` : '') +
|
|
199
|
-
`
|
|
200
|
-
`
|
|
201
|
-
`(
|
|
202
|
-
`(
|
|
211
|
+
`IMPORTANT: do NOT commit onto whatever branch is currently checked out — it may carry unrelated commits. ` +
|
|
212
|
+
`ALWAYS build the PR on a fresh branch off the up-to-date ${mainRef} tip so no stale/topic branch commits leak into the new PR. ` +
|
|
213
|
+
`Steps: (1) stash your intended uncommitted changes with \`git stash push --include-untracked\`; ` +
|
|
214
|
+
`(2) fetch the latest base with \`git fetch origin ${mainRef}\`; ` +
|
|
215
|
+
`(3) create a fresh well-named branch off the remote main tip with \`git checkout -B <new-well-named-branch> origin/${mainRef}\` (never branch off whatever was previously checked out); ` +
|
|
216
|
+
`(4) re-apply your changes with \`git stash pop\` — if this reports a merge conflict, STOP and surface the conflict to me rather than force-resolving it silently; ` +
|
|
217
|
+
`(5) stage and commit the changes with a clear conventional-commit message; ` +
|
|
218
|
+
`(6) push the new branch; (7) open a PR against ${mainRef} using the right CLI for the repo host (gh for GitHub, az repos for ADO); ` +
|
|
219
|
+
`(8) link it to the tracker by calling POST /api/pull-requests/link with ${linkBody} ${trackNote}; ` +
|
|
220
|
+
`(9) finally, ${restoreClause} — do not leave the new PR branch checked out. ` +
|
|
203
221
|
`Then show me the PR URL.`;
|
|
204
222
|
}
|
|
205
223
|
return [{ kind: CREATE_PR_FOLLOWUP.kind, label: CREATE_PR_FOLLOWUP.label, project: proj, branch: br || null, message }];
|
|
@@ -1098,6 +1098,28 @@ function createStreamConsumer(ctx) {
|
|
|
1098
1098
|
// form `...prev.next...`. First push is NEVER prefixed (would leave every
|
|
1099
1099
|
// reply with a leading blank line). Reset by reset() on stream restart.
|
|
1100
1100
|
let _hadPriorAssistantMessage = false;
|
|
1101
|
+
// `terminalText` accumulates EVERY non-tool assistant.message across the
|
|
1102
|
+
// whole turn, including ones separated by intervening tool calls. That
|
|
1103
|
+
// whole-turn scope is wrong for `ctx.setText(...)` on the `result` event:
|
|
1104
|
+
// the dashboard's `ccSegmentsFinalize`/`_ccSegMergeText` (render-utils.js)
|
|
1105
|
+
// expect the authoritative final text to describe ONLY the *trailing* text
|
|
1106
|
+
// segment (correct for Claude's single last-message `result.result`), and
|
|
1107
|
+
// merge it into just the last segment. Sending the whole-turn text there
|
|
1108
|
+
// duplicated earlier paragraphs (W-mr3rdn9j000u6d5d) because the trailing
|
|
1109
|
+
// segment only ever received the post-tool delta via ctx.pushText, not the
|
|
1110
|
+
// full accumulated turn.
|
|
1111
|
+
//
|
|
1112
|
+
// `trailingText`/`_hadPriorTrailingMessage` mirror `terminalText`/
|
|
1113
|
+
// `_hadPriorAssistantMessage` but are reset to '' / false on every tool
|
|
1114
|
+
// boundary (a real tool call — not `task_complete`), so their scope always
|
|
1115
|
+
// matches the trailing segment the dashboard will merge into.
|
|
1116
|
+
let trailingText = '';
|
|
1117
|
+
let _hadPriorTrailingMessage = false;
|
|
1118
|
+
|
|
1119
|
+
function _resetTrailingSegment() {
|
|
1120
|
+
trailingText = '';
|
|
1121
|
+
_hadPriorTrailingMessage = false;
|
|
1122
|
+
}
|
|
1101
1123
|
|
|
1102
1124
|
function _captureTaskComplete(summary, success = true, { clearBuffer = false } = {}) {
|
|
1103
1125
|
if (typeof summary !== 'string' || !summary) return;
|
|
@@ -1117,7 +1139,7 @@ function createStreamConsumer(ctx) {
|
|
|
1117
1139
|
// The result event is the first Copilot event that contains the resumable
|
|
1118
1140
|
// sessionId. Do not mark the earlier assistant.message as terminal or
|
|
1119
1141
|
// Minions can resolve before session persistence data is available.
|
|
1120
|
-
const finalText =
|
|
1142
|
+
const finalText = trailingText || copilotMessageBuffer || taskCompleteSummary;
|
|
1121
1143
|
if (finalText) ctx.setText(finalText);
|
|
1122
1144
|
}
|
|
1123
1145
|
|
|
@@ -1149,21 +1171,30 @@ function createStreamConsumer(ctx) {
|
|
|
1149
1171
|
// the terminal answer. A non-tool assistant.message overrides any
|
|
1150
1172
|
// streamed deltas (Copilot's authoritative final text for the turn).
|
|
1151
1173
|
if (content && !hasTools) {
|
|
1152
|
-
// Second-and-later terminating message
|
|
1153
|
-
//
|
|
1154
|
-
//
|
|
1155
|
-
//
|
|
1156
|
-
//
|
|
1157
|
-
//
|
|
1158
|
-
//
|
|
1159
|
-
|
|
1174
|
+
// Second-and-later terminating message within the SAME trailing
|
|
1175
|
+
// segment (no tool boundary since): prefix the pushed text with
|
|
1176
|
+
// `\n\n` so the dashboard merge sees a clean paragraph boundary
|
|
1177
|
+
// instead of welding `<prev>.<next>` together (W-mq1jwwqo000f20d9
|
|
1178
|
+
// — CC chunk welding fix). The first push in a segment must NOT be
|
|
1179
|
+
// prefixed (would leave every reply/segment with a leading blank
|
|
1180
|
+
// line). This is now gated on `_hadPriorTrailingMessage` (reset on
|
|
1181
|
+
// every real tool boundary below), not the whole-turn flag — a
|
|
1182
|
+
// tool boundary already opens a brand-new segment on the dashboard
|
|
1183
|
+
// side (ccSegmentsApplyChunk), so the first post-tool push needs no
|
|
1184
|
+
// extra separator, and `trailingText` must track exactly what was
|
|
1185
|
+
// pushed so ctx.setText(...) on `result` reconciles cleanly
|
|
1186
|
+
// (W-mr3rdn9j000u6d5d).
|
|
1187
|
+
const pushed = _hadPriorTrailingMessage ? '\n\n' + content : content;
|
|
1160
1188
|
terminalText = _hadPriorAssistantMessage ? terminalText + '\n\n' + content : content;
|
|
1189
|
+
trailingText = _hadPriorTrailingMessage ? trailingText + '\n\n' + content : content;
|
|
1161
1190
|
ctx.pushText(pushed);
|
|
1162
1191
|
_hadPriorAssistantMessage = true;
|
|
1192
|
+
_hadPriorTrailingMessage = true;
|
|
1163
1193
|
}
|
|
1164
1194
|
copilotMessageBuffer = '';
|
|
1165
1195
|
}
|
|
1166
1196
|
if (Array.isArray(data.toolRequests)) {
|
|
1197
|
+
let sawRealTool = false;
|
|
1167
1198
|
for (const tr of data.toolRequests) {
|
|
1168
1199
|
if (!tr || !tr.name) continue;
|
|
1169
1200
|
if (tr.name === 'task_complete') {
|
|
@@ -1171,7 +1202,12 @@ function createStreamConsumer(ctx) {
|
|
|
1171
1202
|
continue;
|
|
1172
1203
|
}
|
|
1173
1204
|
ctx.pushToolUse(tr.name, tr.arguments || {});
|
|
1205
|
+
sawRealTool = true;
|
|
1174
1206
|
}
|
|
1207
|
+
// A real (non-task_complete) tool call closes the trailing segment —
|
|
1208
|
+
// the NEXT non-tool assistant.message starts a fresh segment on the
|
|
1209
|
+
// dashboard side, so its finalize-time scope must reset here too.
|
|
1210
|
+
if (sawRealTool) _resetTrailingSegment();
|
|
1175
1211
|
}
|
|
1176
1212
|
return;
|
|
1177
1213
|
}
|
|
@@ -1188,6 +1224,11 @@ function createStreamConsumer(ctx) {
|
|
|
1188
1224
|
if (!ctx.toolUseAlreadySeen(name, input)) {
|
|
1189
1225
|
ctx.pushToolUse(name, input);
|
|
1190
1226
|
}
|
|
1227
|
+
// Same trailing-segment boundary reset as the toolRequests branch above
|
|
1228
|
+
// — `tool.execution_start` is the standalone event shape observed in
|
|
1229
|
+
// the primary repro (assistant.message → tool.execution_start →
|
|
1230
|
+
// assistant.message → result).
|
|
1231
|
+
_resetTrailingSegment();
|
|
1191
1232
|
}
|
|
1192
1233
|
}
|
|
1193
1234
|
|
|
@@ -1196,6 +1237,7 @@ function createStreamConsumer(ctx) {
|
|
|
1196
1237
|
terminalText = '';
|
|
1197
1238
|
taskCompleteSummary = '';
|
|
1198
1239
|
_hadPriorAssistantMessage = false;
|
|
1240
|
+
_resetTrailingSegment();
|
|
1199
1241
|
}
|
|
1200
1242
|
|
|
1201
1243
|
return { consume, reset };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2321",
|
|
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"
|