@yemi33/minions 0.1.2414 → 0.1.2416

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.
@@ -185,7 +185,7 @@ const RENDER_VERSIONS = {
185
185
  // Bumped to 4 by W-mqrdavob (Agents table: per-agent charter editor column).
186
186
  // Bumped to 5 by W-mr0qs0vw (Worker Pool: CC session idle-timeout field).
187
187
  // Bumped to 6 for the fleet ACP pool opt-in default and warning copy.
188
- settings: 6,
188
+ settings: 7,
189
189
  };
190
190
  const _sectionCache = {};
191
191
  const _lastValueByKey = {};
@@ -713,6 +713,13 @@ async function openSettings() {
713
713
  // non-dispatch toggles that historically lived under Auto-fix; placed
714
714
  // adjacent to Auto-fix so operators following the legacy mental model find
715
715
  // the moved flags quickly.
716
+ // W-mryz3r7h — a section may opt into a collapsible rail fold by declaring a
717
+ // `group`. Grouped sections render as an indented child list beneath a fold
718
+ // header (see RAIL_GROUPS + the railHtml builder below); their PANES stay in
719
+ // the content area unchanged. Copilot/Claude Tuning + Budget + the former
720
+ // standalone Advanced pane now live under the "Advanced" fold (collapsed by
721
+ // default). The former standalone Advanced leaf is nested here too so there is
722
+ // only ever one thing labeled "Advanced".
716
723
  const sections = [
717
724
  { id: 'runtime', label: 'Agents Runtime & Models', featured: true, html: paneRuntime },
718
725
  { id: 'autofix', label: 'Auto-fix & Review Loop', featured: true, html: paneAutoFix },
@@ -723,15 +730,19 @@ async function openSettings() {
723
730
  { id: 'polling', label: 'Polling', html: panePolling },
724
731
  { id: 'concurrency', label: 'Concurrency & Timeouts', html: paneConcurrency },
725
732
  { id: 'worktree', label: 'Worker Pool & Worktrees', html: paneWorktree },
726
- { id: 'copilot', label: 'Copilot Tuning', html: paneCopilot },
727
- { id: 'claude', label: 'Claude Tuning', html: paneClaude },
728
733
  { id: 'harness', label: 'Runtime Harness', html: paneHarness },
729
- { id: 'budget', label: 'Budget', html: paneBudget },
730
734
  { id: 'maxturns', label: 'Max Turns', html: paneMaxTurns },
731
735
  { id: 'features', label: 'Feature Flags', html: paneFeatures },
732
- { id: 'advanced', label: 'Advanced', html: paneAdvanced },
736
+ { id: 'copilot', label: 'Copilot Tuning', html: paneCopilot, group: 'advanced' },
737
+ { id: 'claude', label: 'Claude Tuning', html: paneClaude, group: 'advanced' },
738
+ { id: 'budget', label: 'Budget', html: paneBudget, group: 'advanced' },
739
+ { id: 'advanced', label: 'Advanced', html: paneAdvanced, group: 'advanced' },
733
740
  ];
734
741
 
742
+ // Rail fold registry: group id → display label. Sections opt in via `group`.
743
+ const RAIL_GROUPS = { advanced: { label: 'Advanced' } };
744
+ const ADV_FOLD_KEY = 'minions.settings.advancedFoldOpen';
745
+
735
746
  // localStorage-persisted active tab. Falls back to the first section when no
736
747
  // saved value or the saved value points at a section we no longer render.
737
748
  const SAVED_TAB_KEY = 'minions.settings.activeTab';
@@ -741,12 +752,41 @@ async function openSettings() {
741
752
  if (saved && sections.some(s => s.id === saved)) activeTab = saved;
742
753
  } catch { /* localStorage may be blocked in some embeds */ }
743
754
 
744
- const railHtml = sections.map(s =>
745
- '<button class="settings-rail-btn' + (s.featured ? ' featured' : '') + (s.id === activeTab ? ' active' : '') + '" data-settings-tab="' + s.id + '" type="button">' +
755
+ // Compute the Advanced fold's initial expanded state. Default (no saved
756
+ // value) is COLLAPSED. Persisted choice survives reloads. If the restored
757
+ // active tab lives inside the fold, force it open so the active rail button
758
+ // is visible on load.
759
+ let advancedFoldOpen = false;
760
+ try { advancedFoldOpen = localStorage.getItem(ADV_FOLD_KEY) === '1'; } catch { /* localStorage may be blocked in some embeds */ }
761
+ if (sections.some(s => s.id === activeTab && s.group === 'advanced')) advancedFoldOpen = true;
762
+ const foldStates = { advanced: advancedFoldOpen };
763
+
764
+ const railBtnHtml = (s, child) =>
765
+ '<button class="settings-rail-btn' + (s.featured ? ' featured' : '') + (child ? ' settings-rail-btn--child' : '') + (s.id === activeTab ? ' active' : '') + '" data-settings-tab="' + s.id + '" type="button">' +
746
766
  '<span class="rail-label">' + escHtml(s.label) + '</span>' +
747
767
  '<span class="match-count" style="display:none">0</span>' +
748
- '</button>'
749
- ).join('');
768
+ '</button>';
769
+
770
+ // Walk the sections in order. Leaves render directly; the first grouped
771
+ // section emits the whole fold (header + indented child list), and later
772
+ // members of the same group are skipped (they were rendered with the header).
773
+ const renderedGroups = new Set();
774
+ const railHtml = sections.map(function(s) {
775
+ if (!s.group) return railBtnHtml(s, false);
776
+ if (renderedGroups.has(s.group)) return '';
777
+ renderedGroups.add(s.group);
778
+ const members = sections.filter(x => x.group === s.group);
779
+ const open = !!foldStates[s.group];
780
+ const label = (RAIL_GROUPS[s.group] && RAIL_GROUPS[s.group].label) || s.group;
781
+ return '<button class="settings-rail-fold' + (open ? ' open' : '') + '" data-settings-fold="' + s.group + '" type="button" aria-expanded="' + (open ? 'true' : 'false') + '">' +
782
+ '<span class="fold-chevron" aria-hidden="true"></span>' +
783
+ '<span class="rail-label">' + escHtml(label) + '</span>' +
784
+ '<span class="match-count" style="display:none">0</span>' +
785
+ '</button>' +
786
+ '<div class="settings-rail-group' + (open ? ' open' : '') + '" data-settings-group="' + s.group + '" role="group" aria-label="' + escHtml(label) + '">' +
787
+ members.map(m => railBtnHtml(m, true)).join('') +
788
+ '</div>';
789
+ }).join('');
750
790
 
751
791
  const panesHtml = sections.map(s =>
752
792
  '<div class="settings-pane' + (s.id === activeTab ? ' active' : '') + '" data-settings-pane="' + s.id + '">' +
@@ -806,6 +846,20 @@ async function openSettings() {
806
846
  });
807
847
  });
