@yemi33/minions 0.1.2259 → 0.1.2261

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.
@@ -70,6 +70,25 @@ Any violation rejects the **whole turn** with a typed error envelope (`code: 'in
70
70
 
71
71
  **No new `engine.*` flag.** The limits above are module-level constants in `dashboard.js` (`CC_IMAGE_MAX_COUNT`, `CC_IMAGE_MAX_DECODED_BYTES`, `CC_IMAGE_MIME_ALLOWLIST`), not config keys; `engine/shared.js` `ENGINE_DEFAULTS` was not touched. Per CLAUDE.md Best Practice #9 (Settings parity), no Settings toggle is added because no new `engine.*` flag was introduced. Runtime selection that determines whether images are accepted is the existing `engine.ccCli` / `ccModel` override, which already has a Settings control.
72
72
 
73
+ ## Create-PR flow — checkout-mode-aware (PR #387)
74
+
75
+ The "Create PR" chip offered by CC after a local edit follows the **same checkout pattern** as a normal dispatch for the project (`shared.resolveCheckoutMode`).
76
+
77
+ - **`checkoutMode: 'live'`** (default for live-mode projects): unchanged behavior — the agent commits and pushes directly in the operator's live checkout as the CC system prompt instructs.
78
+ - **`checkoutMode: 'worktree'`** (the default for most projects): the engine routes the changes through an isolated worktree deterministically and server-side so nothing depends on the LLM running git correctly.
79
+
80
+ For worktree-mode projects the flow uses **`engine/create-pr-worktree.js`**:
81
+
82
+ 1. `prepareCreatePrWorktree` captures the live checkout's uncommitted changes (tracked diff via `git diff --binary HEAD` + untracked files) and applies them into a fresh worktree on a `cc-pr/<project>-<uid>` branch. 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 (operator never loses work).
83
+ 2. The agent then commits, pushes, and opens the PR from inside the worktree.
84
+ 3. `cleanupCreatePrWorktree` removes the worktree, refusing any path without the `.minions-worktree` ownership marker.
85
+
86
+ Dashboard endpoints added by this feature:
87
+ - `POST /api/pr-action/prepare-create-pr-worktree` — called before the agent's git operations
88
+ - `POST /api/pr-action/cleanup-create-pr-worktree` — called after the PR is open (or on failure)
89
+
90
+ The `buildCreatePrFollowups` helper in `engine/pr-action.js` emits worktree-aware instructions for worktree mode and the legacy live-checkout instructions for live mode; `dashboard.js /api/pr-action/offer-create-pr` resolves the checkout mode and passes it through.
91
+
73
92
  ## Per-turn surfacing pipeline
74
93
 
75
94
  CC handler generates `ccTurnId = 'cct-' + shared.uid()` per request; injected into sysprompt AND prompt body via `_ccTurnHeaderPart(turnId)` (load-bearing: on resumed sessions `engine/llm.js` skips re-sending the sysprompt, so without body injection CC keeps the stale turn ID). Handler reads via `_readCcTurnIdHeader(req)` and calls `_recordCcTurnCreation(turnId, ...)` on success. End-of-turn: `_buildSyntheticActionResultsForTurn` produces synthetic `{action, result}` pairs (`_serverExecuted: true`). Client renders as standalone `role='action'` messages outside the assistant bubble. TTL: 5 min. Endpoints wired: `/api/work-items`, `/api/notes`, `/api/plan`, `/api/knowledge`, `/api/watches`.
@@ -174,6 +174,40 @@ EPERM/EBUSY stragglers that the dispatch-end GC couldn't reap and sweeps
174
174
  the `git worktree list` registry for OUT-of-root entries the in-root
175
175
  scanner is blind to.
176
176
 
177
+ ### Scratch-dir skip and bounded reaper (PR #388)
178
+
179
+ The worktree root (`<localPath>/../worktrees/`) is a **shared namespace**: it
180
+ holds both engine-managed worktrees AND `.agent-temp` — the scratch base where
181
+ dispatched agents and agent-run test suites write throwaway git repos. Before
182
+ this fix, `cleanup.js` enumerated the worktree root one level deep on every
183
+ cleanup cycle and called `removeWorktree` on each entry (which correctly
184
+ refused — scratch repos are not worktrees). With 26k+ leaked scratch dirs that
185
+ readdir + stat walk stalled the tick loop for minutes, surfacing as "engine
186
+ stale" (process alive but `lastTickAt` minutes in the past).
187
+
188
+ **Architectural invariant:** any dot-prefixed entry in the worktree root is
189
+ infra / scratch, never a managed worktree. Engine-created worktree dir names
190
+ (`buildWorktreeDirName`) are always `W-…` or `<project>-<branch>-<hash>` and
191
+ never start with `.`. The predicate `shared.isWorktreeRootInfraEntry(name)`
192
+ (returns `true` when `name` starts with `.`) is now applied at **both**
193
+ on-disk enumeration sites — `cleanup.js` worktree scan and
194
+ `worktree-gc.js#pruneOrphanWorktrees` — so the sweep is O(1) on scratch size
195
+ regardless of backlog.
196
+
197
+ **Bounded TTL reaper (`cleanup.js#reapAgentScratch`).** Skipping scratch
198
+ prevents stalls but doesn't shrink the pile. `reapAgentScratch` deletes
199
+ entries under `<worktreeRoot>/.agent-temp`
200
+ (`shared.WORKTREE_SCRATCH_DIR_NAME`) whose `mtime` is older than **6 h**
201
+ (comfortably above the 5 h `agentTimeout`, so only dead-dispatch scratch is
202
+ touched). Scan is capped at **2,000 entries per cleanup cycle** so the reaper
203
+ itself can never stall the tick on a huge backlog — it drains across cycles.
204
+ Deletion is plain `fs.rmSync` (not `removeWorktree`) because scratch repos are
205
+ not registered worktrees.
206
+
207
+ When adding new code that enumerates the worktree root directory, apply
208
+ `shared.isWorktreeRootInfraEntry(name)` to skip dot-prefixed entries before
209
+ calling any `removeWorktree` path.
210
+
177
211
  ### Ownership marker — out-of-root GC only touches engine worktrees (W-mqecdoot)
178
212
 
179
213
  A project repo's `git worktree list` includes EVERY worktree registered
@@ -118,9 +118,17 @@ async function prepareCreatePrWorktree({
118
118
  }
119
119
 
120
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 */ }
121
+ try {
122
+ await git(['-C', localPath, 'checkout', '--', '.']);
123
+ for (const rel of untracked) {
124
+ try { fsm.rmSync(path.join(localPath, rel.replace(/\/$/, '')), { recursive: true, force: true }); } catch { /* ignore */ }
125
+ }
126
+ } catch (e) {
127
+ try { await git(['-C', localPath, 'worktree', 'remove', '--force', wtPath]); } catch { /* leak rather than double-throw */ }
128
+ throw new Error(
129
+ `prepareCreatePrWorktree: failed to restore live checkout — ${e.message}. ` +
130
+ `Worktree at ${wtPath} may need manual cleanup.`,
131
+ );
124
132
  }
125
133
 
126
134
  log('info', `[cc-create-pr] staged ${project.name} changes into isolated worktree ${wtPath} on branch ${branchName} (live checkout restored)`);
package/engine/shared.js CHANGED
@@ -5720,14 +5720,32 @@ function resolveProjectRootDir(localPath, minionsDir) {
5720
5720
  * unaffected — it uses GH_TOKEN / the OS credential store, not files under the
5721
5721
  * home — and the operator's real `~/.copilot` (interactive copilot) is untouched.
5722
5722
  *
5723
- * Placed on the MINIONS_DIR volume (copilot's session-store can grow to GBs, so
5724
- * keep it off a possibly-full system drive) but OUTSIDE the repo, at the drive
5725
- * root, so concurrent git branch switches in the working tree can never delete or
5726
- * dirty it. Stable path so copilot session resume (`--resume`) stays consistent
5727
- * across spawns. Falls back to the user home when no minionsDir is given.
5723
+ * Placed OUTSIDE the repo so concurrent git branch switches in the working tree
5724
+ * can never delete or dirty it, on the MINIONS_DIR volume (copilot's session-store
5725
+ * can grow to GBs, so keep it off a possibly-full system drive). Stable path so
5726
+ * copilot session resume (`--resume`) stays consistent across spawns. Falls back
5727
+ * to the user home when no minionsDir is given.
5728
+ *
5729
+ * Base selection is cross-platform (W-copilot-home-posix-root): on Windows the
5730
+ * MINIONS_DIR drive root (`C:\`, `D:\`, or a UNC share root) is user-writable, so
5731
+ * we anchor there. On POSIX the filesystem root (`/`) is NOT user-writable — a
5732
+ * home placed there can never be created (`mkdir` EACCES), `ensureAgentCopilotHome`
5733
+ * fail-opens and returns the uncreatable path anyway, and every spawned
5734
+ * `copilot --acp` / copilot agent then inherits a `COPILOT_HOME` it can't use and
5735
+ * exits code 1 silently (surfaced in CC as "Load failed"). So when the drive root
5736
+ * is the bare POSIX root we fall back to the user home — repo-external, writable,
5737
+ * and on the user's own volume.
5728
5738
  */
5729
5739
  function resolveAgentCopilotHome(minionsDir) {
5730
- const base = minionsDir ? path.parse(path.resolve(String(minionsDir))).root : os.homedir();
5740
+ let base;
5741
+ if (minionsDir) {
5742
+ const root = path.parse(path.resolve(String(minionsDir))).root;
5743
+ // The POSIX filesystem root ('/') is not user-writable; Windows drive/UNC
5744
+ // roots are. Anchor at the user home in the unwritable-root case.
5745
+ base = (root === '/' || root === path.sep) ? os.homedir() : root;
5746
+ } else {
5747
+ base = os.homedir();
5748
+ }
5731
5749
  return path.join(base, '.minions-agent-copilot-home');
5732
5750
  }
5733
5751
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2259",
3
+ "version": "0.1.2261",
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"