@yemi33/minions 0.1.2309 → 0.1.2310

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.
@@ -135,7 +135,8 @@ const RENDER_VERSIONS = {
135
135
  prdPrs: 1,
136
136
  inbox: 2,
137
137
  // Bumped 4→5 for the clickable checkout-mode pill + picker (W-mr1b67zi0006b788).
138
- projects: 5,
138
+ // Bumped 5→6 for multi-select hybrid liveValidation.type support (W-mr2m1ute000a9c01).
139
+ projects: 6,
139
140
  notes: 1,
140
141
  prd: 3,
141
142
  prs: 3,
@@ -108,9 +108,11 @@ function _renderWorktreeModePill(p) {
108
108
  const name = escHtml(p.name);
109
109
  const common = ' data-checkout-pill="' + name + '" role="button" tabindex="0" aria-haspopup="true" aria-label="Change checkout mode"';
110
110
  if (p.checkoutMode === 'live') {
111
- if (p.liveValidationType) {
112
- const vt = escapeHtml(p.liveValidationType);
113
- return ' <span class="project-mode-pill project-mode-hybrid project-mode-pill-clickable"' + common + ' title="Hybrid live-validation — coding work items author in isolated worktrees; only the &quot;' + vt + '&quot; validation type runs in-place on the live checkout (capped to one mutating dispatch, refused on a dirty tree). Click to change.">⚡ Hybrid · ' + vt + '</span>';
111
+ const types = _liveValidationTypesArray(p);
112
+ if (types.length > 0) {
113
+ const typesText = types.join(', ');
114
+ const vt = escapeHtml(typesText);
115
+ return ' <span class="project-mode-pill project-mode-hybrid project-mode-pill-clickable"' + common + ' title="Hybrid live-validation — coding work items author in isolated worktrees; only the &quot;' + vt + '&quot; validation type(s) run in-place on the live checkout (capped to one mutating dispatch, refused on a dirty tree). Click to change.">⚡ Hybrid · ' + vt + '</span>';
114
116
  }
115
117
  return ' <span class="project-mode-pill project-mode-live project-mode-pill-clickable"' + common + ' title="Live-checkout dispatch mode — agents run in-place inside the project working tree (no isolated worktree); capped to one mutating dispatch and refused on a dirty tree. Click to change.">⚡ Live checkout</span>';
116
118
  }
@@ -130,6 +132,16 @@ function _findProjectInLastStatus(name) {
130
132
  return projects.find(function(p) { return p.name === name; }) || null;
131
133
  }
132
134
 
135
+ // Normalize p.liveValidationType (single string legacy shape, OR array of
136
+ // strings — W-mr2m1ute000a9c01 multi-select) into an array of non-empty
137
+ // strings for uniform handling by menu/pill rendering code.
138
+ function _liveValidationTypesArray(p) {
139
+ const raw = p && p.liveValidationType;
140
+ if (!raw) return [];
141
+ const arr = Array.isArray(raw) ? raw : [raw];
142
+ return arr.filter(function(t) { return typeof t === 'string' && t.length > 0; });
143
+ }
144
+
133
145
  function _closeCheckoutModeMenu() {
134
146
  const existing = document.getElementById('checkout-mode-menu');
135
147
  if (existing) existing.remove();
@@ -183,8 +195,8 @@ function _openCheckoutModeMenu(projectName, anchorEl) {
183
195
  item.appendChild(desc);
184
196
  }
185
197
  const isActive = (mode === 'worktree' && project.checkoutMode !== 'live')
186
- || (mode === 'live' && project.checkoutMode === 'live' && !project.liveValidationType)
187
- || (mode === 'hybrid' && project.checkoutMode === 'live' && !!project.liveValidationType);
198
+ || (mode === 'live' && project.checkoutMode === 'live' && _liveValidationTypesArray(project).length === 0)
199
+ || (mode === 'hybrid' && project.checkoutMode === 'live' && _liveValidationTypesArray(project).length > 0);
188
200
  if (isActive) {
189
201
  item.setAttribute('aria-checked', 'true');
190
202
  item.classList.add('checkout-mode-menu-item-active');
@@ -221,9 +233,12 @@ function _openCheckoutModeMenu(projectName, anchorEl) {
221
233
  if (first) first.focus();
222
234
  }
223
235
 
224
- // Second step of the picker for hybrid mode: pick which work-item type runs
236
+ // Second step of the picker for hybrid mode: pick which work-item type(s) run
225
237
  // in-place on the live checkout while everything else authors in isolated
226
- // worktrees. Renders a <select> + Back/Apply into the existing menu element.
238
+ // worktrees. Renders a checkbox list (multi-select, W-mr2m1ute000a9c01) +
239
+ // Back/Apply into the existing menu element. A checkbox list (rather than a
240
+ // native <select multiple>) keeps the existing "click one row" affordance
241
+ // discoverable while allowing more than one type to be selected at once.
227
242
  function _renderCheckoutModeHybridStep(menu, projectName) {
228
243
  const project = _findProjectInLastStatus(projectName);
229
244
  while (menu.firstChild) menu.removeChild(menu.firstChild);
@@ -235,21 +250,31 @@ function _renderCheckoutModeHybridStep(menu, projectName) {
235
250
 
236
251
  const label = document.createElement('div');
237
252
  label.className = 'checkout-mode-menu-item';
238
- label.textContent = 'Work-item type to validate live:';
253
+ label.textContent = 'Work-item type(s) to validate live:';
239
254
  menu.appendChild(label);
240
255
 
241
- const select = document.createElement('select');
242
- select.className = 'checkout-mode-menu-select';
256
+ const currentTypes = _liveValidationTypesArray(project);
257
+ const preselect = currentTypes.length > 0 ? currentTypes : [CHECKOUT_MODE_HYBRID_DEFAULT_TYPE];
258
+
259
+ const checkboxList = document.createElement('div');
260
+ checkboxList.className = 'checkout-mode-menu-checkboxes';
261
+ const checkboxes = [];
243
262
  CHECKOUT_MODE_HYBRID_TYPES.forEach(function(t) {
244
- const opt = document.createElement('option');
245
- opt.value = t;
246
- opt.textContent = t;
247
- select.appendChild(opt);
263
+ const row = document.createElement('label');
264
+ row.className = 'checkout-mode-menu-checkbox-row';
265
+ const cb = document.createElement('input');
266
+ cb.type = 'checkbox';
267
+ cb.value = t;
268
+ cb.className = 'checkout-mode-menu-checkbox';
269
+ cb.checked = preselect.includes(t);
270
+ checkboxes.push(cb);
271
+ const text = document.createElement('span');
272
+ text.textContent = t;
273
+ row.appendChild(cb);
274
+ row.appendChild(text);
275
+ checkboxList.appendChild(row);
248
276
  });
249
- select.value = (project && project.liveValidationType && CHECKOUT_MODE_HYBRID_TYPES.includes(project.liveValidationType))
250
- ? project.liveValidationType
251
- : CHECKOUT_MODE_HYBRID_DEFAULT_TYPE;
252
- menu.appendChild(select);
277
+ menu.appendChild(checkboxList);
253
278
 
254
279
  const actions = document.createElement('div');
255
280
  actions.className = 'checkout-mode-menu-actions';
@@ -270,9 +295,10 @@ function _renderCheckoutModeHybridStep(menu, projectName) {
270
295
  applyBtn.className = 'checkout-mode-menu-btn checkout-mode-menu-btn-primary';
271
296
  applyBtn.textContent = 'Apply';
272
297
  applyBtn.addEventListener('click', function() {
273
- const type = select.value;
298
+ const types = checkboxes.filter(function(cb) { return cb.checked; }).map(function(cb) { return cb.value; });
299
+ if (types.length === 0) return; // require at least one selected type
274
300
  _closeCheckoutModeMenu();
275
- _applyCheckoutModeChange(projectName, 'live', type);
301
+ _applyCheckoutModeChange(projectName, 'live', types);
276
302
  });
277
303
  actions.appendChild(applyBtn);
278
304
 
@@ -284,12 +310,21 @@ function _renderCheckoutModeHybridStep(menu, projectName) {
284
310
  // call (mirrors projectChipRemove / removePinnedNote), then POSTs to
285
311
  // /api/settings. On failure, reverts the optimistic flip, re-renders, and
286
312
  // shows an error toast.
313
+ //
314
+ // `liveValidationType` accepts a single type string (legacy call sites) OR an
315
+ // array of type strings (W-mr2m1ute000a9c01 — multi-select hybrid types);
316
+ // whichever shape is passed through is what gets POSTed as
317
+ // liveValidation.type, so shared.validateLiveValidation on the server decides
318
+ // the final persisted shape.
287
319
  async function _applyCheckoutModeChange(projectName, newMode, liveValidationType) {
288
320
  const project = _findProjectInLastStatus(projectName);
289
321
  if (!project) return;
322
+ const typesForLabel = liveValidationType
323
+ ? (Array.isArray(liveValidationType) ? liveValidationType : [liveValidationType])
324
+ : [];
290
325
  const label = newMode === 'worktree'
291
326
  ? 'Worktrees'
292
- : (liveValidationType ? ('Hybrid (' + liveValidationType + ')') : 'Live checkout');
327
+ : (typesForLabel.length > 0 ? ('Hybrid (' + typesForLabel.join(', ') + ')') : 'Live checkout');
293
328
  const ok = await confirmDialog({
294
329
  title: 'Change checkout mode?',
295
330
  message: 'Switch "' + projectName + '" to ' + label + '? This changes how every future dispatch on this project runs.',
@@ -1330,11 +1330,17 @@
1330
1330
  font-size: var(--text-xs); color: var(--muted); font-weight: 400;
1331
1331
  margin-top: 2px; line-height: 1.35;
1332
1332
  }
1333
- .checkout-mode-menu-select {
1334
- width: 100%; margin: 4px 0 8px; padding: 4px 6px;
1335
- background: var(--surface); color: var(--text); border: 1px solid var(--border);
1336
- border-radius: 4px;
1333
+ .checkout-mode-menu-checkboxes {
1334
+ display: flex; flex-direction: column; gap: 2px;
1335
+ margin: 4px 0 8px; max-height: 220px; overflow-y: auto;
1337
1336
  }
1337
+ .checkout-mode-menu-checkbox-row {
1338
+ display: flex; align-items: center; gap: 6px;
1339
+ padding: 4px 6px; border-radius: 4px; cursor: pointer;
1340
+ color: var(--text); font-size: var(--text-sm);
1341
+ }
1342
+ .checkout-mode-menu-checkbox-row:hover { background: var(--surface); }
1343
+ .checkout-mode-menu-checkbox { cursor: pointer; }
1338
1344
  .checkout-mode-menu-actions {
1339
1345
  display: flex; justify-content: flex-end; gap: 6px; padding-top: 4px;
1340
1346
  }
package/dashboard.js CHANGED
@@ -4300,7 +4300,10 @@ function _resetPreambleCache() {
4300
4300
  function _projectCheckoutModeLabel(p) {
4301
4301
  const mode = shared.resolveCheckoutMode(p);
4302
4302
  if (mode === 'live' && p && p.liveValidation && p.liveValidation.type) {
4303
- return `hybrid (live-validation: ${p.liveValidation.type})`;
4303
+ // liveValidation.type may be a single string (legacy) or an array of
4304
+ // strings (W-mr2m1ute000a9c01 — multi-select hybrid types); render all.
4305
+ const types = Array.isArray(p.liveValidation.type) ? p.liveValidation.type : [p.liveValidation.type];
4306
+ return `hybrid (live-validation: ${types.join(', ')})`;
4304
4307
  }
4305
4308
  return mode;
4306
4309
  }
@@ -6503,9 +6503,20 @@ function collapseAllDuplicatePrRecords(config) {
6503
6503
 
6504
6504
  // M003 — After a coding WI completes successfully, auto-dispatch a live-validation
6505
6505
  // WI when project.liveValidation.autoDispatch === true and the completed item is
6506
- // a coding WI (not the validation type itself). Skips if the coding WI has no PR.
6507
- // Deduplicates: a non-terminal WI with meta.liveValidationFor === codingWiId
6508
- // blocks a second dispatch.
6506
+ // a coding WI (not one of the validation types itself). Skips if the coding WI
6507
+ // has no PR.
6508
+ //
6509
+ // liveValidation.type may be a single string (legacy / back-compat) or an
6510
+ // array of strings (W-mr2m1ute000a9c01 — multi-select hybrid types). When an
6511
+ // array, ONE validation WI is dispatched per configured type (each validation
6512
+ // WI still carries a single canonical `type` field so downstream routing —
6513
+ // playbooks, agent resolution, resolveCheckoutMode's membership check — keeps
6514
+ // working per-type).
6515
+ //
6516
+ // Deduplicates per (codingWiId, validationType) pair: a non-terminal WI with
6517
+ // meta.liveValidationFor === codingWiId AND meta.liveValidationType === type
6518
+ // blocks a second dispatch of that specific type, so multiple configured
6519
+ // types don't collide or skip each other.
6509
6520
  function autoDispatchLiveValidationWi(meta, config) {
6510
6521
  const item = meta?.item;
6511
6522
  if (!item?.id || !item?.type) return;
@@ -6521,8 +6532,12 @@ function autoDispatchLiveValidationWi(meta, config) {
6521
6532
  const lv = project.liveValidation;
6522
6533
  if (!lv || lv.autoDispatch !== true || !lv.type) return;
6523
6534
 
6524
- // Only auto-dispatch for coding WIs skip if this IS the validation type.
6525
- if (item.type === lv.type) return;
6535
+ const lvTypes = Array.isArray(lv.type) ? lv.type : [lv.type];
6536
+
6537
+ // Only auto-dispatch for coding WIs — skip types that ARE one of the
6538
+ // configured validation types (avoid an infinite validate-the-validation loop).
6539
+ const typesToDispatch = lvTypes.filter(t => t !== item.type);
6540
+ if (typesToDispatch.length === 0) return;
6526
6541
 
6527
6542
  // Resolve PR reference: prefer the canonical stamped _pr field (set by
6528
6543
  // stampWiPrRef which runs earlier in runPostCompletionHooks), then fall
@@ -6538,34 +6553,41 @@ function autoDispatchLiveValidationWi(meta, config) {
6538
6553
  mutateWorkItems(wiPath, items => {
6539
6554
  if (!Array.isArray(items)) return items;
6540
6555
 
6541
- // Dedup: skip if a non-terminal WI already tracks this coding WI.
6542
- const existing = items.find(i =>
6543
- i &&
6544
- i.meta &&
6545
- i.meta.liveValidationFor === codingWiId &&
6546
- !PLAN_TERMINAL_STATUSES.has(i.status)
6547
- );
6548
- if (existing) {
6549
- log('info', `liveValidation: dedup — non-terminal validation WI ${existing.id} already exists for ${codingWiId}`);
6550
- return items;
6551
- }
6556
+ for (const validationType of typesToDispatch) {
6557
+ // Dedup: skip if a non-terminal WI already tracks this (coding WI, type)
6558
+ // pair. Falls back to the WI's own `type` field when meta.liveValidationType
6559
+ // is absent (back-compat with validation WIs created before this field
6560
+ // existed, e.g. single-type configs from before W-mr2m1ute000a9c01).
6561
+ const existing = items.find(i =>
6562
+ i &&
6563
+ i.meta &&
6564
+ i.meta.liveValidationFor === codingWiId &&
6565
+ (i.meta.liveValidationType === validationType ||
6566
+ (!i.meta.liveValidationType && i.type === validationType)) &&
6567
+ !PLAN_TERMINAL_STATUSES.has(i.status)
6568
+ );
6569
+ if (existing) {
6570
+ log('info', `liveValidation: dedup — non-terminal validation WI ${existing.id} (type=${validationType}) already exists for ${codingWiId}`);
6571
+ continue;
6572
+ }
6552
6573
 
6553
- const validationWi = {
6554
- id: 'W-' + shared.uid(),
6555
- title: 'Validate: ' + (item.title || codingWiId),
6556
- type: lv.type,
6557
- status: WI_STATUS.PENDING,
6558
- depends_on: [codingWiId],
6559
- references: [prRef],
6560
- meta: { liveValidationFor: codingWiId },
6561
- project: projectName,
6562
- priority: item.priority || 'medium',
6563
- created: ts(),
6564
- createdBy: 'lifecycle:live-validation-auto-dispatch',
6565
- };
6574
+ const validationWi = {
6575
+ id: 'W-' + shared.uid(),
6576
+ title: 'Validate: ' + (item.title || codingWiId),
6577
+ type: validationType,
6578
+ status: WI_STATUS.PENDING,
6579
+ depends_on: [codingWiId],
6580
+ references: [prRef],
6581
+ meta: { liveValidationFor: codingWiId, liveValidationType: validationType },
6582
+ project: projectName,
6583
+ priority: item.priority || 'medium',
6584
+ created: ts(),
6585
+ createdBy: 'lifecycle:live-validation-auto-dispatch',
6586
+ };
6566
6587
 
6567
- items.push(validationWi);
6568
- log('info', `liveValidation: auto-dispatched validation WI ${validationWi.id} (type=${lv.type}) for coding WI ${codingWiId}`);
6588
+ items.push(validationWi);
6589
+ log('info', `liveValidation: auto-dispatched validation WI ${validationWi.id} (type=${validationType}) for coding WI ${codingWiId}`);
6590
+ }
6569
6591
  return items;
6570
6592
  });
6571
6593
  } catch (err) {
package/engine/shared.js CHANGED
@@ -2853,8 +2853,14 @@ function resolveCheckoutMode(project, workItemType) {
2853
2853
  }
2854
2854
  if (canonical === CHECKOUT_MODES.LIVE) {
2855
2855
  // Apply liveValidation routing when the block is present and workItemType is provided.
2856
+ // liveValidation.type may be a single string (legacy / back-compat) or an
2857
+ // array of strings (W-mr2m1ute000a9c01, multi-select hybrid types) —
2858
+ // normalize to an array and do a membership check so either shape works.
2856
2859
  if (project.liveValidation && workItemType !== undefined) {
2857
- return workItemType === project.liveValidation.type
2860
+ const lvTypes = Array.isArray(project.liveValidation.type)
2861
+ ? project.liveValidation.type
2862
+ : [project.liveValidation.type];
2863
+ return lvTypes.includes(workItemType)
2858
2864
  ? CHECKOUT_MODES.LIVE
2859
2865
  : CHECKOUT_MODES.WORKTREE;
2860
2866
  }
@@ -2910,13 +2916,26 @@ function validateCheckoutMode(value) {
2910
2916
  // Validate + normalize a per-project `liveValidation` block (hybrid mode).
2911
2917
  // Hybrid = checkoutMode:'live' + liveValidation:{ type, autoDispatch }: coding
2912
2918
  // work items author in isolated worktrees (escaping the live cap) while only
2913
- // work items whose type === liveValidation.type run in-place on the live
2919
+ // work items whose type is IN liveValidation.type run in-place on the live
2914
2920
  // checkout. See resolveCheckoutMode (which routes per work-item type) and
2915
2921
  // docs/live-checkout-mode.md.
2916
2922
  //
2923
+ // `type` may be a single work-item-type string (legacy / back-compat) OR a
2924
+ // non-empty array of non-empty-string work-item types
2925
+ // (W-mr2m1ute000a9c01 — multi-select hybrid types, e.g. both "implement" and
2926
+ // "fix" validate live while other types stay in worktrees). Every reader of
2927
+ // liveValidation.type (resolveCheckoutMode, lifecycle.autoDispatchLiveValidationWi,
2928
+ // dashboard project mapper / pill / CC label) must handle BOTH shapes —
2929
+ // existing config.json entries with a plain string `type` continue to work
2930
+ // unchanged, no migration required.
2931
+ //
2917
2932
  // Returns:
2918
2933
  // - undefined → caller should clear the field (empty / null / '' input)
2919
- // - { type, autoDispatch? } normalized object on success
2934
+ // - { type, autoDispatch? } normalized object on success, where `type` is
2935
+ // whatever shape the caller passed in (string stays a string; array stays
2936
+ // a deduped array) — we intentionally do NOT force single-string input
2937
+ // into an array so already-persisted single-type configs round-trip
2938
+ // byte-identical through a Settings save that doesn't touch this field.
2920
2939
  // Throws HTTP 400 (via _httpError) on malformed input, or when the effective
2921
2940
  // checkout mode is not 'live' (liveValidation is meaningless without it — it is
2922
2941
  // silently ignored at resolve time, so we refuse to persist a misconfiguration).
@@ -2933,15 +2952,33 @@ function validateLiveValidation(value, opts) {
2933
2952
  if (checkoutMode !== CHECKOUT_MODES.LIVE) {
2934
2953
  throw _httpError(400, 'liveValidation requires checkoutMode "live" (hybrid mode). Set checkoutMode to "live" first, or clear liveValidation.');
2935
2954
  }
2936
- const type = typeof value.type === 'string' ? value.type.trim() : '';
2937
- if (!type) {
2938
- throw _httpError(400, 'Invalid liveValidation: "type" is required and must be a non-empty string — the work-item type that runs on the live checkout (e.g. "build-and-test").');
2955
+ const typeErrorMsg = 'Invalid liveValidation: "type" is required and must be a non-empty string, or a non-empty array of non-empty-string work-item types — the work-item type(s) that run on the live checkout (e.g. "build-and-test" or ["implement", "fix"]).';
2956
+ const rawType = value.type;
2957
+ let out;
2958
+ if (typeof rawType === 'string') {
2959
+ const trimmed = rawType.trim();
2960
+ if (!trimmed) throw _httpError(400, typeErrorMsg);
2961
+ out = { type: trimmed };
2962
+ } else if (Array.isArray(rawType)) {
2963
+ if (!rawType.every(t => typeof t === 'string' && t.trim().length > 0)) {
2964
+ throw _httpError(400, typeErrorMsg);
2965
+ }
2966
+ // Dedupe (preserving first-seen order) and cap at the number of distinct
2967
+ // work-item types the engine recognizes — a longer array can only be
2968
+ // full of duplicates or invented types.
2969
+ const deduped = [...new Set(rawType.map(t => t.trim()))];
2970
+ if (deduped.length === 0) throw _httpError(400, typeErrorMsg);
2971
+ if (deduped.length > VALID_WORK_TYPES.size) {
2972
+ throw _httpError(400, `Invalid liveValidation: "type" array has ${deduped.length} entries, more than the ${VALID_WORK_TYPES.size} distinct work-item types the engine recognizes — remove duplicates or invalid types.`);
2973
+ }
2974
+ out = { type: deduped };
2975
+ } else {
2976
+ throw _httpError(400, typeErrorMsg);
2939
2977
  }
2940
- const out = { type };
2941
2978
  // autoDispatch (optional): when true, lifecycle auto-creates a validation WI
2942
- // of `type` after each coding WI completes with a PR (engine/lifecycle.js
2943
- // autoDispatchLiveValidationWi). Coerce to a strict boolean; absent leaves it
2944
- // off so the operator opts in explicitly.
2979
+ // per configured `type` after each coding WI completes with a PR
2980
+ // (engine/lifecycle.js autoDispatchLiveValidationWi). Coerce to a strict
2981
+ // boolean; absent leaves it off so the operator opts in explicitly.
2945
2982
  if (Object.prototype.hasOwnProperty.call(value, 'autoDispatch')) {
2946
2983
  out.autoDispatch = !!value.autoDispatch;
2947
2984
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2309",
3
+ "version": "0.1.2310",
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"
@@ -325,7 +325,7 @@ Every configured project has an effective **checkout mode** — surfaced in your
325
325
 
326
326
  - **`worktree`** (default) — each dispatch gets its own isolated `git worktree`. Agents run fully in parallel; nothing touches the operator's working tree. Use for normal repos.
327
327
  - **`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 and **refuses on a dirty tree**. Use only when worktrees are unworkable (e.g. Android `repo`, submodules, deep Windows paths, emulators that bind the real checkout).
328
- - **`hybrid`** — `live` **plus** a `liveValidation: { type, autoDispatch }` block. Coding work items (`implement`/`fix`/`docs`/`decompose`) author in **isolated worktrees** (full parallelism), while **only** work items whose type matches `liveValidation.type` (e.g. `build-and-test`) run **in-place on the live checkout**. This is the best of both: parallel code authoring + a real on-disk build/validation that can't run in a worktree.
328
+ - **`hybrid`** — `live` **plus** a `liveValidation: { type, autoDispatch }` block. Coding work items (`implement`/`fix`/`docs`/`decompose`) author in **isolated worktrees** (full parallelism), while **only** work items whose type matches `liveValidation.type` (e.g. `build-and-test`) run **in-place on the live checkout**. This is the best of both: parallel code authoring + a real on-disk build/validation that can't run in a worktree. `type` accepts a single string (e.g. `"build-and-test"`) or an array of strings (e.g. `["implement","fix"]`) to route multiple work-item types to the live checkout at once.
329
329
 
330
330
  **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`.
331
331