808
848
 
849
+ // ── Wire up fold headers (expand/collapse the Advanced group) ───────────
850
+ // <button> headers are keyboard-operable natively (Enter/Space fire click).
851
+ // The user's choice is persisted so it survives reloads; the render-time
852
+ // default is collapsed unless the active tab lives inside the fold.
853
+ body.querySelectorAll('[data-settings-fold]').forEach(function(btn) {
854
+ btn.addEventListener('click', function() {
855
+ const gid = btn.getAttribute('data-settings-fold');
856
+ const open = _toggleSettingsFold(gid);
857
+ if (gid === 'advanced') {
858
+ try { localStorage.setItem(ADV_FOLD_KEY, open ? '1' : '0'); } catch { /* ignore */ }
859
+ }
860
+ });
861
+ });
862
+
809
863
  // ── Cross-tab search box ───────────────────────────────────────────────
810
864
  // Filters every [data-search] row across every pane. Empty query restores
811
865
  // the active-tab view; non-empty query shows ALL panes with matches and
@@ -1257,6 +1311,32 @@ function _selectSettingsTab(id) {
1257
1311
  body.querySelectorAll('[data-settings-pane]').forEach(function(pane) {
1258
1312
  pane.classList.toggle('active', pane.getAttribute('data-settings-pane') === id);
1259
1313
  });
1314
+ // If the selected tab lives inside a collapsed fold, expand the fold so its
1315
+ // rail button is visible. The pane renders regardless of fold state.
1316
+ const activeBtn = body.querySelector('[data-settings-tab="' + id + '"]');
1317
+ const group = activeBtn && activeBtn.closest('[data-settings-group]');
1318
+ if (group) {
1319
+ group.classList.add('open');
1320
+ const gid = group.getAttribute('data-settings-group');
1321
+ const fold = body.querySelector('[data-settings-fold="' + gid + '"]');
1322
+ if (fold) { fold.classList.add('open'); fold.setAttribute('aria-expanded', 'true'); }
1323
+ }
1324
+ }
1325
+
1326
+ // Expand/collapse a rail fold. Returns the resulting open state (or undefined
1327
+ // if the fold isn't mounted). `forceState` (boolean) sets an explicit state;
1328
+ // omitting it toggles. Keeps the header's aria-expanded + chevron in sync.
1329
+ function _toggleSettingsFold(groupId, forceState) {
1330
+ const body = document.getElementById('modal-body');
1331
+ if (!body) return undefined;
1332
+ const fold = body.querySelector('[data-settings-fold="' + groupId + '"]');
1333
+ const group = body.querySelector('[data-settings-group="' + groupId + '"]');
1334
+ if (!fold || !group) return undefined;
1335
+ const open = typeof forceState === 'boolean' ? forceState : !group.classList.contains('open');
1336
+ group.classList.toggle('open', open);
1337
+ fold.classList.toggle('open', open);
1338
+ fold.setAttribute('aria-expanded', open ? 'true' : 'false');
1339
+ return open;
1260
1340
  }
