@yemi33/minions 0.1.2379 → 0.1.2381

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.
Files changed (100) hide show
  1. package/bin/minions.js +19 -9
  2. package/dashboard/js/refresh.js +3 -4
  3. package/dashboard/js/render-other.js +43 -23
  4. package/dashboard/js/render-prd.js +1 -1
  5. package/dashboard/js/render-work-items.js +13 -6
  6. package/dashboard/js/settings.js +40 -17
  7. package/dashboard.js +435 -768
  8. package/docs/architecture-review-2026-07-09.md +2 -4
  9. package/docs/auto-discovery.md +36 -49
  10. package/docs/blog-first-successful-dispatch.md +1 -1
  11. package/docs/branch-derivation.md +2 -2
  12. package/docs/command-center.md +1 -1
  13. package/docs/completion-reports.md +14 -4
  14. package/docs/constellation-bridge.md +59 -10
  15. package/docs/constellation-style-telemetry.md +6 -6
  16. package/docs/cooldown-merge-semantics.md +4 -0
  17. package/docs/copilot-cli-schema.md +3 -3
  18. package/docs/cross-repo-plans.md +17 -17
  19. package/docs/deprecated.json +2 -2
  20. package/docs/design-state-storage.md +1 -1
  21. package/docs/documentation-audit-2026-07-09.md +2 -2
  22. package/docs/engine-restart.md +20 -8
  23. package/docs/harness-mode.md +1 -1
  24. package/docs/live-checkout-mode.md +45 -26
  25. package/docs/managed-spawn.md +4 -4
  26. package/docs/onboarding.md +1 -2
  27. package/docs/pr-comment-followup.md +3 -3
  28. package/docs/pr-review-fix-loop.md +1 -1
  29. package/docs/proposals/repo-pool-for-live-checkout.md +1 -2
  30. package/docs/qa-runbook-lifecycle.md +4 -4
  31. package/docs/qa-runbooks.md +2 -2
  32. package/docs/rfc-completion-json.md +4 -1
  33. package/docs/runtime-adapters.md +1 -1
  34. package/docs/self-improvement.md +4 -5
  35. package/docs/shared-lifecycle-module-map.md +3 -1
  36. package/docs/slim-ux/architecture-suggestions.md +5 -6
  37. package/docs/slim-ux/concepts.md +23 -25
  38. package/docs/watches.md +7 -7
  39. package/docs/workspace-manifests.md +1 -1
  40. package/docs/worktree-lifecycle.md +1 -1
  41. package/engine/abandoned-pr-reconciliation.js +4 -5
  42. package/engine/ado-status.js +5 -5
  43. package/engine/ado.js +20 -25
  44. package/engine/agent-worker-pool.js +58 -1
  45. package/engine/bridge.js +260 -5
  46. package/engine/cleanup.js +48 -131
  47. package/engine/cli.js +125 -83
  48. package/engine/cooldown.js +9 -16
  49. package/engine/db/index.js +22 -9
  50. package/engine/db/migrations/009-qa.js +1 -1
  51. package/engine/db/migrations/020-qa-session-scopes.js +23 -0
  52. package/engine/db/migrations/021-archived-work-items.js +72 -0
  53. package/engine/db/migrations/022-global-cc-session.js +31 -0
  54. package/engine/db/migrations/023-engine-state.js +30 -0
  55. package/engine/db/migrations/024-prd-ghost-collisions.js +53 -0
  56. package/engine/db/migrations/025-malformed-work-item-phantoms.js +33 -0
  57. package/engine/dispatch-store.js +2 -7
  58. package/engine/dispatch.js +39 -44
  59. package/engine/github.js +20 -27
  60. package/engine/lifecycle.js +279 -354
  61. package/engine/live-checkout.js +193 -149
  62. package/engine/llm.js +1 -1
  63. package/engine/logs-store.js +2 -2
  64. package/engine/managed-spawn.js +2 -23
  65. package/engine/meeting.js +6 -6
  66. package/engine/metrics-store.js +2 -2
  67. package/engine/note-link-backfill.js +6 -11
  68. package/engine/pipeline.js +18 -36
  69. package/engine/playbook.js +13 -16
  70. package/engine/prd-store.js +73 -54
  71. package/engine/preflight.js +2 -5
  72. package/engine/projects.js +15 -62
  73. package/engine/pull-requests-store.js +0 -17
  74. package/engine/qa-runbooks.js +2 -2
  75. package/engine/qa-runs.js +1 -8
  76. package/engine/qa-sessions.js +41 -64
  77. package/engine/queries.js +120 -219
  78. package/engine/routing.js +4 -6
  79. package/engine/scheduler.js +0 -4
  80. package/engine/shared-branch-pr-reconcile.js +2 -3
  81. package/engine/shared.js +268 -699
  82. package/engine/small-state-store.js +89 -10
  83. package/engine/state-operations.js +16 -4
  84. package/engine/stdio-timestamps.js +1 -1
  85. package/engine/timeout.js +5 -12
  86. package/engine/watch-actions.js +20 -22
  87. package/engine/watches-store.js +1 -1
  88. package/engine/watches.js +6 -10
  89. package/engine/work-item-validation.js +52 -0
  90. package/engine/work-items-store.js +127 -29
  91. package/engine/worktree-gc.js +2 -2
  92. package/engine/worktree-pool.js +8 -18
  93. package/engine.js +197 -358
  94. package/minions.js +2 -2
  95. package/package.json +1 -1
  96. package/playbooks/plan-to-prd.md +2 -2
  97. package/playbooks/shared-rules.md +3 -3
  98. package/playbooks/templates/followup-dispatch.md +1 -1
  99. package/playbooks/verify.md +1 -1
  100. package/prompts/cc-system.md +9 -9
