@yemi33/minions 0.1.2424 → 0.1.2426

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 (185) hide show
  1. package/bin/install-internal-minions.js +1209 -0
  2. package/bin/minions.js +75 -9
  3. package/dashboard/docs/typography.md +27 -12
  4. package/dashboard/js/command-center.js +13 -11
  5. package/dashboard/js/live-stream.js +1 -1
  6. package/dashboard/js/memory-search.js +388 -43
  7. package/dashboard/js/modal-qa.js +5 -5
  8. package/dashboard/js/qa.js +39 -26
  9. package/dashboard/js/refresh.js +37 -18
  10. package/dashboard/js/render-agents.js +14 -18
  11. package/dashboard/js/render-dispatch.js +14 -74
  12. package/dashboard/js/render-inbox.js +3 -3
  13. package/dashboard/js/render-meetings.js +8 -8
  14. package/dashboard/js/render-other.js +45 -20
  15. package/dashboard/js/render-pipelines.js +6 -6
  16. package/dashboard/js/render-plans.js +14 -14
  17. package/dashboard/js/render-prd.js +46 -46
  18. package/dashboard/js/render-prs.js +9 -65
  19. package/dashboard/js/render-schedules.js +5 -5
  20. package/dashboard/js/render-skills.js +6 -6
  21. package/dashboard/js/render-utils.js +4 -4
  22. package/dashboard/js/render-watches.js +4 -16
  23. package/dashboard/js/render-work-items.js +95 -107
  24. package/dashboard/js/settings.js +175 -74
  25. package/dashboard/layout.html +3 -2
  26. package/dashboard/pages/inbox.html +1 -1
  27. package/dashboard/pages/work.html +1 -1
  28. package/dashboard/shared/model-display.js +9 -0
  29. package/dashboard/shared/watches-source.js +43 -0
  30. package/dashboard/shared/welcome-popup.js +259 -0
  31. package/dashboard/slim/body.html +6 -6
  32. package/dashboard/slim/js/chat.js +4 -2
  33. package/dashboard/slim/js/history.js +10 -3
  34. package/dashboard/slim/js/modals-tiles.js +85 -16
  35. package/dashboard/slim/js/status.js +12 -16
  36. package/dashboard/slim/styles.css +24 -4
  37. package/dashboard/styles.css +62 -27
  38. package/dashboard-build.js +8 -2
  39. package/dashboard.js +3854 -2514
  40. package/docs/README.md +7 -5
  41. package/docs/api-errors.md +144 -0
  42. package/docs/auto-discovery.md +83 -61
  43. package/docs/capture-demos.js +173 -30
  44. package/docs/command-center.md +5 -1
  45. package/docs/completion-reports.md +127 -11
  46. package/docs/constants.md +15 -3
  47. package/docs/constellation-style-telemetry.md +2 -2
  48. package/docs/copilot-cli-schema.md +32 -22
  49. package/docs/cross-repo-plans.md +30 -30
  50. package/docs/demo/memory-system.html +1 -1
  51. package/docs/deprecated.json +132 -18
  52. package/docs/diagnostics-memory.md +8 -4
  53. package/docs/human-vs-automated.md +2 -2
  54. package/docs/index.html +9 -2
  55. package/docs/internal-install.md +212 -0
  56. package/docs/kb-pr3223-cascade-archiving.md +16 -0
  57. package/docs/kb-pr696-merge-conflict-docs.md +23 -0
  58. package/docs/kb-sweep.md +2 -2
  59. package/docs/keep-processes.md +8 -1
  60. package/docs/live-checkout-mode.md +1 -1
  61. package/docs/managed-spawn.md +21 -3
  62. package/docs/named-agents.md +3 -2
  63. package/docs/onboarding.md +26 -0
  64. package/docs/plan-lifecycle.md +32 -32
  65. package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
  66. package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
  67. package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
  68. package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
  69. package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
  70. package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
  71. package/docs/qa-runbook-lifecycle.md +67 -51
  72. package/docs/qa-runbooks.md +2 -2
  73. package/docs/runtime-adapters.md +101 -50
  74. package/docs/security.md +28 -0
  75. package/docs/self-improvement.md +47 -13
  76. package/docs/skills.md +29 -0
  77. package/docs/slim-ux/concepts.md +2 -2
  78. package/docs/specs/agent-configurability.md +43 -41
  79. package/docs/specs/agent-rename.md +13 -13
  80. package/docs/team-memory.md +283 -14
  81. package/docs/tutorials/01-install-and-connect.md +23 -0
  82. package/docs/tutorials/03-plan-driven-feature.md +3 -3
  83. package/docs/tutorials/04-runtimes-and-harness.md +18 -21
  84. package/docs/tutorials/08-operations-and-recovery.md +3 -2
  85. package/docs/tutorials/README.md +4 -3
  86. package/docs/visual-evidence-ci.md +103 -0
  87. package/docs/watches.md +35 -30
  88. package/docs/worktree-lifecycle.md +151 -13
  89. package/engine/acp-transport.js +1 -0
  90. package/engine/ado.js +80 -33
  91. package/engine/agent-api-validation.js +571 -0
  92. package/engine/agent-worker-pool.js +7 -6
  93. package/engine/api-contracts/agent-content.js +761 -0
  94. package/engine/api-contracts/cc-ops.js +219 -0
  95. package/engine/api-contracts/config-runtime.js +419 -0
  96. package/engine/api-contracts/core.js +138 -0
  97. package/engine/api-contracts/index.js +506 -0
  98. package/engine/api-contracts/orchestration.js +602 -0
  99. package/engine/api-contracts/pull-requests.js +323 -0
  100. package/engine/api-contracts/qa-process.js +764 -0
  101. package/engine/api-contracts/work-plan-prd.js +494 -0
  102. package/engine/api-validation.js +543 -0
  103. package/engine/cc-api-validation.js +586 -0
  104. package/engine/cc-worker-pool.js +9 -1
  105. package/engine/cleanup.js +393 -271
  106. package/engine/cli.js +65 -7
  107. package/engine/comment-classifier.js +70 -5
  108. package/engine/consolidation.js +123 -5
  109. package/engine/content-api-validation.js +606 -0
  110. package/engine/create-pr-worktree.js +22 -1
  111. package/engine/db/migrations/027-review-learning-lifecycle.js +162 -0
  112. package/engine/dispatch.js +33 -0
  113. package/engine/distribution.js +189 -0
  114. package/engine/execution-model.js +59 -0
  115. package/engine/features.js +12 -0
  116. package/engine/github.js +67 -25
  117. package/engine/harness-context.js +313 -0
  118. package/engine/inbox-store.js +1 -1
  119. package/engine/kb-sweep.js +161 -38
  120. package/engine/keep-process-sweep.js +43 -3
  121. package/engine/lifecycle.js +1066 -165
  122. package/engine/llm.js +4 -0
  123. package/engine/managed-spawn.js +463 -48
  124. package/engine/meeting.js +197 -34
  125. package/engine/memory-retrieval.js +280 -0
  126. package/engine/memory-store.js +821 -38
  127. package/engine/model-discovery.js +17 -3
  128. package/engine/pipeline.js +802 -14
  129. package/engine/plan-prd-validation.js +745 -0
  130. package/engine/playbook.js +206 -17
  131. package/engine/pooled-agent-process.js +6 -1
  132. package/engine/pr-action.js +1 -1
  133. package/engine/pr-issue-validation.js +653 -0
  134. package/engine/pr-resolve.js +75 -13
  135. package/engine/prd-store.js +39 -6
  136. package/engine/preflight.js +7 -2
  137. package/engine/process-utils.js +79 -31
  138. package/engine/projects.js +198 -12
  139. package/engine/promotion.js +371 -0
  140. package/engine/qa-from-prd.js +3 -0
  141. package/engine/qa-process-validation.js +572 -0
  142. package/engine/qa-runbooks.js +41 -37
  143. package/engine/qa-runners/maestro.js +11 -7
  144. package/engine/qa-runners/playwright.js +37 -14
  145. package/engine/qa-runners.js +9 -10
  146. package/engine/qa-runs.js +124 -11
  147. package/engine/qa-sessions.js +139 -40
  148. package/engine/quarantine-refs.js +81 -11
  149. package/engine/queries.js +163 -9
  150. package/engine/review-learning-backfill.js +482 -0
  151. package/engine/review-learning.js +1236 -0
  152. package/engine/runtimes/claude.js +60 -2
  153. package/engine/runtimes/codex.js +57 -16
  154. package/engine/runtimes/contract.js +21 -0
  155. package/engine/runtimes/copilot.js +51 -3
  156. package/engine/runtimes/index.js +1 -0
  157. package/engine/scheduler.js +201 -32
  158. package/engine/settings-validation.js +909 -0
  159. package/engine/shared.js +705 -208
  160. package/engine/spawn-agent.js +48 -21
  161. package/engine/steering-constraints.js +31 -0
  162. package/engine/steering-store.js +10 -2
  163. package/engine/steering.js +14 -2
  164. package/engine/timeout.js +404 -111
  165. package/engine/untrusted-fence.js +24 -10
  166. package/engine/watch-actions.js +7 -2
  167. package/engine/watches.js +353 -51
  168. package/engine/work-item-validation.js +561 -0
  169. package/engine/work-items-store.js +61 -1
  170. package/engine/worktree-gc.js +656 -37
  171. package/engine/worktree-preflight.js +154 -0
  172. package/engine.js +2068 -342
  173. package/package.json +1 -1
  174. package/playbooks/_pr-description-audit.md +88 -41
  175. package/playbooks/build-fix-complex.md +3 -3
  176. package/playbooks/docs.md +2 -2
  177. package/playbooks/fix.md +18 -18
  178. package/playbooks/implement.md +18 -18
  179. package/playbooks/qa-session-draft.md +10 -12
  180. package/playbooks/qa-session-execute.md +16 -12
  181. package/playbooks/qa-validate.md +8 -7
  182. package/playbooks/shared-rules.md +45 -0
  183. package/playbooks/test.md +4 -0
  184. package/prompts/cc-system.md +1 -1
  185. package/skills/check-self-authored-review-comment/SKILL.md +2 -2