1261
1341
 
1262
1342
  function _applySettingsSearch(rawQuery) {
@@ -1281,6 +1361,25 @@ function _applySettingsSearch(rawQuery) {
1281
1361
  const badge = btn.querySelector('.match-count');
1282
1362
  if (badge) badge.style.display = 'none';
1283
1363
  });
1364
+ // Restore each fold's resting state: persisted collapsed default, but kept
1365
+ // open if an active child tab lives inside it (so the visible pane's rail
1366
+ // button isn't hidden). Clears any search-driven fold badge/no-match.
1367
+ body.querySelectorAll('[data-settings-fold]').forEach(function(fold) {
1368
+ const gid = fold.getAttribute('data-settings-fold');
1369
+ const group = body.querySelector('[data-settings-group="' + gid + '"]');
1370
+ if (!group) return;
1371
+ fold.classList.remove('no-match');
1372
+ const foldBadge = fold.querySelector('.match-count');
1373
+ if (foldBadge) foldBadge.style.display = 'none';
1374
+ let open = false;
1375
+ if (gid === 'advanced') {
1376
+ try { open = localStorage.getItem('minions.settings.advancedFoldOpen') === '1'; } catch { /* ignore */ }
1377
+ }
1378
+ if (group.querySelector('[data-settings-tab].active')) open = true;
1379
+ group.classList.toggle('open', open);
1380
+ fold.classList.toggle('open', open);
1381
+ fold.setAttribute('aria-expanded', open ? 'true' : 'false');
1382
+ });
1284
1383
  return;
1285
1384
  }
1286
1385
 
@@ -1311,6 +1410,32 @@ function _applySettingsSearch(rawQuery) {
1311
1410
  }
1312
1411
  }
1313
1412
  });
1413
+
1414
+ // Fold handling during search: expand any fold that has a matching child so
1415
+ // results aren't hidden behind a collapsed header, aggregate the child match
1416
+ // counts onto the fold header badge, and dim folds with zero matches. Runs
1417
+ // AFTER the per-pane loop so child rail badges are already computed.
1418
+ body.querySelectorAll('[data-settings-fold]').forEach(function(fold) {
1419
+ const gid = fold.getAttribute('data-settings-fold');
1420
+ const group = body.querySelector('[data-settings-group="' + gid + '"]');
1421
+ if (!group) return;
1422
+ let total = 0;
1423
+ let anyMatch = false;
1424
+ group.querySelectorAll('[data-settings-tab]').forEach(function(cb) {
1425
+ if (!cb.classList.contains('no-match')) anyMatch = true;
1426
+ const badge = cb.querySelector('.match-count');
1427
+ if (badge && badge.style.display !== 'none') total += Number(badge.textContent) || 0;
1428
+ });
1429
+ group.classList.toggle('open', anyMatch);
1430
+ fold.classList.toggle('open', anyMatch);
1431
+ fold.setAttribute('aria-expanded', anyMatch ? 'true' : 'false');
1432
+ fold.classList.toggle('no-match', !anyMatch);
1433
+ const foldBadge = fold.querySelector('.match-count');
1434
+ if (foldBadge) {
1435
+ foldBadge.textContent = String(total);
1436
+ foldBadge.style.display = total > 0 ? 'inline-block' : 'none';
1437
+ }
1438
+ });
1314
1439
  }