package/bin/minions.js CHANGED
@@ -1404,9 +1404,21 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
1404
1404
  // PowerShell is unavailable, the engine is hung, or its cmdline doesn't match.
1405
1405
  const oldEnginePid = readEnginePid(MINIONS_HOME);
1406
1406
  try { execSync(`node "${path.join(MINIONS_HOME, 'engine.js')}" stop`, { stdio: 'ignore', cwd: MINIONS_HOME, timeout: 10000, windowsHide: true }); } catch {}
1407
- // Force-kill the recorded engine PID (NOT the tree agent children must
1408
- // survive so the new engine can re-attach them via PID files).
1409
- killPidOnly(oldEnginePid);
1407
+ // Give the engine's graceful handler time to drain non-reattachable pooled
1408
+ // leases. Detached cold agents are not part of that drain and remain alive
1409
+ // for normal PID-file reattachment by the replacement engine.
1410
+ const shutdownTimeout = (() => {
1411
+ try {
1412
+ const cfg = JSON.parse(fs.readFileSync(path.join(MINIONS_HOME, 'config.json'), 'utf8'));
1413
+ return Number(cfg?.engine?.shutdownTimeout) || shared.ENGINE_DEFAULTS.shutdownTimeout;
1414
+ } catch { return shared.ENGINE_DEFAULTS.shutdownTimeout; }
1415
+ })();
1416
+ const gracefulEngineStop = waitForPidDeath(oldEnginePid, shutdownTimeout + 5000);
1417
+ if (!gracefulEngineStop.ok) {
1418
+ // Fallback for a hung engine. Kill only the engine PID, never its detached
1419
+ // cold-agent children; pooled leases have already exceeded their drain budget.
1420
+ killPidOnly(oldEnginePid);
1421
+ }
1410
1422
  killByPort(restartResolved.port);
1411
1423
  killMinionsProcesses(['engine.js', 'dashboard.js', 'supervisor.js']);
1412
1424
  // Confirm the OS finished the asynchronous termination before we spawn new
@@ -1491,15 +1503,13 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
1491
1503
  const engineDir = path.join(MINIONS_HOME, 'engine');
1492
1504
 
1493
1505
  // Engine state
1494
- for (const f of ['dispatch.json', 'control.json', 'log.json', 'metrics.json', 'cooldowns.json', 'schedule-runs.json', 'kb-checkpoint.json', 'cc-session.json', 'doc-sessions.json', 'pipeline-runs.json']) rm(path.join(engineDir, f));
1506
+ require(path.join(PKG_ROOT, 'engine', 'db')).cleanupRetiredJsonFiles(MINIONS_HOME);
1507
+ for (const f of ['state.db', 'state.db-wal', 'state.db-shm', 'control.json', 'kb-checkpoint.json']) {
1508
+ rm(path.join(engineDir, f));
1509
+ }
1495
1510
  glob(engineDir, /^pid-.*\.pid$/).forEach(rm);
1496
1511
  rmDir(path.join(engineDir, 'tmp'));
1497
1512
 
1498
- // Work items + PRs
1499
- rm(path.join(MINIONS_HOME, 'work-items.json'));
1500
- rm(path.join(MINIONS_HOME, 'work-items-archive.json'));
1501
- rm(path.join(MINIONS_HOME, 'pull-requests.json'));
1502
-
1503
1513
  // Plans + PRDs + Pipelines + Meetings
1504
1514
  rmDir(path.join(MINIONS_HOME, 'plans'));
1505
1515
  rmDir(path.join(MINIONS_HOME, 'prd'));