@@ -97,7 +97,10 @@ async function openSettings() {
97
97
  // muted text — operators see exactly what each agent will resolve to without
98
98
  // chasing config files. Empty input clears the override → re-inherit fleet.
99
99
  const fleetCliLabel = e.defaultCli || 'copilot';
100
- const fleetModelLabel = e.defaultModel ? String(e.defaultModel) : 'CLI default';
100
+ const statusAutoMode = window._lastStatus?.autoMode || {};
101
+ const fleetModelLabel = MinionsModelDisplay.label(
102
+ e.defaultModel || statusAutoMode.defaultModelDisplay
103
+ );
101
104
  const agentRows = Object.entries(agents).map(function([id, a]) {
102
105
  return '<tr>' +
103
106
  '<td style="font-weight:600">' + escHtml(a.emoji || '') + ' ' + escHtml(a.name || id) + '</td>' +
@@ -140,18 +143,18 @@ async function openSettings() {
140
143
  '<select id="set-defaultCli" 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)">' +
141
144
  '<option value="">Loading…</option>' +
142
145
  '</select>' +
143
- '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:1px">Fleet-wide runtime — registered adapters from <code>/api/runtimes</code></div>' +
146
+ '<div style="font-size:var(--text-sm);color:var(--muted);margin-top:1px">Fleet-wide runtime — registered adapters from <code>/api/runtimes</code></div>' +
144
147
  '</div>' +
145
148
  '<div data-search="default model fleet">' +
146
149
  '<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">Default Model</label>' +
147
150
  '<div id="set-defaultModel-wrap"><input id="set-defaultModel" value="' + escHtml(e.defaultModel || '') + '" placeholder="Default (CLI chooses)" style="' + MODEL_FIELD_INPUT_STYLE + '"></div>' +
148
- '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:1px">Empty = let the runtime pick its own default</div>' +
151
+ '<div style="font-size:var(--text-sm);color:var(--muted);margin-top:1px">Empty = let the runtime pick its own default</div>' +
149
152
  '</div>' +
150
153
  '</div>' +
151
154
  // CC overrides — collapsed by default
152
155
  '<details id="set-cc-overrides-details"' + ((e.ccCli || e.ccModel) ? ' open' : '') + ' style="margin-top:8px;border-top:1px solid var(--border);padding-top:8px">' +
153
156
  '<summary style="cursor:pointer;font-size:var(--text-base);color:var(--text);user-select:none">Customize CC separately ' +
154
- '<span style="font-size:var(--text-xs);color:var(--muted)">(Command Center + doc-chat use the fleet defaults unless overridden)</span>' +
157
+ '<span style="font-size:var(--text-sm);color:var(--muted)">(Command Center + doc-chat use the fleet defaults unless overridden)</span>' +
155
158
  '</summary>' +
156
159
  '<div style="display:grid;grid-template-columns:1fr 2fr 1fr;gap:8px;margin-top:8px">' +
157
160
  '<div data-search="cc cli command center runtime">' +
@@ -159,7 +162,7 @@ async function openSettings() {
159
162
  '<select id="set-ccCli" 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)">' +
160
163
  '<option value="">Loading…</option>' +
161
164
  '</select>' +
162
- '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:1px">Empty = inherit Default CLI</div>' +
165
+ '<div style="font-size:var(--text-sm);color:var(--muted);margin-top:1px">Empty = inherit Default CLI</div>' +
163
166
  '</div>' +
164
167
  '<div data-search="cc model command center">' +
165
168
  '<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">CC Model</label>' +
@@ -169,7 +172,7 @@ async function openSettings() {
169
172
  // and the hint below, breaking vertical alignment with the
170
173
  // sibling CC CLI / Effort columns.
171
174
  '<div id="set-ccModel-wrap"><input id="set-ccModel" value="' + escHtml(e.ccModel || '') + '" placeholder="(inherits Default Model)" style="' + MODEL_FIELD_INPUT_STYLE + '"></div>' +
172
- '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:1px">Empty = inherit Default Model</div>' +
175
+ '<div style="font-size:var(--text-sm);color:var(--muted);margin-top:1px">Empty = inherit Default Model</div>' +
173
176
  '</div>' +
174
177
  '<div data-search="cc effort reasoning">' +
175
178
  '<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">Effort</label>' +
@@ -179,13 +182,13 @@ async function openSettings() {
179
182
  '<option value="medium"' + (e.ccEffort === 'medium' ? ' selected' : '') + '>Medium</option>' +
180
183
  '<option value="high"' + (e.ccEffort === 'high' ? ' selected' : '') + '>High</option>' +
181
184
  '</select>' +
182
- '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:1px">CC reasoning depth</div>' +
185
+ '<div style="font-size:var(--text-sm);color:var(--muted);margin-top:1px">CC reasoning depth</div>' +
183
186
  '</div>' +
184
187
  '</div>' +
185
188
  // W-mpmwxni2000c25c7-d — per-turn watchdog. Surfaced under CC overrides
186
189
  // because it gates CC/doc-chat error visibility (not the agent fleet).
187
190
  '<div style="display:grid;grid-template-columns:1fr;gap:8px;margin-top:8px">' +
188
- settingsField('CC Turn Timeout', 'set-ccTurnTimeoutMs', e.ccTurnTimeoutMs || 300000, 'ms', 'Per-turn no-progress watchdog for CC + doc-chat. Resets on every liveness signal (token chunk, tool call, tool update) so an actively-streaming turn — long shell command, deep search, sub-agent loop — survives indefinitely up to the outer 1h hard ceiling. Only true silence past this window fires event: error with code: cc-turn-timeout, stops the spinner, and offers Retry. Clamped to 100003600000 ms.') +
191
+ settingsField('CC Turn Timeout', 'set-ccTurnTimeoutMs', (e.ccTurnTimeoutMs || 300000) / 1000, 's', 'Per-turn no-progress watchdog for CC + doc-chat. Resets on every liveness signal (token chunk, tool call, tool update) so an actively-streaming turn — long shell command, deep search, sub-agent loop — survives indefinitely up to the outer 1h hard ceiling. Only true silence past this window fires event: error with code: cc-turn-timeout, stops the spinner, and offers Retry. Clamped to 103600 s.', '0.001') +
189
192
  '</div>' +
190
193
  '</details>' +
191
194
  '</div>' +
@@ -198,18 +201,20 @@ async function openSettings() {
198
201
 
199
202
  const paneAutoFix =
200
203
  '<h3>Auto-fix &amp; Review Loop</h3>' +
201
- '<div class="settings-pane-sub">PR-triggered dispatch gates only. Each toggle below decides when an agent is auto-spawned in response to a PR signal (build failure, merge conflict, review verdict, human comment). All require the matching provider polling (Polling tab). The <em>Pause All Auto-fix</em> kill-switch below inerts every per-cause gate at once. Non-dispatch knobs (auto-merge, auto-vote, plan/decompose defaults) moved to the new <strong>PR Lifecycle</strong> and <strong>Workflow Defaults</strong> panes.</div>' +
204
+ '<div class="settings-pane-sub">Everything in this pane is <strong>automatic</strong>. Each tile below enables or disables one automatic PR-triggered dispatch trigger (build failure, merge conflict, review verdict, human comment) — the section framing carries the "auto", so the tiles just name the cause. All require the matching provider polling (Polling tab). The <em>Pause ALL</em> kill-switch below inerts every trigger at once. Non-dispatch knobs (auto-merge, auto-vote, plan/decompose defaults) moved to the new <strong>PR Lifecycle</strong> and <strong>Workflow Defaults</strong> panes.</div>' +
202
205
  '<div class="settings-stack" style="margin-bottom:16px">' +
203
- settingsToggle('🛑 Pause ALL PR auto-fix dispatches', 'set-autoFixPaused', !!e.autoFixPaused, 'Halts every PR-triggered dispatch (review, re-review, build-failure fix, review-feedback fix, human-comment fix, merge-conflict fix) on the next tick. In-flight agents keep running. Reversible without restart. Per-cause flags below are inert while this is set.') +
206
+ settingsToggle('🛑 Pause ALL PR auto-fix dispatches', 'set-autoFixPaused', !!e.autoFixPaused, 'Halts every PR-triggered dispatch (review, re-review, build-failure fix, review-feedback fix, human-comment fix, merge-conflict fix) on the next tick. In-flight agents keep running. Reversible without restart. Per-cause tiles below are inert while this is set.') +
207
+ '</div>' +
208
+ '<div class="settings-pane-sub" style="margin-bottom:8px">Per-cause triggers — each tile automatically dispatches an agent for one PR signal (it names the exact discoverFromPrs site it gates). Enabled tiles are highlighted; disabled tiles are dimmed. When <em>Pause ALL</em> is ON every tile is inert; when it is OFF, each tile decides independently whether its site fires.</div>' +
209
+ '<div class="settings-tile-grid" style="margin-bottom:16px">' +
210
+ settingsTile('Builds', 'set-autoFixBuilds', e.autoFixBuilds !== false, 'Shared dispatch gate: auto-fix agent when a PR build fails; also requires that PR provider polling is enabled', '🔨', 'auto-fix builds') +
211
+ settingsTile('Conflicts', 'set-autoFixConflicts', e.autoFixConflicts !== false, 'Shared dispatch gate: auto-fix agent when a PR merge conflict is detected; also requires that PR provider polling is enabled', '🔀', 'auto-fix conflicts') +
212
+ settingsTile('Review PRs', 'set-autoReviewPrs', e.autoReviewPrs !== false, 'Shared dispatch gate: review agent for newly opened agent PRs; also requires that PR provider polling is enabled', '🔍', 'auto-review prs') +
213
+ settingsTile('Re-review PRs', 'set-autoReReviewPrs', e.autoReReviewPrs !== false, 'Shared dispatch gate: review agent after a fix push is awaiting re-review; also requires that PR provider polling is enabled', '🔁', 'auto-re-review prs') +
214
+ settingsTile('Review Feedback', 'set-autoFixReviewFeedback', e.autoFixReviewFeedback !== false, 'Shared dispatch gate: fix agent for minions changes-requested verdicts; also requires that PR provider polling is enabled', '✍️', 'auto-fix review feedback') +
215
+ settingsTile('Human Comments', 'set-autoFixHumanComments', e.autoFixHumanComments !== false, 'Shared dispatch gate: fix agent for actionable human PR comments; also requires that PR provider polling is enabled', '💬', 'auto-fix human comments') +
204
216
  '</div>' +
205
- '<div class="settings-pane-sub" style="margin-bottom:8px">Per-cause dispatch gates — each one names the exact discoverFromPrs site it gates. When <em>Pause All Auto-fix</em> is ON these are inert; when it is OFF, each gate decides independently whether its site fires.</div>' +
206
217
  '<div class="settings-stack" style="margin-bottom:16px">' +
207
- settingsToggle('Auto-fix Builds', 'set-autoFixBuilds', e.autoFixBuilds !== false, 'Shared dispatch gate: auto-fix agent when a PR build fails; also requires that PR provider polling is enabled') +
208
- settingsToggle('Auto-fix Conflicts', 'set-autoFixConflicts', e.autoFixConflicts !== false, 'Shared dispatch gate: auto-fix agent when a PR merge conflict is detected; also requires that PR provider polling is enabled') +
209
- settingsToggle('Auto-review PRs', 'set-autoReviewPrs', e.autoReviewPrs !== false, 'Shared dispatch gate: review agent for newly opened agent PRs; also requires that PR provider polling is enabled') +
210
- settingsToggle('Auto-re-review PRs', 'set-autoReReviewPrs', e.autoReReviewPrs !== false, 'Shared dispatch gate: review agent after a fix push is awaiting re-review; also requires that PR provider polling is enabled') +
211
- settingsToggle('Auto-fix Review Feedback', 'set-autoFixReviewFeedback', e.autoFixReviewFeedback !== false, 'Shared dispatch gate: fix agent for minions changes-requested verdicts; also requires that PR provider polling is enabled') +
212
- settingsToggle('Auto-fix Human Comments', 'set-autoFixHumanComments', e.autoFixHumanComments !== false, 'Shared dispatch gate: fix agent for actionable human PR comments; also requires that PR provider polling is enabled') +
213
218
  settingsToggle('Eval Loop', 'set-evalLoop', e.evalLoop !== false, 'Gates the review→fix iteration loop only (first review, re-review, review-feedback fix). Does NOT gate build-failure, merge-conflict, or human-comment fixes. Use the emergency stop above to halt everything.') +
214
219
  settingsToggle('Pre-dispatch Eval (cheap LLM gate)', 'set-enablePreDispatchEval', e.enablePreDispatchEval !== false, 'P-d2a9f6e5: cheap LLM gate that screens work items for clear/actionable/testable criteria BEFORE queueing the agent. Catches noop dispatches authored from impossible/ambiguous WIs. Fail-open on any validator error.') +
215
220
  settingsToggle('Pre-dispatch Eval: skip PRD-sourced items', 'set-preDispatchEvalSkipPrdSourced', e.preDispatchEvalSkipPrdSourced !== false, 'W-mq9acoo800177bcb: short-circuit the validator for items materialized from an approved/active PRD — plan-to-prd already LLM-vets them. Reduces queue-time on an N-item approved PRD from ~N×25s to nearly instant. OFF = always re-validate even PRD-sourced items.') +
@@ -231,7 +236,9 @@ async function openSettings() {
231
236
  '<div class="settings-pane-sub">Post-merge / post-review lifecycle knobs. These are not dispatch gates — they control what Minions does with a PR once review or build state lands.</div>' +
232
237
  '<div class="settings-stack">' +
233
238
  settingsToggle('Auto-apply review vote to PR', 'set-autoApplyReviewVote', !!e.autoApplyReviewVote, 'When ON, Minions review verdicts (APPROVE / REQUEST_CHANGES) automatically flip the platform vote on ADO/GitHub. When OFF (default), verdicts are informational only and the human casts the final vote.') +
239
+ settingsToggle('Create PRs as draft', 'set-createPrsAsDraft', !!e.createPrsAsDraft, 'When ON, Minions-created PRs open as DRAFT (GitHub `gh pr create --draft`; ADO `az repos pr create --draft true`). When OFF (default), PRs open ready-for-review as today. Aggregate/E2E verify PRs already open as draft regardless of this flag.') +
234
240
  settingsToggle('Auto-complete PRs', 'set-autoCompletePrs', !!e.autoCompletePrs, 'Auto-merge PRs when builds pass and review is approved (opt-in). Independent of the per-cause auto-fix gates above.') +
241
+ settingsToggle('Surface repo harnesses in PR body', 'set-includeHarnessSectionInPrBody', e.includeHarnessSectionInPrBody !== false, 'When ON (default), Minions-authored PRs get a fenced "Repo harnesses used" section listing the harness assets in effect for the dispatch\'s runtime + project — runtime, injected CLAUDE.md files, AGENTS.md discovery, MCP servers, and skills. Pure reporting from the same read-only source as `minions doctor --harness`; it does not change harness propagation. OFF suppresses the section.') +
235
242
  '</div>';
236
243
 
237
244
  // P-g7a2b4c5 — new "Workflow Defaults" pane. Owns the four workflow-level
@@ -245,7 +252,7 @@ async function openSettings() {
245
252
  settingsToggle('Auto-approve Plans', 'set-autoApprovePlans', !!e.autoApprovePlans, 'PRDs are approved automatically without human review.') +
246
253
  settingsToggle('Auto-decompose', 'set-autoDecompose', e.autoDecompose !== false, 'Large implement items are auto-split into sub-tasks.') +
247
254
  settingsToggle('Auto-archive Plans', 'set-autoArchive', !!e.autoArchive, 'Automatically archive plans after verify completes (off = manual archive via dashboard).') +
248
- settingsToggle('Auto-consolidate Memory', 'set-autoConsolidateMemory', !!e.autoConsolidateMemory, 'Periodically spawn the KB sweep (dedup + compress + normalize knowledge/) from the engine tick on a 4h cadence. Inbox→notes consolidation already runs every tick (gated by the Consolidation Threshold under Advanced); this toggle controls only the KB sweep that was previously dashboard-button-only.') +
255
+ settingsToggle('Auto-consolidate Memory', 'set-autoConsolidateMemory', !!e.autoConsolidateMemory, 'Enabled by default. Periodically spawn the KB sweep (dedup + compress + normalize knowledge/) from the engine tick on a 4h cadence. Inbox→notes consolidation runs independently every tick.') +
249
256
  settingsToggle('Bootstrap Default Schedules', 'set-bootstrapDefaultSchedules', e.bootstrapDefaultSchedules !== false, 'Auto-create the per-project git-fetch-freshness schedule (every 12h) when a project is added, and ensure one shared daily harness-sweep schedule exists engine-wide. Idempotent and non-destructive — never overwrites a schedule you have customized. Turn off to manage these schedules entirely by hand.') +
250
257
  '</div>';
251
258
 
@@ -291,7 +298,7 @@ async function openSettings() {
291
298
  }).join('');
292
299
  })() +
293
300
  '</select>' +
294
- '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:1px">Scales the whole dashboard. Persisted in browser + server.</div>' +
301
+ '<div style="font-size:var(--text-sm);color:var(--muted);margin-top:1px">Scales the whole dashboard. Persisted in browser + server.</div>' +
295
302
  '</div>' +
296
303
  slimUxControl +
297
304
  '</div>';
@@ -340,7 +347,7 @@ async function openSettings() {
340
347
  '<div>' +
341
348
  '<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">Remote default <span style="opacity:0.6">(read-only)</span></label>' +
342
349
  '<input id="set-remoteDefaultBranch-' + p.name + '" value="' + escHtml(remoteDefault || '(unset)') + '" readonly style="width:100%;padding:4px 6px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--muted);font-size:var(--text-md);font-family:ui-monospace,SFMono-Regular,Menlo,monospace">' +
343
- '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:1px">Parsed from <code>git symbolic-ref refs/remotes/origin/HEAD</code>' + (localBranch ? '. Local HEAD: <code>' + escHtml(localBranch) + '</code>' : '') + '.</div>' +
350
+ '<div style="font-size:var(--text-sm);color:var(--muted);margin-top:1px">Parsed from <code>git symbolic-ref refs/remotes/origin/HEAD</code>' + (localBranch ? '. Local HEAD: <code>' + escHtml(localBranch) + '</code>' : '') + '.</div>' +
344
351
  '</div>' +
345
352
  '</div>';
346
353
  // P-a3f9b207 (consolidated W-mqiaw974) — per-project checkoutMode dropdown
@@ -377,7 +384,7 @@ async function openSettings() {
377
384
  }).join('');
378
385
  var hybridPanel =
379
386
  '<div data-checkout-hybrid-panel="' + escHtml(p.name) + '" style="' + (currentCheckoutMode === 'hybrid' ? '' : 'display:none;') + 'margin-top:6px">' +
380
- '<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">' +
387
+ '<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-sm);line-height:1.4;margin-bottom:6px">' +
381
388
  '⚠ 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).' +
382
389
  '</div>' +
383
390
  '<div style="font-size:var(--text-sm);color:var(--muted);margin-bottom:2px">Work-item type(s) to run live:</div>' +
@@ -386,7 +393,7 @@ async function openSettings() {
386
393
  '<input type="checkbox" id="set-liveValidationAutoDispatch-' + escHtml(p.name) + '"' + (hybridAutoPre ? ' checked' : '') + '>' +
387
394
  '<span>Automatically run a test validation after each eligible coding work item completes</span>' +
388
395
  '</label>' +
389
- '<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>' +
396
+ '<div data-checkout-hybrid-error="' + escHtml(p.name) + '" style="display:none;margin-top:6px;color:var(--red);font-size:var(--text-sm)">Select at least one work-item type to run live.</div>' +
390
397
  '</div>';
391
398
  var worktreeModeBlock =
392
399
  '<div data-search="' + escHtml(wtModeSearch) + '" style="margin-bottom:6px">' +
@@ -396,7 +403,7 @@ async function openSettings() {
396
403
  '<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>' +
397
404
  '<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>' +
398
405
  '</select>' +
399
- '<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">' +
406
+ '<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-sm);line-height:1.4">' +
400
407
  '⚠ 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.' +
401
408
  '</div>' +
402
409
  hybridPanel +
@@ -416,7 +423,24 @@ async function openSettings() {
416
423
  '<option value="on"' + (autoStashRaw === 'on' ? ' selected' : '') + '>On — auto-stash dirty tree</option>' +
417
424
  '<option value="off"' + (autoStashRaw === 'off' ? ' selected' : '') + '>Off — fail on dirty tree</option>' +
418
425
  '</select>' +
419
- '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:2px;line-height:1.4">Live mode only. On = `git stash push --include-untracked` a dirty tree before dispatch instead of failing; the engine never pops the stash (run `git stash pop` manually). Overrides the fleet-wide setting under Worktrees.</div>' +
426
+ '<div style="font-size:var(--text-sm);color:var(--muted);margin-top:2px;line-height:1.4">Live mode only. On = `git stash push --include-untracked` a dirty tree before dispatch instead of failing; the engine never pops the stash (run `git stash pop` manually). Overrides the fleet-wide setting under Worktrees.</div>' +
427
+ '</div>';
428
+ // W-mrxv648b — per-project "Create PRs as draft" override. Tri-state:
429
+ // "" (Inherit → delete the key so the fleet-wide engine.createPrsAsDraft
430
+ // applies), "draft" (force true), "ready" (force false). Only an explicit
431
+ // draft/ready overrides the fleet setting; Inherit leaves p.createPrsAsDraft
432
+ // unset so shared.resolveCreatePrsAsDraft falls back to the engine value.
433
+ var createPrsAsDraftRaw = (p.createPrsAsDraft === true) ? 'draft' : (p.createPrsAsDraft === false) ? 'ready' : '';
434
+ var createPrsAsDraftSearch = 'create prs as draft pull request pr draft ready review lifecycle override';
435
+ var createPrsAsDraftBlock =
436
+ '<div data-search="' + escHtml(createPrsAsDraftSearch) + '" style="margin-bottom:6px">' +
437
+ '<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">Create PRs as draft</label>' +
438
+ '<select id="set-createPrsAsDraft-' + 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)">' +
439
+ '<option value=""' + (createPrsAsDraftRaw === '' ? ' selected' : '') + '>Inherit fleet default</option>' +
440
+ '<option value="draft"' + (createPrsAsDraftRaw === 'draft' ? ' selected' : '') + '>Draft — open PRs as draft</option>' +
441
+ '<option value="ready"' + (createPrsAsDraftRaw === 'ready' ? ' selected' : '') + '>Not draft — open PRs ready</option>' +
442
+ '</select>' +
443
+ '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:2px;line-height:1.4">Overrides the fleet-wide "Create PRs as draft" setting under PR Lifecycle for THIS project only. Draft = Minions-created PRs open as draft (GitHub `--draft`; ADO `--draft true`). Inherit follows the engine flag. Aggregate/E2E verify PRs and the shared-branch aggregate PR are unaffected.</div>' +
420
444
  '</div>';
421
445
  // W-mrchrfe100067d3a — per-project explicit allowlist of secondary repo
422
446
  // scopes this project's checkout is also allowed to track PRs for (e.g.
@@ -434,12 +458,13 @@ async function openSettings() {
434
458
  return '<div data-settings-project="' + escHtml(p.name) + '" class="settings-project-block' + (p.name === selectedProject ? '' : ' project-config-hidden') + '" data-search="project ' + escHtml(p.name.toLowerCase()) + '" style="border:1px solid var(--border);border-radius:6px;padding:10px 12px;margin-bottom:12px">' +
435
459
  '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">' +
436
460
  '<div style="font-size:var(--text-md);font-weight:600">' + escHtml(p.name) + '</div>' +
437
- '<button onclick="MinionsSettings.removeProject(\'' + escHtml(p.name) + '\')" style="font-size:var(--text-xs);padding:2px 8px;background:transparent;color:var(--red);border:1px solid var(--red);border-radius:3px;cursor:pointer">Remove</button>' +
461
+ '<button onclick="MinionsSettings.removeProject(\'' + escHtml(p.name) + '\')" style="font-size:var(--text-sm);padding:2px 8px;background:transparent;color:var(--red);border:1px solid var(--red);border-radius:3px;cursor:pointer">Remove</button>' +
438
462
  '</div>' +
439
463
  pathRow +
440
464
  branchGrid +
441
465
  worktreeModeBlock +
442
466
  autoStashBlock +
467
+ createPrsAsDraftBlock +
443
468
  additionalScopesBlock +
444
469
  driftNote +
445
470
  '<div style="display:flex;flex-direction:column;gap:6px;margin-top:8px">' +
@@ -510,16 +535,16 @@ async function openSettings() {
510
535
  '<div class="settings-pane-sub">How many agents run at once and how long they get before being killed for silence or runaway duration.</div>' +
511
536
  '<div class="settings-grid-2">' +
512
537
  settingsField('Max Concurrent Agents', 'set-maxConcurrent', e.maxConcurrent || 5, '', 'Max agents working simultaneously') +
513
- settingsField('Min Tick Interval', 'set-tickInterval', e.tickInterval || 10000, 'ms', 'Minimum gap between tick starts. Slow ticks (PR polls, reconcilers) may delay the next slot — see `running` indicator on /engine.') +
514
- settingsField('Agent Timeout', 'set-agentTimeout', e.agentTimeout || 18000000, 'ms', 'Kill agent after this duration') +
515
- settingsField('Heartbeat Timeout', 'set-heartbeatTimeout', e.heartbeatTimeout || 300000, 'ms', 'No output = dead after this') +
538
+ settingsField('Min Tick Interval', 'set-tickInterval', (e.tickInterval || 10000) / 1000, 's', 'Minimum gap between tick starts. Slow ticks (PR polls, reconcilers) may delay the next slot — see `running` indicator on /engine.', '0.001') +
539
+ settingsField('Agent Timeout', 'set-agentTimeout', (e.agentTimeout || 18000000) / 1000, 's', 'Kill agent after this duration', '0.001') +
540
+ settingsField('Heartbeat Timeout', 'set-heartbeatTimeout', (e.heartbeatTimeout || 300000) / 1000, 's', 'No output = dead after this', '0.001') +
516
541
  settingsField('Idle Alert', 'set-idleAlertMinutes', e.idleAlertMinutes || 15, 'min', 'Alert after agent idle this long') +
517
- settingsField('Agent Busy Reassign', 'set-agentBusyReassignMs', e.agentBusyReassignMs || 600000, 'ms', 'Reassign work to another agent after it waits this long on a busy agent') +
542
+ settingsField('Agent Busy Reassign', 'set-agentBusyReassignMs', (e.agentBusyReassignMs || 600000) / 1000, 's', 'Reassign work to another agent after it waits this long on a busy agent', '0.001') +
518
543
  settingsField('Max Retries Per Agent', 'set-maxRetriesPerAgent', e.maxRetriesPerAgent ?? 2, '', 'After the same agent fails the same work item this many times, the next retry reassigns to a different eligible agent (consults routing.md + availability). Falls back to the same agent only when no alternate is available. Counted separately from total maxRetries (which still caps overall retries).') +
519
- settingsField('Restart Grace Period', 'set-restartGracePeriod', e.restartGracePeriod || 1200000, 'ms', 'Grace period before orphan detection on restart') +
520
- settingsField('Shutdown Timeout', 'set-shutdownTimeout', e.shutdownTimeout || 300000, 'ms', 'Max wait for agents during graceful shutdown') +
521
- settingsField('Meeting Round Timeout', 'set-meetingRoundTimeout', e.meetingRoundTimeout || 900000, 'ms', 'Auto-advance meeting round after this') +
522
- settingsField('Steering Deferred Max', 'set-steeringDeferredMaxMs', e.steeringDeferredMaxMs || 900000, 'ms', 'Max wait for a runtime to emit a resumable checkpoint before a deferred steering message is flagged stranded. After this, the engine warns to live-output, marks _steeringStranded on the dispatch, and (when the steering store is present) sets store status=stranded. Default 15min; range 60s–4h.') +
544
+ settingsField('Restart Grace Period', 'set-restartGracePeriod', (e.restartGracePeriod || 1200000) / 1000, 's', 'Grace period before orphan detection on restart', '0.001') +
545
+ settingsField('Shutdown Timeout', 'set-shutdownTimeout', (e.shutdownTimeout || 300000) / 1000, 's', 'Max wait for agents during graceful shutdown', '0.001') +
546
+ settingsField('Meeting Round Timeout', 'set-meetingRoundTimeout', (e.meetingRoundTimeout || 900000) / 1000, 's', 'Auto-advance meeting round after this', '0.001') +
547
+ settingsField('Steering Deferred Max', 'set-steeringDeferredMaxMs', (e.steeringDeferredMaxMs || 900000) / 1000, 's', 'Max wait for a runtime to emit a resumable checkpoint before a deferred steering message is flagged stranded. After this, the engine warns to live-output, marks _steeringStranded on the dispatch, and (when the steering store is present) sets store status=stranded. Default 15min; range 60s–4h.', '0.001') +
523
548
  settingsField('Steering Max Kill Retries', 'set-steeringMaxKillRetries', e.steeringMaxKillRetries ?? 3, '', 'Cap on graceful+escalation kill attempts after a steering kill is issued. Ladder waits 30s → 60s → 120s between attempts (last interval reused). Attempt 1 is graceful; attempts 2..cap are platform hard kills (taskkill /F /T on Windows; descendant-tree SIGKILL + pkill on Unix). Past cap, the engine gives up with a [steering-stuck] log + inbox notice. Default 3; range 1–5.') +
524
549
  '</div>' +
525
550
  '<h4 style="margin:14px 0 4px;font-size:var(--text-md)">Spawn-phase watchdog</h4>' +
@@ -528,7 +553,7 @@ async function openSettings() {
528
553
  settingsToggle('Spawn-phase Watchdog', 'set-spawnPhaseWatchdogEnabled', e.spawnPhaseWatchdogEnabled !== false, 'Enable the spawn-phase MCP-init wedge detector. Default ON. Turn OFF only if you suspect false positives against a runtime whose legitimate startup exceeds the grace window.') +
529
554
  '</div>' +
530
555
  '<div class="settings-grid-2">' +
531
- settingsField('Spawn-phase Grace', 'set-spawnPhaseGraceMs', e.spawnPhaseGraceMs || 120000, 'ms', 'Time after spawn before the watchdog can fire. Default 2min. A healthy startup ships a real event in seconds; this is generous headroom for slow MCP servers and corporate DNS.') +
556
+ settingsField('Spawn-phase Grace', 'set-spawnPhaseGraceMs', (e.spawnPhaseGraceMs || 120000) / 1000, 's', 'Time after spawn before the watchdog can fire. Default 2min. A healthy startup ships a real event in seconds; this is generous headroom for slow MCP servers and corporate DNS.', '0.001') +
532
557
  settingsField('Spawn-phase CPU floor', 'set-spawnPhaseMaxCpuSeconds', e.spawnPhaseMaxCpuSeconds ?? 5, 's', 'Process is "idle" if cumulative CPU seconds ≤ this. Default 5s. A genuinely busy startup burns CPU on tokenizer bootstrap, MCP IPC, or model warmup; a wedged child sits in epoll near 0s.') +
533
558
  '</div>';
534
559
 
@@ -536,26 +561,39 @@ async function openSettings() {
536
561
  '<h3>Worker Pool &amp; Worktrees</h3>' +
537
562
  '<div class="settings-pane-sub">Persistent CC worker pool and git worktree provisioning. Tune these only if you see worktree-create timeouts or slow CC cold-spawns.</div>' +
538
563
  '<div class="settings-stack" style="margin-bottom:12px">' +
539
- settingsToggle('CC Worker Pool', 'set-ccUseWorkerPool', (e.ccUseWorkerPool === undefined ? ((e.ccCli || e.defaultCli) === 'copilot') : !!e.ccUseWorkerPool), 'Route Command Center / doc-chat through a persistent copilot --acp worker per tab instead of spawning a fresh CLI per turn. Copilot-only (Agent Client Protocol transport); Claude does not implement ACP, so this toggle has no effect when CC runtime is Claude. Default ON for copilot (cold-spawn ~20s on Windows); forced OFF for non-copilot CC runtimes regardless of this toggle.') +
540
- settingsToggle('Agent Worker Pool', 'set-agentUseWorkerPool', e.agentUseWorkerPool === true, 'Route fleet Copilot dispatches through persistent copilot --acp workers. Default OFF while the newer fleet-pool lifecycle path stabilizes; runtimes without ACP support ignore this setting.') +
564
+ settingsToggle('CC Worker Pool', 'set-ccUseWorkerPool', (e.ccUseWorkerPool === undefined ? ((e.ccCli || e.defaultCli) === 'copilot') : !!e.ccUseWorkerPool), 'Route Command Center / doc-chat through a persistent copilot --acp worker per tab instead of spawning a fresh CLI per turn. Copilot-only (Agent Client Protocol transport); Claude does not implement ACP, so this toggle has no effect when CC runtime is Claude. Default ON for copilot (cold-spawn ~20s on Windows); forced OFF for non-copilot CC runtimes regardless of this toggle.', { blockHint: true }) +
565
+ settingsToggle('Agent Worker Pool', 'set-agentUseWorkerPool', e.agentUseWorkerPool === true, 'Route fleet Copilot dispatches through persistent copilot --acp workers. Default OFF while the newer fleet-pool lifecycle path stabilizes; runtimes without ACP support ignore this setting.', { blockHint: true }) +
541
566
  // W-mq6f2fe0000557fa — opt-in: auto-kill orphan spawn-agent processes
542
567
  // holding a stuck worktree's cwd. Gated by safe-reap criteria (cmdline
543
568
  // matches spawn-agent.js + basename + age > agentTimeout * 2). Default
544
569
  // OFF because killing a foreign process is destructive.
545
570
  settingsToggle('Auto-reap orphan worktree holders', 'set-autoReapOrphanWorktreeHolders', !!e.autoReapOrphanWorktreeHolders,
546
- 'When ON, the periodic orphan-worktree sweep automatically kills unambiguous orphan `spawn-agent.js` processes holding a stuck worktree\'s cwd (Windows file-lock pattern, e.g. W-mq1k8z6o003acd89 / PID 16828). Safe-reap criteria: cmdline must match spawn-agent.js, reference the worktree basename, AND process age must exceed `engine.agentTimeout * 2`. Default OFF because killing a foreign process is destructive. The holder SCAN runs either way and is appended to the escalation inbox note.') +
571
+ 'When ON, the periodic orphan-worktree sweep automatically kills unambiguous orphan `spawn-agent.js` processes holding a stuck worktree\'s cwd (Windows file-lock pattern, e.g. W-mq1k8z6o003acd89 / PID 16828). Safe-reap criteria: cmdline must match spawn-agent.js, reference the worktree basename, AND process age must exceed `engine.agentTimeout * 2`. Default OFF because killing a foreign process is destructive. The holder SCAN runs either way and is appended to the escalation inbox note.', { blockHint: true }) +
572
+ settingsToggle('Live-checkout auto-stash (fleet-wide)', 'set-liveCheckoutAutoStash', e.liveCheckoutAutoStash !== false,
573
+ 'Fleet-wide fallback for live-checkout mode (project.liveCheckoutAutoStash overrides per-project). When ON, a dirty live-checkout tree is `git stash push --include-untracked`\'d before dispatch instead of failing with LIVE_CHECKOUT_DIRTY, so dispatch proceeds without manual operator intervention. The engine NEVER pops the stash automatically — it logs the stash name and writes an inbox note so you can `git stash pop` manually. Default ON — non-destructive and recoverable via `git stash pop`; it is the first line of defense in dirty-tree recovery now that the destructive Live-checkout auto-reset defaults OFF.', { blockHint: true }) +
547
574
  settingsToggle('Live-checkout auto-reset (fleet-wide)', 'set-liveCheckoutAutoReset', !!e.liveCheckoutAutoReset,
548
- 'When ON, a dirty or broken live-checkout (in-place) tree is force-recovered before dispatch via `git fetch origin` + `git reset --hard origin/<branch>` instead of failing the dispatch with non-retryable LIVE_CHECKOUT_DIRTY. This is the FLEET-WIDE fallback; a per-project `liveCheckoutAutoReset` in config.json overrides it. Default ON — discarding disposable dirt keeps live-checkout branches aligned with origin instead of accumulating auto-save WIP commits; the reset is DESTRUCTIVE (it discards the operator\'s uncommitted changes in the live checkout, and a `live-checkout-autoreset-<workItem>` inbox note records exactly what was discarded for reflog recovery), so set it OFF for live checkouts whose local drift you want preserved. Only affects projects running in live/in-place checkout mode.') +
575
+ 'When ON, a dirty live-checkout (in-place) tree is destructively cleaned before dispatch via `git reset --hard HEAD` + `git clean -fd` instead of failing with LIVE_CHECKOUT_DIRTY. The current branch and committed history are preserved; tracked WIP and ordinary untracked files are discarded and may be unrecoverable. This is the FLEET-WIDE fallback; a per-project `liveCheckoutAutoReset` in config.json overrides it. Default OFF. Mid-operation and detached-HEAD states are always refused before cleanup, and auto-stash is attempted first when both policies are enabled. Only affects projects running in live/in-place checkout mode.', { blockHint: true }) +
576
+ settingsToggle('Live-checkout auto-base-repair (fleet-wide)', 'set-liveCheckoutAutoBaseRepair', !!e.liveCheckoutAutoBaseRepair,
577
+ 'When ON, a live-checkout (in-place) dispatch that would fail LIVE_CHECKOUT_WRONG_BASE — because HEAD sits on an unrelated topic/feature branch and no local base branch (`refs/heads/<mainBranch>`) exists to fork from — is auto-recovered by materializing a local base branch from the `origin/<mainBranch>` remote-tracking ref (a purely LOCAL ref: NO `git fetch`, so issue #226 is preserved) and forking cleanly off it. origin/<mainBranch> IS the correct base, so there is NO scope-contamination risk. This is the FLEET-WIDE fallback; a per-project `liveCheckoutAutoBaseRepair` in config.json overrides it. Bounded by the git timeout — if a partial-clone blob hydration ever hangs it degrades to exactly the pre-existing fail-closed refusal. Default OFF. Only affects projects running in live/in-place checkout mode.', { blockHint: true }) +
578
+ settingsToggle('Live-checkout auto-freshen (fleet-wide)', 'set-liveCheckoutAutoFreshen', !!e.liveCheckoutAutoFreshen,
579
+ 'When ON, a live-checkout (or hybrid liveValidation) dispatch that is about to fork a NEW branch off the local base branch fast-forwards that base to `origin/<mainBranch>` FIRST — but ONLY in the strictly-safe case: the tree is clean, HEAD is on the base branch, the local base has ZERO commits ahead of `origin/<mainBranch>` (NOT the stale-base-with-unpushed-commits case — that refusal is unchanged), and the local base IS behind origin. It runs `git fetch origin <mainBranch>` then a `--ff-only` update — the ONE sanctioned live-mode fetch (issue #226 otherwise forbids fetching live checkouts). A fast-forward can never rewrite history or lose local commits, never touches a dirty or mid-operation tree, and on any fetch/network/auth failure FAILS OPEN to the pre-existing (un-freshened) behavior. A `live-checkout-autofreshen-<workItem>` inbox note records the before/after SHA only when a freshen actually happens. This is the FLEET-WIDE fallback; a per-project `liveCheckoutAutoFreshen` in config.json overrides it. Default OFF. Only affects projects running in live/in-place (or hybrid liveValidation) checkout mode.', { blockHint: true }) +
580
+ // W-mr1qbj90000d30bd — explicit opt-out for the ACP idle reaper. When
581
+ // ON, the CC Session Idle Timeout field above is ignored and the
582
+ // engine persists the `0` sentinel, which cc-worker-pool.js maps to an
583
+ // Infinity window (only closeTab ever kills the warm worker).
584
+ settingsToggle('Never idle-reap CC workers (uses more memory for long-idle tabs)', 'set-ccNeverIdleReap', e.ccWorkerIdleTimeoutMs === 0,
585
+ 'When ON, a persistent `copilot --acp` CC worker is NEVER killed for inactivity alone — only closing the tab (closeTab) tears it down. Overrides CC Session Idle Timeout above. Trades idle memory/process footprint for guaranteed context durability on long-idle tabs. Default OFF.', { blockHint: true }) +
549
586
  '</div>' +
550
587
  '<div class="settings-grid-2">' +
551
- settingsField('CC Session Idle Timeout', 'set-ccWorkerIdleTimeoutMs', Math.round((e.ccWorkerIdleTimeoutMs || 1800000) / 60000), 'minutes', 'How long a persistent `copilot --acp` CC worker stays warm with no activity before the idle reaper kills it. After a reap the next message cold-spawns a FRESH session with no memory of prior turns and CC shows a "context cleared after inactivity" notice. Shorter = less idle memory/process footprint; longer = more context durability across gaps between messages. Only affects the Copilot worker-pool path (CC Worker Pool ON). Clamped 1–480 minutes.') +
552
- settingsField('Worktree Create Timeout', 'set-worktreeCreateTimeout', e.worktreeCreateTimeout || 300000, 'ms', 'Timeout for git worktree add (increase for large repos/Windows)') +
588
+ settingsField('CC Session Idle Timeout', 'set-ccWorkerIdleTimeoutMs', Math.round((e.ccWorkerIdleTimeoutMs || 1800000) / 60000), 'minutes', 'How long a persistent `copilot --acp` CC worker stays warm with no activity before the idle reaper kills it. After a reap the next message cold-spawns a FRESH session with no memory of prior turns and CC shows a "context cleared after inactivity" notice. Shorter = less idle memory/process footprint; longer = more context durability across gaps between messages. Only affects the Copilot worker-pool path (CC Worker Pool ON). Clamped 1–480 minutes. Ignored when "Never idle-reap CC workers" below is ON.') +
589
+ settingsField('Worktree Create Timeout', 'set-worktreeCreateTimeout', (e.worktreeCreateTimeout || 300000) / 1000, 's', 'Timeout for git worktree add (increase for large repos/Windows)', '0.001') +
553
590
  settingsField('Worktree Create Retries', 'set-worktreeCreateRetries', e.worktreeCreateRetries || 1, '', 'Retry count for transient worktree add failures (0-3)') +
554
591
  settingsField('Worktree Root', 'set-worktreeRoot', e.worktreeRoot || '../worktrees', '', 'Relative or absolute path for git worktrees; on Windows prefer a short path like C:\\wt') +
555
- settingsField('Assert-Clean Status Probe Timeout', 'set-assertCleanStatusTimeoutMs', e.assertCleanStatusTimeoutMs || 10000, 'ms', 'Timeout for the `git status --porcelain` preflight probe in assertCleanSharedWorktree. Raise this (e.g. 60000) on GVFS/Plastic-backed monorepos where sparse hydration runs longer than the 10s default. On timeout the engine quarantines the bad worktree and emits a RETRYABLE failure so the next dispatch starts fresh. Clamped 1000–120000ms.') +
556
- settingsField('Restart-health Timeout', 'set-restartHealthTimeoutMs', e.restartHealthTimeoutMs || 60000, 'ms', 'Cold-start budget for `minions restart` / post-update restart verification. The dashboard runs node:sqlite migrations + SPA HTML assembly + initial state reads synchronously BEFORE it binds its port, so right after `npm install` on a slow/loaded box the PID is alive but the port is unbound for tens of seconds. Below this budget that no longer prints a false "Restart verification failed" + exit(1) — the verifier keeps polling until the port listens. A genuinely crashed spawn (PID dies) still fails fast, well under the full budget. Clamped 15000300000ms.') +
557
- settingsField('Orphan-holder scan timeout', 'set-orphanHolderScanTimeoutMs', e.orphanHolderScanTimeoutMs || 5000, 'ms', 'Cap on the cross-platform scan (PowerShell / /proc walk / lsof) that identifies the OS process holding a stuck worktree\'s cwd. Bumps up only matter on heavily-loaded hosts where the holder scan races the sweep tick. Clamped 100030000ms.') +
558
- settingsField('Worktree-holder reap probe timeout', 'set-statusProbeKillTimeoutMs', e.statusProbeKillTimeoutMs || 12000, 'ms', 'Windows-only. Timeout for the pre-quarantine worktree-holder reap probe (legacy git.exe-cmdline sweep + PEB-CWD scan that kills an orphaned agent process tree whose CWD pins the worktree dir, causing EBUSY on rename AND `git worktree remove --force`). The old hardcoded 2000ms was the proximate cause of the reaper being a no-op under concurrent-agent load (PowerShell cold-start + process enumeration exceeded 2s → ETIMEDOUT → reaped nothing). ETIMEDOUT is always swallowed (reap nothing; quarantine still proceeds), so raising this only widens the success window. Clamped 200060000ms.') +
592
+ settingsField('Minimum Free Space', 'set-worktreeMinFreeSpaceGiB', Number(((e.worktreeMinFreeSpaceBytes ?? (10 * 1024 * 1024 * 1024)) / (1024 * 1024 * 1024)).toFixed(2)), 'GiB', 'Defer worktree-mode dispatch before git worktree add when the target filesystem has less free space than this reserve. The pending item is re-checked every tick and does not consume an agent retry. Set 0 to disable.') +
593
+ settingsField('Assert-Clean Status Probe Timeout', 'set-assertCleanStatusTimeoutMs', (e.assertCleanStatusTimeoutMs || 10000) / 1000, 's', 'Timeout for the `git status --porcelain` preflight probe in assertCleanSharedWorktree. Raise this (e.g. 60) on GVFS/Plastic-backed monorepos where sparse hydration runs longer than the 10s default. On timeout the engine quarantines the bad worktree and emits a RETRYABLE failure so the next dispatch starts fresh. Clamped 1120 s.', '0.001') +
594
+ settingsField('Restart-health Timeout', 'set-restartHealthTimeoutMs', (e.restartHealthTimeoutMs || 60000) / 1000, 's', 'Cold-start budget for `minions restart` / post-update restart verification. The dashboard runs node:sqlite migrations + SPA HTML assembly + initial state reads synchronously BEFORE it binds its port, so right after `npm install` on a slow/loaded box the PID is alive but the port is unbound for tens of seconds. Below this budget that no longer prints a false "Restart verification failed" + exit(1) — the verifier keeps polling until the port listens. A genuinely crashed spawn (PID dies) still fails fast, well under the full budget. Clamped 15300 s.', '0.001') +
595
+ settingsField('Orphan-holder scan timeout', 'set-orphanHolderScanTimeoutMs', (e.orphanHolderScanTimeoutMs || 5000) / 1000, 's', 'Cap on the cross-platform scan (PowerShell / /proc walk / lsof) that identifies the OS process holding a stuck worktree\'s cwd. Bumps up only matter on heavily-loaded hosts where the holder scan races the sweep tick. Clamped 130 s.', '0.001') +
596
+ settingsField('Worktree-holder reap probe timeout', 'set-statusProbeKillTimeoutMs', (e.statusProbeKillTimeoutMs || 12000) / 1000, 's', 'Windows-only. Timeout for the pre-quarantine worktree-holder reap probe (legacy git.exe-cmdline sweep + PEB-CWD scan that kills an orphaned agent process tree whose CWD pins the worktree dir, causing EBUSY on rename AND `git worktree remove --force`). The old hardcoded 2s was the proximate cause of the reaper being a no-op under concurrent-agent load (PowerShell cold-start + process enumeration exceeded 2s → ETIMEDOUT → reaped nothing). ETIMEDOUT is always swallowed (reap nothing; quarantine still proceeds), so raising this only widens the success window. Clamped 2–60 s.', '0.001') +
559
597
  '</div>';
560
598
 
561
599
  const paneCopilot =
@@ -661,7 +699,7 @@ async function openSettings() {
661
699
  displayFeatures.map(function(f) {
662
700
  const checked = f.enabled ? ' checked' : '';
663
701
  const expiredBadge = f.expired
664
- ? ' <span style="font-size:var(--text-xs);padding:1px 5px;background:rgba(220,80,80,0.15);color:var(--red);border-radius:3px;margin-left:4px">EXPIRED</span>'
702
+ ? ' <span style="font-size:var(--text-sm);padding:1px 5px;background:rgba(220,80,80,0.15);color:var(--red);border-radius:3px;margin-left:4px">EXPIRED</span>'
665
703
  : '';
666
704
  const meta = [];
667
705
  if (f.addedIn) meta.push('added in ' + escHtml(f.addedIn));
@@ -673,7 +711,7 @@ async function openSettings() {
673
711
  '<div style="flex:1">' +
674
712
  '<div style="font-size:var(--text-base);font-weight:600;color:var(--text)">' + escHtml(f.id) + expiredBadge + '</div>' +
675
713
  (f.description ? '<div style="font-size:var(--text-sm);color:var(--muted);margin-top:2px">' + escHtml(f.description) + '</div>' : '') +
676
- '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:3px">' + meta.join(' · ') + '</div>' +
714
+ '<div style="font-size:var(--text-sm);color:var(--muted);margin-top:3px">' + meta.join(' · ') + '</div>' +
677
715
  '</div>' +
678
716
  '</label>';
679
717
  }).join('') +
@@ -690,7 +728,7 @@ async function openSettings() {
690
728
  '<h4>Inbox</h4>' +
691
729
  '<div class="settings-grid-2">' +
692
730
  settingsField('Consolidation Threshold', 'set-inboxConsolidateThreshold', e.inboxConsolidateThreshold || 5, 'notes', 'Inbox notes before auto-consolidation') +
693
- settingsField('Version Check Interval', 'set-versionCheckInterval', e.versionCheckInterval || 3600000, 'ms', 'How often to check npm for updates (default: 1 hour)') +
731
+ settingsField('Version Check Interval', 'set-versionCheckInterval', (e.versionCheckInterval || 3600000) / 1000, 's', 'How often to check npm for updates (default: 1 hour)', '0.001') +
694
732
  '</div>' +
695
733
  '<h4>Agent Memory</h4>' +
696
734
  '<div class="settings-stack">' +
@@ -719,7 +757,7 @@ async function openSettings() {
719
757
  '<h4>Diagnostics</h4>' +
720
758
  '<div data-search="diagnostics refresh diag ring buffer status poll etag">' +
721
759
  '<button type="button" class="btn-add" onclick="window._openRefreshDiagModal && window._openRefreshDiagModal()">Open Refresh Diagnostics</button>' +
722
- '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:4px">Inspect the dashboard\'s /api/status refresh ring buffer (response status, ETag hits, render timings, per-section change flags) and optionally send a snapshot to the engine.</div>' +
760
+ '<div style="font-size:var(--text-sm);color:var(--muted);margin-top:4px">Inspect the dashboard\'s /api/status refresh ring buffer (response status, ETag hits, render timings, per-section change flags) and optionally send a snapshot to the engine.</div>' +
723
761
  '</div>';
724
762
 
725
763
  // Section registry — order is intentional (Runtime + Auto-fix surface first
@@ -1293,24 +1331,65 @@ async function loadModelsForAgent(agentId, runtimeName, currentValue) {
1293
1331
  cell.innerHTML = '<input ' + baseAttrs + ' list="' + escHtml(listId) + '" value="' + escHtml(currentValue || '') + '" placeholder="' + escHtml(runtimeName) + ' default" autocomplete="off" style="' + baseStyle + '"><datalist id="' + escHtml(listId) + '">' + opts + '</datalist>';
1294
1332
  }
1295
1333
 
1296
- function settingsToggle(label, id, checked, hint) {
1334
+ function settingsToggle(label, id, checked, hint, opts) {
1297
1335
  // data-search powers the cross-tab search box — lowercase label + hint so
1298
1336
  // the input filter can substring-match without dragging the visible text
1299
1337
  // through .toLowerCase() on every keystroke.
1300
1338
  const searchKey = (String(label || '') + ' ' + String(hint || '')).toLowerCase().replace(/"/g, '');
1301
- return '<div class="settings-row" data-search="' + escHtml(searchKey) + '" style="display:flex;align-items:center;gap:8px;padding:4px 0">' +
1302
- '<input type="checkbox" id="' + id + '"' + (checked ? ' checked' : '') + ' style="accent-color:var(--blue);width:16px;height:16px;cursor:pointer">' +
1339
+ // blockHint: render the hint on its own full-width line UNDER the
1340
+ // checkbox+label (indented to align beneath the label), mirroring the
1341
+ // label->control->hint block layout of settingsField. Opt-in per call — used
1342
+ // by panes whose toggles carry long, multi-sentence hints (e.g. Worker Pool
1343
+ // & Worktrees) where the default inline hint squeezes the label into a
1344
+ // narrow one-word-per-line column. Panes that omit opts are byte-identical
1345
+ // to the previous inline layout.
1346
+ const blockHint = !!(opts && opts.blockHint);
1347
+ const rowStyle = blockHint
1348
+ ? 'display:flex;flex-wrap:wrap;align-items:flex-start;gap:2px 8px;padding:4px 0'
1349
+ : 'display:flex;align-items:center;gap:8px;padding:4px 0';
1350
+ const hintHtml = hint
1351
+ ? (blockHint
1352
+ ? '<div style="flex-basis:100%;margin-left:24px;font-size:var(--text-sm);color:var(--muted);line-height:1.4">' + escHtml(hint) + '</div>'
1353
+ : '<span style="font-size:var(--text-sm);color:var(--muted)">' + escHtml(hint) + '</span>')
1354
+ : '';
1355
+ return '<div class="settings-row" data-search="' + escHtml(searchKey) + '" style="' + rowStyle + '">' +
1356
+ '<input type="checkbox" id="' + id + '"' + (checked ? ' checked' : '') + ' style="accent-color:var(--blue);width:16px;height:16px;cursor:pointer;flex-shrink:0">' +
1303
1357
  '<label for="' + id + '" style="font-size:var(--text-md);color:var(--text);cursor:pointer">' + escHtml(label) + '</label>' +
1304
- (hint ? '<span style="font-size:var(--text-xs);color:var(--muted)">' + escHtml(hint) + '</span>' : '') +
1358
+ hintHtml +
1305
1359
  '</div>';
1306
1360
  }
1307
1361
 
1308
- function settingsField(label, id, value, unit, hint) {
1362
+ // W-mryyzhug000454b7 enable/disabled TILE for a boolean per-cause auto-fix
1363
+ // gate. Renders a real (visually-hidden but focusable) `<input type=checkbox
1364
+ // id="set-...">` as the source of truth so the existing id-based save/collect
1365
+ // path (getElementById('set-...').checked) is untouched, wrapped in a <label>
1366
+ // so the whole tile is clickable and keyboard-accessible (Tab focuses the
1367
+ // checkbox, Space toggles). `icon` is a decorative emoji; `extraSearch` folds
1368
+ // the pre-rename label into data-search so the cross-tab search box still finds
1369
+ // the gate by its old name (e.g. "auto-fix builds").
1370
+ function settingsTile(label, id, checked, hint, icon, extraSearch) {
1371
+ const searchKey = (String(label || '') + ' ' + String(hint || '') + ' ' + String(extraSearch || '')).toLowerCase().replace(/"/g, '');
1372
+ return '<label class="settings-tile" data-search="' + escHtml(searchKey) + '" title="' + escHtml(hint || '') + '">' +
1373
+ '<input type="checkbox" id="' + id + '"' + (checked ? ' checked' : '') + ' class="settings-tile-input">' +
1374
+ '<span class="settings-tile-inner">' +
1375
+ '<span class="settings-tile-icon" aria-hidden="true">' + escHtml(icon || '') + '</span>' +
1376
+ '<span class="settings-tile-label">' + escHtml(label) + '</span>' +
1377
+ '<span class="settings-tile-state" aria-hidden="true"></span>' +
1378
+ '</span>' +
1379
+ '</label>';
1380
+ }
1381
+
1382
+ function settingsField(label, id, value, unit, hint, step) {
1309
1383
  const searchKey = (String(label || '') + ' ' + String(hint || '') + ' ' + String(unit || '')).toLowerCase().replace(/"/g, '');
1384
+ // `step` (optional) opts the input into type=number so fractional seconds
1385
+ // (e.g. a 1500ms value shown as 1.5 s) round-trip losslessly via the arrows.
1386
+ const stepAttr = (step !== undefined && step !== null && step !== '')
1387
+ ? ' type="number" step="' + escHtml(String(step)) + '"'
1388
+ : '';
1310
1389
  return '<div class="settings-row" data-search="' + escHtml(searchKey) + '">' +
1311
1390
  '<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">' + escHtml(label) + (unit ? ' <span style="opacity:0.6">(' + escHtml(unit) + ')</span>' : '') + '</label>' +
1312
- '<input id="' + id + '" value="' + escHtml(String(value)) + '" 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)">' +
1313
- (hint ? '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:1px">' + escHtml(hint) + '</div>' : '') +
1391
+ '<input id="' + id + '"' + stepAttr + ' value="' + escHtml(String(value)) + '" 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)">' +
1392
+ (hint ? '<div style="font-size:var(--text-sm);color:var(--muted);margin-top:1px">' + escHtml(hint) + '</div>' : '') +
1314
1393
  '</div>';
1315
1394
  }
1316
1395
 
@@ -1462,8 +1541,20 @@ async function saveSettings() {
1462
1541
  if (saveBtn) { saveBtn.disabled = true; saveBtn.style.opacity = '0.6'; saveBtn.innerHTML = 'Saving...'; }
1463
1542
 
1464
1543
  try {
1544
+ // Time fields are entered/displayed in SECONDS but the engine + config wire
1545
+ // format stays in MILLISECONDS. Convert s -> ms on save (render divides by
1546
+ // 1000). Missing element -> undefined so the server keeps the prior value;
1547
+ // blank/NaN/negative -> 0 so server-side range validation rejects the
1548
+ // complete save instead of silently retaining or adjusting a bad value.
1549
+ const secToMs = (id) => {
1550
+ const el = document.getElementById(id);
1551
+ if (!el) return undefined;
1552
+ const s = Number(el.value);
1553
+ if (!Number.isFinite(s) || s < 0) return 0;
1554
+ return Math.round(s * 1000);
1555
+ };
1465
1556
  const enginePayload = {
1466
- tickInterval: document.getElementById('set-tickInterval').value,
1557
+ tickInterval: secToMs('set-tickInterval'),
1467
1558
  maxConcurrent: document.getElementById('set-maxConcurrent').value,
1468
1559
  inboxConsolidateThreshold: document.getElementById('set-inboxConsolidateThreshold').value,
1469
1560
  memoryRetrievalShadowMode: document.getElementById('set-memoryRetrievalShadowMode').checked,
@@ -1471,22 +1562,23 @@ async function saveSettings() {
1471
1562
  memoryRetrievalTopK: document.getElementById('set-memoryRetrievalTopK').value,
1472
1563
  memoryRetrievalMaxBytes: document.getElementById('set-memoryRetrievalMaxBytes').value,
1473
1564
  memoryRetrievalCandidateLimit: document.getElementById('set-memoryRetrievalCandidateLimit').value,
1474
- agentTimeout: document.getElementById('set-agentTimeout').value,
1565
+ agentTimeout: secToMs('set-agentTimeout'),
1475
1566
  maxTurns: document.getElementById('set-maxTurns').value,
1476
- heartbeatTimeout: document.getElementById('set-heartbeatTimeout').value,
1477
- worktreeCreateTimeout: document.getElementById('set-worktreeCreateTimeout').value,
1567
+ heartbeatTimeout: secToMs('set-heartbeatTimeout'),
1568
+ worktreeCreateTimeout: secToMs('set-worktreeCreateTimeout'),
1478
1569
  worktreeCreateRetries: document.getElementById('set-worktreeCreateRetries').value,
1479
1570
  worktreeRoot: document.getElementById('set-worktreeRoot').value,
1480
- assertCleanStatusTimeoutMs: document.getElementById('set-assertCleanStatusTimeoutMs').value,
1481
- restartHealthTimeoutMs: document.getElementById('set-restartHealthTimeoutMs')?.value,
1571
+ worktreeMinFreeSpaceBytes: Math.round(Math.max(0, Number(document.getElementById('set-worktreeMinFreeSpaceGiB').value) || 0) * 1024 * 1024 * 1024),
1572
+ assertCleanStatusTimeoutMs: secToMs('set-assertCleanStatusTimeoutMs'),
1573
+ restartHealthTimeoutMs: secToMs('set-restartHealthTimeoutMs'),
1482
1574
  idleAlertMinutes: document.getElementById('set-idleAlertMinutes').value,
1483
- shutdownTimeout: document.getElementById('set-shutdownTimeout').value,
1484
- restartGracePeriod: document.getElementById('set-restartGracePeriod').value,
1485
- meetingRoundTimeout: document.getElementById('set-meetingRoundTimeout').value,
1486
- steeringDeferredMaxMs: document.getElementById('set-steeringDeferredMaxMs').value,
1575
+ shutdownTimeout: secToMs('set-shutdownTimeout'),
1576
+ restartGracePeriod: secToMs('set-restartGracePeriod'),
1577
+ meetingRoundTimeout: secToMs('set-meetingRoundTimeout'),
1578
+ steeringDeferredMaxMs: secToMs('set-steeringDeferredMaxMs'),
1487
1579
  steeringMaxKillRetries: document.getElementById('set-steeringMaxKillRetries').value,
1488
1580
  spawnPhaseWatchdogEnabled: document.getElementById('set-spawnPhaseWatchdogEnabled').checked,
1489
- spawnPhaseGraceMs: document.getElementById('set-spawnPhaseGraceMs').value,
1581
+ spawnPhaseGraceMs: secToMs('set-spawnPhaseGraceMs'),
1490
1582
  spawnPhaseMaxCpuSeconds: document.getElementById('set-spawnPhaseMaxCpuSeconds').value,
1491
1583
  operatorLogin: (document.getElementById('set-operatorLogin')?.value ?? '').trim(),
1492
1584
  autoApprovePlans: document.getElementById('set-autoApprovePlans').checked,
@@ -1497,6 +1589,7 @@ async function saveSettings() {
1497
1589
  autoConsolidateMemory: document.getElementById('set-autoConsolidateMemory').checked,
1498
1590
  bootstrapDefaultSchedules: document.getElementById('set-bootstrapDefaultSchedules').checked,
1499
1591
  autoApplyReviewVote: document.getElementById('set-autoApplyReviewVote').checked,
1592
+ createPrsAsDraft: document.getElementById('set-createPrsAsDraft').checked,
1500
1593
  autoFixBuilds: document.getElementById('set-autoFixBuilds').checked,
1501
1594
  autoFixConflicts: document.getElementById('set-autoFixConflicts').checked,
1502
1595
  autoFixPaused: document.getElementById('set-autoFixPaused').checked,
@@ -1505,6 +1598,7 @@ async function saveSettings() {
1505
1598
  autoFixReviewFeedback: document.getElementById('set-autoFixReviewFeedback').checked,
1506
1599
  autoFixHumanComments: document.getElementById('set-autoFixHumanComments').checked,
1507
1600
  autoCompletePrs: document.getElementById('set-autoCompletePrs').checked,
1601
+ includeHarnessSectionInPrBody: !!document.getElementById('set-includeHarnessSectionInPrBody')?.checked,
1508
1602
  ccUseWorkerPool: !!document.getElementById('set-ccUseWorkerPool')?.checked,
1509
1603
  agentUseWorkerPool: !!document.getElementById('set-agentUseWorkerPool')?.checked,
1510
1604
  autoReapOrphanWorktreeHolders: !!document.getElementById('set-autoReapOrphanWorktreeHolders')?.checked,
@@ -1512,8 +1606,8 @@ async function saveSettings() {
1512
1606
  liveCheckoutAutoReset: !!document.getElementById('set-liveCheckoutAutoReset')?.checked,
1513
1607
  liveCheckoutAutoBaseRepair: !!document.getElementById('set-liveCheckoutAutoBaseRepair')?.checked,
1514
1608
  liveCheckoutAutoFreshen: !!document.getElementById('set-liveCheckoutAutoFreshen')?.checked,
1515
- orphanHolderScanTimeoutMs: document.getElementById('set-orphanHolderScanTimeoutMs')?.value,
1516
- statusProbeKillTimeoutMs: document.getElementById('set-statusProbeKillTimeoutMs')?.value,
1609
+ orphanHolderScanTimeoutMs: secToMs('set-orphanHolderScanTimeoutMs'),
1610
+ statusProbeKillTimeoutMs: secToMs('set-statusProbeKillTimeoutMs'),
1517
1611
  adoPollEnabled: document.getElementById('set-adoPollEnabled').checked,
1518
1612
  ghPollEnabled: document.getElementById('set-ghPollEnabled').checked,
1519
1613
  pollingPaused: document.getElementById('set-pollingPaused').checked,
@@ -1541,10 +1635,10 @@ async function saveSettings() {
1541
1635
  enablePreDispatchEval: document.getElementById('set-enablePreDispatchEval').checked,
1542
1636
  preDispatchEvalSkipPrdSourced: document.getElementById('set-preDispatchEvalSkipPrdSourced').checked,
1543
1637
  preDispatchEvalConcurrency: document.getElementById('set-preDispatchEvalConcurrency').value,
1544
- agentBusyReassignMs: document.getElementById('set-agentBusyReassignMs').value,
1638
+ agentBusyReassignMs: secToMs('set-agentBusyReassignMs'),
1545
1639
  maxRetriesPerAgent: document.getElementById('set-maxRetriesPerAgent').value,
1546
1640
  ignoredCommentAuthors: document.getElementById('set-ignoredCommentAuthors').value,
1547
- versionCheckInterval: document.getElementById('set-versionCheckInterval').value,
1641
+ versionCheckInterval: secToMs('set-versionCheckInterval'),
1548
1642
  // Runtime fleet (P-7a5c1f8e). Empty strings are intentional — they signal
1549
1643
  // "clear this override". The server deletes the key from config.engine.
1550
1644
  defaultCli: (document.getElementById('set-defaultCli')?.value ?? '').trim(),
@@ -1553,10 +1647,10 @@ async function saveSettings() {
1553
1647
  ccModel: (document.getElementById('set-ccModel')?.value ?? '').trim(),
1554
1648
  ccEffort: document.getElementById('set-ccEffort').value || null,
1555
1649
  codexCcSandbox: document.getElementById('set-codexCcSandbox')?.value || 'danger-full-access',
1556
- ccTurnTimeoutMs: document.getElementById('set-ccTurnTimeoutMs')?.value,
1650
+ ccTurnTimeoutMs: secToMs('set-ccTurnTimeoutMs'),
1557
1651
  // Settings UI presents the idle timeout in minutes for readability; the
1558
1652
  // engine stores ms (ENGINE_DEFAULTS.ccWorkerIdleTimeoutMs). Convert here;
1559
- // the server clamps to [60000, 28800000] ms. undefined when blank so the
1653
+ // the server validates against [60000, 28800000] ms. undefined when blank so the
1560
1654
  // server keeps the prior value rather than zeroing it. W-mr1qbj90000d30bd:
1561
1655
  // the "Never idle-reap CC workers" toggle wins and submits the explicit
1562
1656
  // `0` sentinel (bypasses the clamp server-side), ignoring the minutes field.
@@ -1575,9 +1669,8 @@ async function saveSettings() {
1575
1669
  claudePreApproveWorkspaceMcps: !!document.getElementById('set-claudePreApproveWorkspaceMcps')?.checked,
1576
1670
  maxBudgetUsd: (document.getElementById('set-maxBudgetUsd')?.value ?? '').trim(),
1577
1671
  disableModelDiscovery: !!document.getElementById('set-disableModelDiscovery')?.checked,
1578
- qaDualWriteJson: !!document.getElementById('set-qaDualWriteJson')?.checked,
1579
1672
  // W-mpmwxkrw000872ec — global font-size scale. Allowlist validation
1580
- // (and clamp messaging) lives server-side in handleSettingsUpdate.
1673
+ // lives server-side in the atomic settings candidate builder.
1581
1674
  fontSize: document.getElementById('set-fontSize')?.value || 'small',
1582
1675
  maxTurnsByType: (function() {
1583
1676
  var mbt = {};
@@ -1662,6 +1755,13 @@ async function saveSettings() {
1662
1755
  const autoStashInput = document.getElementById('set-liveCheckoutAutoStash-' + p.name);
1663
1756
  const autoStashRaw = autoStashInput ? autoStashInput.value : '';
1664
1757
  const autoStashValue = autoStashRaw === 'on' ? true : autoStashRaw === 'off' ? false : null;
1758
+ // W-mrxv648b — per-project "Create PRs as draft" override. 'draft' → true;
1759
+ // 'ready' → false; '' → null (Inherit: clear the override so the engine
1760
+ // fleet-wide `engine.createPrsAsDraft` applies). The server deletes the
1761
+ // field on null and validates booleans.
1762
+ const createPrsAsDraftInput = document.getElementById('set-createPrsAsDraft-' + p.name);
1763
+ const createPrsAsDraftRaw = createPrsAsDraftInput ? createPrsAsDraftInput.value : '';
1764
+ const createPrsAsDraftValue = createPrsAsDraftRaw === 'draft' ? true : createPrsAsDraftRaw === 'ready' ? false : null;
1665
1765
  // W-mrchrfe100067d3a — parse the comma-separated scope list back into an
1666
1766
  // array; empty string → null (clears the override server-side).
1667
1767
  const additionalScopesInput = document.getElementById('set-additionalRepoScopes-' + p.name);
@@ -1673,6 +1773,7 @@ async function saveSettings() {
1673
1773
  checkoutMode: checkoutMode,
1674
1774
  liveValidation: liveValidation,
1675
1775
  liveCheckoutAutoStash: autoStashValue,
1776
+ createPrsAsDraft: createPrsAsDraftValue,
1676
1777
  additionalRepoScopes: additionalRepoScopes.length > 0 ? additionalRepoScopes : null,
1677
1778
  workSources: {
1678
1779
  pullRequests: { enabled: document.getElementById('set-ws-prs-' + p.name)?.checked ?? true },