1315
1440
 
1316
1441
  async function saveSettings() {
@@ -1682,7 +1807,7 @@ async function removeProject(name) {
1682
1807
  }
1683
1808
  }
1684
1809
 
1685
- window.MinionsSettings = { openSettings, saveSettings, addProject, removeProject, resetSettingsToDefaults, _selectSettingsTab, _applySettingsSearch };
1810
+ window.MinionsSettings = { openSettings, saveSettings, addProject, removeProject, resetSettingsToDefaults, _selectSettingsTab, _applySettingsSearch, _toggleSettingsFold };
1686
1811
 
1687
1812
  // ── Settings embed mode (Slim UX parity — W-mrkpke07000i8ed5) ───────────
1688
1813
  // The Slim UX Settings modal embeds this LITERAL classic Settings surface in
@@ -1308,6 +1308,19 @@
1308
1308
  .settings-rail-btn.active { color: var(--blue); border-left-color: var(--blue); background: var(--surface); font-weight: 600; }
1309
1309
  .settings-rail-btn.no-match { opacity: 0.3; }
1310
1310
  .settings-rail-btn .match-count { font-size: var(--text-xs); color: var(--blue); background: var(--bg); border-radius: var(--radius-xl); padding: 1px 6px; min-width: 18px; text-align: center; }
1311
+ /* W-mryz3r7h — collapsible rail fold ("Advanced"). The fold header is a real
1312
+ <button> (keyboard-operable); child rail buttons live in a container that
1313
+ is hidden unless the fold is .open. Children are indented under the header. */
1314
+ .settings-rail-fold { display: flex; align-items: center; gap: var(--space-2); width: 100%; padding: var(--space-3) var(--space-6); background: transparent; border: none; border-left: 3px solid transparent; color: var(--muted); font-size: var(--text-md); font-weight: 400; font-family: inherit; cursor: pointer; text-align: left; transition: all var(--transition-fast); }
1315
+ .settings-rail-fold:hover { color: var(--text); background: var(--surface); }
1316
+ .settings-rail-fold.no-match { opacity: 0.3; }
1317
+ .settings-rail-fold .rail-label { flex: 1; }
1318
+ .settings-rail-fold .match-count { font-size: var(--text-xs); color: var(--blue); background: var(--bg); border-radius: var(--radius-xl); padding: 1px 6px; min-width: 18px; text-align: center; }
1319
+ .settings-rail-fold .fold-chevron::before { content: '\25B8'; display: inline-block; font-size: var(--text-xs); color: var(--muted); transition: transform var(--transition-fast); }
1320
+ .settings-rail-fold.open .fold-chevron::before { transform: rotate(90deg); }
1321
+ .settings-rail-group { display: none; }
1322
+ .settings-rail-group.open { display: block; }
1323
+ .settings-rail-btn--child { padding-left: var(--space-9); }
1311
1324
  .settings-content { flex: 1; overflow-y: auto; padding: var(--space-7) var(--space-8); min-width: 0; }
1312
1325
  .settings-pane { display: none; }
1313
1326
  .settings-pane.active { display: block; }
@@ -77,6 +77,12 @@ Because that bypass can spend tens of seconds starting a one-off process before
77
77
 
78
78
  **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.
79
79
 