@@ -138,7 +138,7 @@ const RENDER_VERSIONS = {
138
138
  // Bumped 4→5 for the clickable checkout-mode pill + picker (W-mr1b67zi0006b788).
139
139
  // Bumped 5→6 for multi-select hybrid liveValidation.type support (W-mr2m1ute000a9c01).
140
140
  // Bumped 6→7 for the liveValidation.autoDispatch toggle + pill "· auto-validate" suffix (W-mr2q361a00097e5c).
141
- projects: 7,
141
+ projects: 9,
142
142
  notes: 1,
143
143
  prd: 3,
144
144
  prs: 3,
@@ -151,9 +151,8 @@ const RENDER_VERSIONS = {
151
151
  dispatch: 2,
152
152
  engineLog: 2,
153
153
  metrics: 1,
154
- // Bumped 7→8: WI detail modal's branch pill moved out of the Artifacts
155
- // chip row into its own non-interactive "Branch" field (W-mr2qjr3b0002522f).
156
- workItems: 8,
154
+ // Bumped 8->9: row actions now preserve work-item source scope when IDs collide.
155
+ workItems: 9,
157
156
  skills: 1,
158
157
  commands: 1,
159
158
  mcpServers: 1,
@@ -105,13 +105,13 @@ function _renderProjectBranch(p) {
105
105
  // "Live checkout" (live — agents run in-place inside the operator's working
106
106
  // tree). Driven by p.checkoutMode (shared.CHECKOUT_MODES; engine defaults
107
107
  // unset → 'worktree'). Live is the riskier/special mode (capped to one mutating
108
- // dispatch per project, refuses on a dirty tree) so it gets a more prominent
109
- // color than the muted worktree pill.
108
+ // dispatch per project, with configurable dirty-tree recovery) so it gets a
109
+ // more prominent color than the muted worktree pill.
110
110
  //
111
111
  // Hybrid live-validation: when checkoutMode is 'live' AND p.liveValidationType
112
- // is set, coding WIs author in isolated worktrees and only the named validation
113
- // type runs in-place on the live checkout. This is a materially different
114
- // dispatch profile from full-live, so it gets its own pill.
112
+ // is set, only selected WI types run in-place and all others use isolated
113
+ // worktrees. This is a materially different dispatch profile from full-live,
114
+ // so it gets its own pill.
115
115
  // W-mr1b67zi0006b788: the pill is clickable — opens an inline picker (see
116
116
  // _openCheckoutModeMenu below) to switch between Worktrees / Live checkout /
117
117
  // Hybrid dispatch modes without leaving the Projects view. Shared
@@ -125,6 +125,10 @@ function _renderWorktreeModePill(p) {
125
125
  const common = ' data-checkout-pill="' + name + '" role="button" tabindex="0" aria-haspopup="true" aria-label="Change checkout mode"';
126
126
  if (p.checkoutMode === 'live') {
127
127
  const types = _liveValidationTypesArray(p);
128
+ if (p.liveValidationInvalid) {
129
+ const effectiveTypes = types.length > 0 ? types.join(', ') : 'none';
130
+ return ' <span class="project-mode-pill project-mode-hybrid project-mode-pill-clickable project-warn"' + common + ' title="Invalid hybrid configuration — unsupported settings are ignored. Effective live work-item types: ' + escHtml(effectiveTypes) + '. Click to repair.">⚠ Hybrid · invalid</span>';
131
+ }
128
132
  if (types.length > 0) {
129
133
  const typesText = types.join(', ');
130
134
  const vt = escapeHtml(typesText);
@@ -134,20 +138,19 @@ function _renderWorktreeModePill(p) {
134
138
  const autoDispatch = p.liveValidationAutoDispatch === true;
135
139
  const autoSuffix = autoDispatch ? ' · auto-validate' : '';
136
140
  const autoTitleBit = autoDispatch
137
- ? ' A validation work item is automatically dispatched after each coding work item completes.'
138
- : ' Auto-validation is OFF — validation must be dispatched manually after each coding work item completes.';
139
- return ' <span class="project-mode-pill project-mode-hybrid project-mode-pill-clickable"' + common + ' title="Hybrid live-validationcoding 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).' + escHtml(autoTitleBit) + ' Click to change.">⚡ Hybrid · ' + vt + escHtml(autoSuffix) + '</span>';
141
+ ? ' A test work item is automatically dispatched after each eligible coding work item completes.'
142
+ : ' Auto-validation is OFF — validation must be dispatched manually.';
143
+ return ' <span class="project-mode-pill project-mode-hybrid project-mode-pill-clickable"' + common + ' title="Hybrid dispatch — only the &quot;' + vt + '&quot; work-item type(s) run in-place; all others use isolated worktrees. Live work is capped to one mutating dispatch, and dirty-tree handling follows the configured auto-stash/reset policy.' + escHtml(autoTitleBit) + ' Click to change.">⚡ Hybrid · ' + vt + escHtml(autoSuffix) + '</span>';
140
144
  }
141
- 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>';
145
+ 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. Dirty-tree handling follows the configured auto-stash/reset policy. Click to change.">⚡ Live checkout</span>';
142
146
  }
143
147
  return ' <span class="project-mode-pill project-mode-isolated project-mode-pill-clickable"' + common + ' title="Worktree dispatch mode (default) — each agent runs in its own git worktree. Click to change.">Worktrees</span>';
144
148
  }
145
149
 
146
- // Work-item types eligible for hybrid live-validation (W-mr1b67zi0006b788).
147
- // Mirrors the set the server documents for project.liveValidation.type;
148
- // intentionally excludes the illustrative "build-and-test" example from the
149
- // server comments since that isn't a real WORK_TYPE value.
150
- const CHECKOUT_MODE_HYBRID_TYPES = ['implement', 'fix', 'docs', 'decompose', 'explore', 'ask', 'review', 'test', 'verify', 'setup'];
150
+ // Canonical work-item types that can meaningfully use the live checkout.
151
+ // Read-only root tasks are omitted because hybrid routing cannot change where
152
+ // they run. `build-and-test` is a playbook alias; `test` is the WI type.
153
+ const CHECKOUT_MODE_HYBRID_TYPES = ['test', 'implement', 'implement:large', 'fix', 'build-fix-complex', 'docs', 'decompose', 'review', 'verify', 'setup'];
151
154
  const CHECKOUT_MODE_HYBRID_DEFAULT_TYPE = 'test';
152
155
 
153
156
  function _findProjectInLastStatus(name) {
@@ -230,9 +233,10 @@ function _openCheckoutModeMenu(projectName, anchorEl) {
230
233
  desc.textContent = descText;
231
234
  item.appendChild(desc);
232
235
  }
236
+ const liveTypes = _liveValidationTypesArray(project);
233
237
  const isActive = (mode === 'worktree' && project.checkoutMode !== 'live')
234
- || (mode === 'live' && project.checkoutMode === 'live' && _liveValidationTypesArray(project).length === 0)
235
- || (mode === 'hybrid' && project.checkoutMode === 'live' && _liveValidationTypesArray(project).length > 0);
238
+ || (mode === 'live' && project.checkoutMode === 'live' && liveTypes.length === 0 && !project.liveValidationInvalid)
239
+ || (mode === 'hybrid' && project.checkoutMode === 'live' && (liveTypes.length > 0 || project.liveValidationInvalid));
236
240
  if (isActive) {
237
241
  item.setAttribute('aria-checked', 'true');
238
242
  item.classList.add('checkout-mode-menu-item-active');
@@ -254,8 +258,8 @@ function _openCheckoutModeMenu(projectName, anchorEl) {
254
258
  }
255
259
 
256
260
  addItem('Worktrees (default)', 'worktree', 'Each dispatch runs in its own isolated git worktree. Fully parallel, never touches your working directory.');
257
- addItem('Live checkout', 'live', 'Agents run in-place in the project folder. One mutating dispatch at a time; refuses to run on a dirty tree.');
258
- addItem('Hybrid dispatch…', 'hybrid', 'Coding work authors in isolated worktrees; only a chosen validation step (e.g. build-and-test) runs on the real checkout.');
261
+ addItem('Live checkout', 'live', 'Agents run in-place in the project folder. One mutating dispatch at a time; dirty-tree handling follows the auto-stash/reset policy.');
262
+ addItem('Hybrid dispatch…', 'hybrid', 'Only chosen work-item types (normally test) use the real checkout; all others use isolated worktrees.');
259
263
 
260
264
  document.body.appendChild(menu);
261
265
  const rect = anchorEl.getBoundingClientRect();
@@ -286,7 +290,7 @@ function _renderCheckoutModeHybridStep(menu, projectName) {
286
290
 
287
291
  const label = document.createElement('div');
288
292
  label.className = 'checkout-mode-menu-item';
289
- label.textContent = 'Work-item type(s) to validate live:';
293
+ label.textContent = 'Work-item type(s) to run live:';
290
294
  menu.appendChild(label);
291
295
 
292
296
  const currentTypes = _liveValidationTypesArray(project);
@@ -325,10 +329,23 @@ function _renderCheckoutModeHybridStep(menu, projectName) {
325
329
  autoDispatchCb.className = 'checkout-mode-menu-checkbox checkout-mode-menu-autodispatch-checkbox';
326
330
  autoDispatchCb.checked = project.liveValidationAutoDispatch === true;
327
331
  const autoDispatchText = document.createElement('span');
328
- autoDispatchText.textContent = 'Automatically validate after each coding work item completes';
332
+ autoDispatchText.textContent = 'Automatically run a test validation after each eligible coding work item completes';
329
333
  autoDispatchRow.appendChild(autoDispatchCb);
330
334
  autoDispatchRow.appendChild(autoDispatchText);
331
335
  menu.appendChild(autoDispatchRow);
336
+ const testTypeCheckbox = checkboxes.find(function(cb) {
337
+ return cb.value === CHECKOUT_MODE_HYBRID_DEFAULT_TYPE;
338
+ });
339
+ function syncAutoDispatchType() {
340
+ if (autoDispatchCb.checked && testTypeCheckbox) testTypeCheckbox.checked = true;
341
+ }
342
+ autoDispatchCb.addEventListener('change', syncAutoDispatchType);
343
+ if (testTypeCheckbox) {
344
+ testTypeCheckbox.addEventListener('change', function() {
345
+ if (!testTypeCheckbox.checked) autoDispatchCb.checked = false;
346
+ });
347
+ }
348
+ syncAutoDispatchType();
332
349
 
333
350
  const actions = document.createElement('div');
334
351
  actions.className = 'checkout-mode-menu-actions';
@@ -349,9 +366,9 @@ function _renderCheckoutModeHybridStep(menu, projectName) {
349
366
  applyBtn.className = 'checkout-mode-menu-btn checkout-mode-menu-btn-primary';
350
367
  applyBtn.textContent = 'Apply';
351
368
  applyBtn.addEventListener('click', function() {
369
+ const autoDispatch = autoDispatchCb.checked;
352
370
  const types = checkboxes.filter(function(cb) { return cb.checked; }).map(function(cb) { return cb.value; });
353
371
  if (types.length === 0) return; // require at least one selected type
354
- const autoDispatch = autoDispatchCb.checked;
355
372
  _closeCheckoutModeMenu();
356
373
  _applyCheckoutModeChange(projectName, 'live', types, autoDispatch);
357
374
  });
@@ -392,8 +409,8 @@ async function _applyCheckoutModeChange(projectName, newMode, liveValidationType
392
409
  // explicitly in the confirm message rather than burying it in the label.
393
410
  const autoDispatchNote = typesForLabel.length > 0
394
411
  ? (autoDispatchOn
395
- ? ' Auto-validation will be turned ON — a validation work item will be dispatched automatically after each coding work item completes, and coding agents will skip local builds/tests.'
396
- : ' Auto-validation will be OFF — you will need to dispatch validation manually after each coding work item completes.')
412
+ ? ' Auto-validation will be turned ON — a test work item will be dispatched automatically after each eligible coding work item completes, and eligible coding agents will skip local builds/tests.'
413
+ : ' Auto-validation will be OFF — you will need to dispatch validation manually.')
397
414
  : '';
398
415
  const ok = await confirmDialog({
399
416
  title: 'Change checkout mode?',
@@ -407,12 +424,14 @@ async function _applyCheckoutModeChange(projectName, newMode, liveValidationType
407
424
  const prevMode = project.checkoutMode;
408
425
  const prevType = project.liveValidationType;
409
426
  const prevAutoDispatch = project.liveValidationAutoDispatch;
427
+ const prevInvalid = project.liveValidationInvalid;
410
428
  const nextType = (newMode === 'live' && liveValidationType) ? liveValidationType : null;
411
429
  // Optimistic flip BEFORE awaiting the API call (per dashboard convention —
412
430
  // see projectChipRemove / removePinnedNote); reverted in the catch below.
413
431
  project.checkoutMode = newMode;
414
432
  project.liveValidationType = nextType;
415
433
  project.liveValidationAutoDispatch = nextType ? autoDispatchOn : false;
434
+ project.liveValidationInvalid = false;
416
435
  if (window._lastStatus && Array.isArray(window._lastStatus.projects)) renderProjects(window._lastStatus.projects);
417
436
 
418
437
  try {
@@ -434,6 +453,7 @@ async function _applyCheckoutModeChange(projectName, newMode, liveValidationType
434
453
  project.checkoutMode = prevMode;
435
454
  project.liveValidationType = prevType;
436
455
  project.liveValidationAutoDispatch = prevAutoDispatch;
456
+ project.liveValidationInvalid = prevInvalid;
437
457
  if (window._lastStatus && Array.isArray(window._lastStatus.projects)) renderProjects(window._lastStatus.projects);
438
458
  showToast('cmd-toast', 'Failed to update checkout mode: ' + (err && err.message || err), false);
439
459
  }
@@ -855,7 +855,7 @@ async function prdItemEdit(source, itemId) {
855
855
 
856
856
  completionHtml = '<div style="background:var(--surface);border:1px solid var(--border);border-left:3px solid ' + statusColor + ';border-radius:4px;padding:10px 12px;margin-bottom:12px">' +
857
857
  '<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">' +
858
- (wi ? '<a href="#" onclick="event.preventDefault();event.stopPropagation();openWorkItemDetail(\'' + escHtml(wi.id) + '\')" title="Open ' + escHtml(wi.id) + ' detail" style="font-size:var(--text-base);font-weight:700;color:' + statusColor + ';text-decoration:none;cursor:pointer" onmouseover="this.style.textDecoration=\'underline\'" onmouseout="this.style.textDecoration=\'none\'">' + statusLabel + ' &rarr;</a>' : '<span style="font-size:var(--text-base);font-weight:700;color:' + statusColor + '">' + statusLabel + '</span>') +
858
+ (wi ? '<a href="#" onclick="event.preventDefault();event.stopPropagation();openWorkItemDetail(\'' + escHtml(wi.id) + '\',\'' + escHtml(wi._source || '') + '\')" title="Open ' + escHtml(wi.id) + ' detail" style="font-size:var(--text-base);font-weight:700;color:' + statusColor + ';text-decoration:none;cursor:pointer" onmouseover="this.style.textDecoration=\'underline\'" onmouseout="this.style.textDecoration=\'none\'">' + statusLabel + ' &rarr;</a>' : '<span style="font-size:var(--text-base);font-weight:700;color:' + statusColor + '">' + statusLabel + '</span>') +
859
859
  typePillHtml +
860
860
  (agent ? '<span style="font-size:var(--text-base);color:var(--muted)">by ' + escHtml(agent) + '</span>' : '') +
861
861
  (completedAt ? '<span style="font-size:var(--text-sm);color:var(--muted)">' + escHtml(completedAt.slice(0, 16).replace('T', ' ')) + '</span>' : '') +
@@ -192,7 +192,7 @@ function wiRow(item) {
192
192
  var prLabel = prNumMatch ? ('PR #' + prNumMatch[1]) : 'parent PR';
193
193
  followupChip = ' <a class="pr-badge draft" style="font-size:var(--text-xs);text-decoration:none" target="_blank" rel="noopener" href="' + escapeHtml(prFollowup.parent_pr_url) + '" title="Follow-up dispatched from ' + escapeHtml(prRef) + (prFollowup.parent_comment_author ? ' by ' + escapeHtml(prFollowup.parent_comment_author) : '') + '" onclick="event.stopPropagation()">&#8617; from ' + escapeHtml(prLabel) + '</a>';
194
194
  }
195
- return '<tr data-wi-id="' + escapeHtml(item.id) + '" style="cursor:pointer" onclick="if(shouldIgnoreSelectionClick(event))return;openWorkItemDetail(\'' + escapeHtml(item.id) + '\')">' +
195
+ return '<tr data-wi-id="' + escapeHtml(item.id) + '" style="cursor:pointer" onclick="if(shouldIgnoreSelectionClick(event))return;openWorkItemDetail(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')">' +
196
196
  '<td style="min-width:280px;max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escapeHtml((item.title || '').slice(0, 200)) + '">' + escapeHtml(item.title || '') + followupChip + '</td>' +
197
197
  '<td><span style="font-size:var(--text-sm);color:var(--muted)">' + escapeHtml(item._source || '') + '</span>' +
198
198
  (item.scope === 'fan-out' ? ' <span class="pr-badge ' + (item.status === 'done' || item.status === 'failed' ? 'draft' : 'building') + '" style="font-size:var(--text-xs)">fan-out</span>' : '') + '</td>' +
@@ -312,7 +312,8 @@ function renderWorkItems(items, opts) {
312
312
  }
313
313
 
314
314
  async function editWorkItem(id, source) {
315
- const cached = allWorkItems.find(i => i.id === id);
315
+ source = source || '';
316
+ const cached = allWorkItems.find(i => i.id === id && (!source || i._source === source));
316
317
  if (!cached) return;
317
318
  // Hydrate the full record before rendering the form. The bulk
318
319
  // /api/work-items list endpoint truncates `description` and replaces
@@ -331,7 +332,7 @@ async function editWorkItem(id, source) {
331
332
  (cached.referencesCount > 0 && !Array.isArray(cached.references));
332
333
  if (needsHydration) {
333
334
  try {
334
- const r = await fetch('/api/work-items/' + encodeURIComponent(id));
335
+ const r = await fetch('/api/work-items/' + encodeURIComponent(id) + '?source=' + encodeURIComponent(source));
335
336
  if (!r.ok) throw new Error('HTTP ' + r.status);
336
337
  const data = await r.json();
337
338
  const full = data && data.item;
@@ -972,9 +973,15 @@ function _wiRenderDetail(item) {
972
973
  return html;
973
974
  }
974
975
 
975
- function openWorkItemDetail(id) {
976
+ function openWorkItemDetail(id, source) {
976
977
  if (window.getSelection && window.getSelection().toString().length > 0) return;
977
- const cached = allWorkItems.find(i => i.id === id);
978
+ source = source || '';
979
+ const matches = allWorkItems.filter(i => i.id === id && (!source || i._source === source));
980
+ if (!source && matches.length > 1) {
981
+ alert('Multiple work items share this ID. Open it from the Work Items page to select the project.');
982
+ return;
983
+ }
984
+ const cached = matches[0];
978
985
  if (!cached) return;
979
986
 
980
987
  // Render the modal immediately from the cached (slim) record so the click
@@ -999,7 +1006,7 @@ function openWorkItemDetail(id) {
999
1006
 
1000
1007
  if (!needsHydration) return;
1001
1008
 
1002
- fetch('/api/work-items/' + encodeURIComponent(id))
1009
+ fetch('/api/work-items/' + encodeURIComponent(id) + '?source=' + encodeURIComponent(source))
1003
1010
  .then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
1004
1011
  .then(function(data) {
1005
1012
  // Guard against modal navigation away from this WI during the fetch.
@@ -277,16 +277,18 @@ async function openSettings() {
277
277
  // (honors the legacy worktreeMode field). Chip is hidden by default and
278
278
  // toggled reactively below.
279
279
  // W-mr9mx93d000x1dda — third option "Hybrid dispatch": checkoutMode:'live'
280
- // PLUS a liveValidation block (coding WIs author in isolated worktrees; only
281
- // the chosen validation type(s) run in-place on the live checkout, with an
282
- // optional auto-dispatch-after-coding toggle). This mirrors the Projects-bar
280
+ // PLUS a liveValidation block (only chosen work-item types run in-place;
281
+ // all others use isolated worktrees, with an optional test auto-dispatch
282
+ // after eligible coding work). This mirrors the Projects-bar
283
283
  // picker (render-other.js _openCheckoutModeMenu / _renderCheckoutModeHybridStep)
284
284
  // so both surfaces configure the SAME liveValidation shape identically.
285
285
  var currentWtMode = (p.checkoutMode === 'live') ? 'live' : 'worktree';
286
286
  var lvTypes = (window._liveValidationTypesArray ? window._liveValidationTypesArray(p) : []);
287
- // 3-way UI mode: worktree / live / hybrid. Hybrid = live + >=1 validation type.
287
+ // 3-way UI mode: worktree / live / hybrid. Keep invalid persisted hybrid
288
+ // blocks in the hybrid editor so saving can repair rather than hide them as
289
+ // full-live.
288
290
  var currentCheckoutMode = (currentWtMode === 'live')
289
- ? (lvTypes.length > 0 ? 'hybrid' : 'live')
291
+ ? ((lvTypes.length > 0 || p.liveValidationInvalid) ? 'hybrid' : 'live')
290
292
  : 'worktree';
291
293
  var hybridTypes = window.CHECKOUT_MODE_HYBRID_TYPES || [];
292
294
  var hybridDefaultType = window.CHECKOUT_MODE_HYBRID_DEFAULT_TYPE || 'test';
@@ -303,26 +305,26 @@ async function openSettings() {
303
305
  var hybridPanel =
304
306
  '<div data-checkout-hybrid-panel="' + escHtml(p.name) + '" style="' + (currentCheckoutMode === 'hybrid' ? '' : 'display:none;') + 'margin-top:6px">' +
305
307
  '<div style="padding:6px 8px;background:rgba(234,179,8,0.12);border:1px solid var(--yellow);border-radius:4px;color:var(--yellow);font-size:var(--text-xs);line-height:1.4;margin-bottom:6px">' +
306
- '⚠ Hybrid mode: coding work items author in isolated worktrees; only the chosen validation type(s) run in-place on the live checkout (one mutating dispatch at a time; refused on a dirty tree).' +
308
+ '⚠ Hybrid mode: only the chosen work-item type(s) run in-place on the live checkout; all others use isolated worktrees (one mutating dispatch at a time; dirty-tree recovery follows the live-checkout auto-stash/reset settings).' +
307
309
  '</div>' +
308
- '<div style="font-size:var(--text-sm);color:var(--muted);margin-bottom:2px">Work-item type(s) to validate live:</div>' +
310
+ '<div style="font-size:var(--text-sm);color:var(--muted);margin-bottom:2px">Work-item type(s) to run live:</div>' +
309
311
  '<div style="margin-bottom:4px">' + hybridTypeChecks + '</div>' +
310
312
  '<label style="display:inline-flex;align-items:center;gap:6px;font-size:var(--text-sm);color:var(--text);margin-top:2px">' +
311
313
  '<input type="checkbox" id="set-liveValidationAutoDispatch-' + escHtml(p.name) + '"' + (hybridAutoPre ? ' checked' : '') + '>' +
312
- '<span>Automatically validate after each coding work item completes</span>' +
314
+ '<span>Automatically run a test validation after each eligible coding work item completes</span>' +
313
315
  '</label>' +
314
- '<div data-checkout-hybrid-error="' + escHtml(p.name) + '" style="display:none;margin-top:6px;color:var(--red);font-size:var(--text-xs)">Select at least one work-item type to validate live.</div>' +
316
+ '<div data-checkout-hybrid-error="' + escHtml(p.name) + '" style="display:none;margin-top:6px;color:var(--red);font-size:var(--text-xs)">Select at least one work-item type to run live.</div>' +
315
317
  '</div>';
316
318
  var worktreeModeBlock =
317
319
  '<div data-search="' + escHtml(wtModeSearch) + '" style="margin-bottom:6px">' +
318
320
  '<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">Checkout mode</label>' +
319
321
  '<select id="set-checkoutMode-' + escHtml(p.name) + '" data-checkout-mode-select="' + escHtml(p.name) + '" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-md)">' +
320
322
  '<option value="worktree" title="Each dispatch runs in its own isolated git worktree. Fully parallel, never touches your working directory."' + (currentCheckoutMode === 'worktree' ? ' selected' : '') + '>Worktree (default)</option>' +
321
- '<option value="live" title="Agents run in-place in the project folder. One mutating dispatch at a time; refuses to run on a dirty tree."' + (currentCheckoutMode === 'live' ? ' selected' : '') + '>Live checkout</option>' +
322
- '<option value="hybrid" title="Coding work authors in isolated worktrees; only a chosen validation step runs on the real checkout."' + (currentCheckoutMode === 'hybrid' ? ' selected' : '') + '>Hybrid dispatch</option>' +
323
+ '<option value="live" title="Agents run in-place in the project folder. One mutating dispatch at a time; dirty-tree handling follows the auto-stash/reset policy."' + (currentCheckoutMode === 'live' ? ' selected' : '') + '>Live checkout</option>' +
324
+ '<option value="hybrid" title="Only chosen work-item types run on the real checkout; all others use isolated worktrees."' + (currentCheckoutMode === 'hybrid' ? ' selected' : '') + '>Hybrid dispatch</option>' +
323
325
  '</select>' +
324
326
  '<div data-checkout-mode-chip="' + escHtml(p.name) + '" style="' + (currentCheckoutMode === 'live' ? '' : 'display:none;') + 'margin-top:6px;padding:6px 8px;background:rgba(234,179,8,0.12);border:1px solid var(--yellow);border-radius:4px;color:var(--yellow);font-size:var(--text-xs);line-height:1.4">' +
325
- '⚠ Live mode: dispatches run directly in this repo\'s checkout. Only one mutating dispatch runs at a time. Dirty working trees block dispatch — commit or stash before running.' +
327
+ '⚠ Live mode: dispatches run directly in this repo\'s checkout. Only one mutating dispatch runs at a time. Dirty-tree behavior follows the auto-stash/reset settings below.' +
326
328
  '</div>' +
327
329
  hybridPanel +
328
330
  '</div>';
@@ -774,6 +776,17 @@ async function openSettings() {
774
776
  if (hybridPanel) hybridPanel.style.display = (sel.value === 'hybrid') ? '' : 'none';
775
777
  });
776
778
  });
779
+ document.querySelectorAll('[id^="set-liveValidationAutoDispatch-"]').forEach(function(autoDispatch) {
780
+ autoDispatch.addEventListener('change', function() {
781
+ if (!autoDispatch.checked) return;
782
+ const projectName = autoDispatch.id.slice('set-liveValidationAutoDispatch-'.length);
783
+ const esc = (window.CSS && CSS.escape ? CSS.escape(projectName) : projectName);
784
+ const testType = document.querySelector(
785
+ '[data-live-validation-type="' + esc + '"][value="test"]',
786
+ );
787
+ if (testType) testType.checked = true;
788
+ });
789
+ });
777
790
  }
778
791
 
779
792
  // Lazily open/close the per-agent charter editor in the Settings Agents table.
@@ -1244,13 +1257,23 @@ async function saveSettings() {
1244
1257
  const types = [];
1245
1258
  typeEls.forEach(function(el) { if (el && el.checked) types.push(el.value); });
1246
1259
  const errEl = document.querySelector('[data-checkout-hybrid-error="' + esc + '"]');
1260
+ const autoEl = document.getElementById('set-liveValidationAutoDispatch-' + p.name);
1261
+ const autoDispatch = autoEl?.checked === true;
1247
1262
  if (types.length === 0) {
1248
- hybridValidationErrors.push(p.name);
1249
- if (errEl) errEl.style.display = '';
1263
+ hybridValidationErrors.push(p.name + ': select at least one live type');
1264
+ if (errEl) {
1265
+ errEl.textContent = 'Select at least one work-item type to run live.';
1266
+ errEl.style.display = '';
1267
+ }
1268
+ } else if (autoDispatch && types.indexOf('test') === -1) {
1269
+ hybridValidationErrors.push(p.name + ': auto-validation requires test');
1270
+ if (errEl) {
1271
+ errEl.textContent = 'Automatic validation requires the test type.';
1272
+ errEl.style.display = '';
1273
+ }
1250
1274
  } else {
1251
1275
  if (errEl) errEl.style.display = 'none';
1252
- const autoEl = document.getElementById('set-liveValidationAutoDispatch-' + p.name);
1253
- liveValidation = { type: types, autoDispatch: !!(autoEl && autoEl.checked) };
1276
+ liveValidation = { type: types, autoDispatch };
1254
1277
  }
1255
1278
  }
1256
1279
  // W-mqtvnnj1000357fa — per-project live-checkout auto-stash override.
@@ -1280,7 +1303,7 @@ async function saveSettings() {
1280
1303
  });
1281
1304
 
1282
1305
  if (hybridValidationErrors.length > 0) {
1283
- throw new Error('Hybrid dispatch needs at least one validation work-item type for: ' + hybridValidationErrors.join(', '));
1306
+ throw new Error('Invalid hybrid dispatch configuration: ' + hybridValidationErrors.join(', '));
1284
1307
  }
1285
1308
 
1286
1309
  // Save config