80
+ ## Operator-checkout write authority (interactive turns)
81
+
82
+ Interactive Command Center turns MAY use `Edit`/`Write`/`Bash` to change files and run mutating git commands (e.g. a clean `git pull --ff-only origin main`, a requested `git checkout -- <file>`, a one-off file edit) inside a configured project's `localPath` — but **only** to carry out a change the human explicitly asked to make in that checkout, acting as the operator's hands. The authoritative rule text lives in `prompts/cc-system.md` → "Operator checkouts — write only on an explicit user request" and is regression-tested in `test/unit.test.js` ("cc-system prompt permits explicit-request CC edits to operator checkouts with guardrails").
83
+
84
+ Guardrails preserved: no unsolicited writes (question/inspection-only asks stay read-only), requested scope only (never revert/stash/reset/overwrite the operator's unrelated uncommitted changes, no destructive commands like `git reset --hard` / `git clean -fd` unless explicitly named), and dispatch is still preferred for Medium/Large feature work that should land as a reviewed PR. This authority is **CC-only**: it does not change agent dispatch, which always runs in engine-owned isolated worktrees (`worktree` mode) or the guarded live-checkout path (`live`/`hybrid` mode). Worktree ownership, wipe guards, and live-checkout safety are unchanged.
85
+
80
86
  ## Create-PR flow — checkout-mode-aware (PR #387)
81
87
 
82
88
  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`).
@@ -229,7 +229,9 @@ 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
- 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.
232
+ The **"Create PR"** chip is only for pre-existing operator changes and never fabricates edits itself. 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
+
234
+ (Note: interactive Command Center turns may make direct, explicitly-requested edits to `localPath` — including a requested clean `git pull --ff-only origin main` — as the operator's hands; see `prompts/cc-system.md` → "Operator checkouts — write only on an explicit user request". That interactive authority is separate from this Create-PR chip flow and does not change the dispatch-path or agent-worktree ownership contracts on this page.)
233
235
 
234
236
  **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
237
 
@@ -15,8 +15,10 @@ Lifecycle keeps the cross-cutting invariants; the detail lives here.
15
15
 
16
16
  For `checkoutMode: "worktree"`, `project.localPath` is an operator-owned source
17
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
18
+ dispatches both run in engine-owned worktrees; interactive Command Center turns
19
+ may write to `localPath` only on an explicit user request (never as a dispatch or
20
+ background action — see `prompts/cc-system.md`); Create-PR staging copies existing
21
+ operator changes without resetting or deleting
20
22
  the source; restart restore requires explicit live dispatch provenance plus
21
23
  current live configuration. Branchless project dispatches receive detached
22
24
  worktrees, and path containment resolves symlinks/junctions before accepting a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2414",
3
+ "version": "0.1.2416",
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"
@@ -89,17 +89,19 @@ If you start a small task and discover it's actually Medium (3+ files, more tool
89
89
 
90
90
  When genuinely in doubt about the size, delegate — agents have isolated worktrees, full tool access, durable work-item tracking, and no turn limits.
91
91
 
92
- ### HARD STOPnever edit a configured project's operator checkout
93
- This overrides everything above, **including the direct-handling override**. For every configured project, you MUST NOT use `Edit`/`Write`/`Bash` (or any tool) to mutate files inside that project's `localPath`, **regardless of checkout mode or task size** not even a Small 1-line change, and not even when the human explicitly says to do it yourself.
92
+ ### Operator checkoutswrite only on an explicit user request
93
+ A configured project's `localPath` is the **human operator's own working tree**. You MAY use `Edit`/`Write`/`Bash` to change files and run mutating git commands inside `localPath` — but only as the operator's hands, carrying out a change **they explicitly asked you to make in that checkout**. This includes ordinary requested git operations (e.g. a clean `git pull --ff-only origin main`, `git checkout -- <file>` on a file they named) and requested file edits.
94
94
 
95
- Why: in `worktree` mode, `localPath` is operator-owned and every project-bound dispatch (including read-only work) gets an isolated worktree. In `live`/`hybrid` mode, only the engine-owned dispatch path may deliberately operate there. A direct CC edit bypasses both ownership models.
95
+ Guardrails (these still bind, regardless of checkout mode or task size):
96
+ - **Explicit request only — never unsolicited.** If the human only asked a question, asked you to inspect, or asked you to dispatch, stay read-only. Do not edit or run mutating commands in `localPath` on your own initiative. This is the one place the direct-handling override does **not** apply: absent an explicit ask to change that checkout, treat it as read-only.
97
+ - **Requested scope only — never their unrelated work.** Change exactly what was asked. Never revert, stash, reset, discard, or overwrite the operator's unrelated uncommitted changes, and never run destructive commands (`git reset --hard`, `git clean -fd`, `git checkout .`, branch/force-push/delete operations) unless the human explicitly named that exact destructive action.
98
+ - **Prefer dispatch for real feature work.** For Medium/Large coding work — new features, multi-file refactors, anything that should land as a reviewed PR — still **dispatch a work item** (`POST /api/work-items`, `type: "implement"` or `"fix"`) so the engine runs it in an isolated worktree with the normal review/build loop. Direct `localPath` writes are for the small, explicit "do this here, now" asks: a quick fix, a requested `git pull`, a one-off edit.
99
+ - **CC-only authority — dispatch isolation is unchanged.** This permission covers interactive Command Center turns only. It does **not** loosen how the engine dispatches agents: dispatched agents still run in engine-owned isolated worktrees (`worktree` mode) or the guarded live-checkout path (`live`/`hybrid` mode), and worktree ownership, wipe guards, and live-checkout safety are untouched. Do not hand-edit `localPath` to work around a dirty-tree or busy-dispatch gate — dispatch instead.
96
100
 
97
- Instead, **dispatch a normal work item** (`POST /api/work-items`, `type: "implement"` or `"fix"` as usual). The engine will choose an isolated worktree or the guarded live path from project configuration. Do not shortcut that path.
98
-
99
- Read-only inspection is still fine: viewing files and `git status`/`git log`/`git diff` are allowed. Do not run builds/tests in `localPath` when they may generate files; dispatch them instead.
101
+ Read-only inspection never needs a request: viewing files and `git status`/`git log`/`git diff` are always fine.
100
102
 
101
103
  ### Create a PR from pre-existing operator changes
102
- Use this only when the human asks to open a PR from changes that already exist in a configured worktree-mode project's operator checkout. CC must not create those changes itself.
104
+ Use this when the human asks to open a PR from changes that **already exist** in a configured worktree-mode project's operator checkout so the PR is built from a clean copy and the operator's original tree is left untouched.
103
105
 
104
106
  First call:
105
107
  ```
@@ -355,7 +357,7 @@ Every configured project has an effective **checkout mode** — surfaced in your
355
357
  - **`live`** — agents run **in-place** inside the project's `localPath` (no worktree). The engine caps this to **one mutating dispatch at a time** per project. Dirty-tree behavior follows the configured auto-stash/reset policy (auto-stash defaults on); with recovery disabled, the item remains pending until the tree is clean. Use only when worktrees are unworkable.
356
358
  - **`hybrid`** — `live` **plus** a `liveValidation: { type, autoDispatch }` block. Work items not listed in `type` use isolated worktrees; listed live-capable canonical work-item types run in-place. The normal validation type is `test`. `build-and-test` is a playbook name, not a work-item type. `type` accepts one string or an array. When `autoDispatch:true`, `test` must be included; the engine creates one PR-targeted `test` WI with the `build-and-test` playbook after each eligible coding WI completes.
357
359
 
358
- **CC never writes into any configured project's `localPath`.** Dispatch a work item and let the engine own either the isolated-worktree or guarded-live path. Read-only inspection remains fine.
360
+ **CC may write into a configured project's `localPath` only on an explicit user request** (see "Operator checkouts — write only on an explicit user request"). Dispatched agents never do — the engine owns their isolated-worktree or guarded-live path. Read-only inspection remains fine.
359
361
 
360
362
  **When to recommend hybrid:** the project's build/test/validation genuinely cannot run in an isolated worktree (it needs the real checkout — submodules, a `repo`-managed tree, an emulator/dev-server bound to `localPath`, deep-path tooling), **but** you still want coding agents to work in parallel rather than serialize through the single live checkout. If the *whole* workflow must run on the real tree, use plain `live`. If nothing needs the real tree, stay on `worktree`.
361
363