@yemi33/minions 0.1.2105 → 0.1.2107

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.
@@ -402,8 +402,9 @@ function shortTime(t) {
402
402
  // pre-spawn state (worktree-setup, spawning, ready) without progressing for >2
403
403
  // minutes, the agent appears idle but is silently re-dispatching every tick.
404
404
  // Surface a STUCK chip so the operator notices instead of waiting for the
405
- // engine log. The threshold is 2 minutes — engine ticks every 60s, so 2 ticks
406
- // is enough to distinguish a slow spawn from a wedged one.
405
+ // engine log. Threshold is 2 wall-clock minutes — engine ticks every 10s
406
+ // (W-mpxpckey000laa4f), so a 2-minute floor distinguishes a slow spawn from
407
+ // a wedged one regardless of tick cadence.
407
408
  const _STUCK_PRE_SPAWN_STATES = new Set(['spawning', 'worktree-setup', 'ready']);
408
409
  const _STUCK_THRESHOLD_MS = 2 * 60 * 1000;
409
410
 
@@ -785,7 +785,7 @@ async function prdItemEdit(source, itemId) {
785
785
 
786
786
  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">' +
787
787
  '<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">' +
788
- '<span style="font-size:11px;font-weight:700;color:' + statusColor + '">' + statusLabel + '</span>' +
788
+ (wi ? '<a href="#" onclick="event.preventDefault();event.stopPropagation();openWorkItemDetail(\'' + escHtml(wi.id) + '\')" title="Open ' + escHtml(wi.id) + ' detail" style="font-size:11px;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:11px;font-weight:700;color:' + statusColor + '">' + statusLabel + '</span>') +
789
789
  (agent ? '<span style="font-size:11px;color:var(--muted)">by ' + escHtml(agent) + '</span>' : '') +
790
790
  (completedAt ? '<span style="font-size:10px;color:var(--muted)">' + escHtml(completedAt.slice(0, 16).replace('T', ' ')) + '</span>' : '') +
791
791
  '</div>' +
@@ -265,7 +265,7 @@ async function openSettings() {
265
265
  '<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>' +
266
266
  '<div class="settings-grid-2">' +
267
267
  settingsField('Max Concurrent Agents', 'set-maxConcurrent', e.maxConcurrent || 5, '', 'Max agents working simultaneously') +
268
- settingsField('Min Tick Interval', 'set-tickInterval', e.tickInterval || 60000, 'ms', 'Minimum gap between tick starts. Slow ticks (PR polls, reconcilers) may delay the next slot — see `running` indicator on /engine.') +
268
+ 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.') +
269
269
  settingsField('Agent Timeout', 'set-agentTimeout', e.agentTimeout || 18000000, 'ms', 'Kill agent after this duration') +
270
270
  settingsField('Heartbeat Timeout', 'set-heartbeatTimeout', e.heartbeatTimeout || 300000, 'ms', 'No output = dead after this') +
271
271
  settingsField('Idle Alert', 'set-idleAlertMinutes', e.idleAlertMinutes || 15, 'min', 'Alert after agent idle this long') +
@@ -1,131 +1,56 @@
1
1
  <section>
2
2
  <h2>QA</h2>
3
- <p class="empty" style="margin:4px 0 12px 0">Validation runbooks dispatched against live managed instances. Targets, runbooks, and run history with artifact previews.</p>
3
+ <p class="empty" style="margin:4px 0 12px 0">Passive monitor for QA sessions, live targets, runs, and saved runbooks. Create new sessions or runbooks from <button type="button" class="qa-inline-link" onclick="qaFocusCommandCenter()">Command Center</button>.</p>
4
4
  </section>
5
5
  <section id="qa-sessions-section" class="qa-section">
6
- <h2>QA Sessions <span class="qa-section-subtitle">natural-language smoke tests — engine sets up a live target, drafts a runner-native test, and (with your approval) executes it</span></h2>
7
- <div class="qa-session-form-wrap">
8
- <form id="qa-session-form" onsubmit="event.preventDefault();qaSubmitSessionForm();">
9
- <div class="qa-form-row qa-form-row--grid">
10
- <div>
11
- <label class="qa-form-label" for="qa-session-target-kind">Target</label>
12
- <select id="qa-session-target-kind" class="qa-form-input" onchange="qaSessionTargetKindChanged()">
13
- <option value="current">Current worktree</option>
14
- <option value="pr">Pull request</option>
15
- <option value="branch">Branch</option>
16
- <option value="commit">Commit SHA</option>
17
- </select>
18
- </div>
19
- <div>
20
- <label class="qa-form-label" for="qa-session-projects">Projects</label>
21
- <select id="qa-session-projects" class="qa-form-input" multiple size="3"></select>
22
- <p class="empty" style="margin-top:4px;font-size:0.85em">Hold Ctrl/Cmd to select multiple. First selected = primary (drives DRAFT/EXECUTE). Others = co-services (dev-up only). Leave empty = central.</p>
23
- </div>
24
- </div>
25
- <div class="qa-form-row qa-session-target-fields">
26
- <div id="qa-session-target-pr-wrap" class="qa-session-target-sub" style="display:none">
27
- <label class="qa-form-label" for="qa-session-target-pr-id">PR id</label>
28
- <input id="qa-session-target-pr-id" type="text" class="qa-form-input" placeholder="2887">
29
- </div>
30
- <div id="qa-session-target-branch-wrap" class="qa-session-target-sub" style="display:none">
31
- <label class="qa-form-label" for="qa-session-target-branch">Branch</label>
32
- <input id="qa-session-target-branch" type="text" class="qa-form-input" placeholder="work/my-feature">
33
- </div>
34
- <div id="qa-session-target-sha-wrap" class="qa-session-target-sub" style="display:none">
35
- <label class="qa-form-label" for="qa-session-target-sha">Commit SHA</label>
36
- <input id="qa-session-target-sha" type="text" class="qa-form-input" placeholder="abc1234">
37
- </div>
38
- <div id="qa-session-target-worktree-wrap" class="qa-session-target-sub">
39
- <label class="qa-form-label" for="qa-session-target-worktree">Worktree path</label>
40
- <input id="qa-session-target-worktree" type="text" class="qa-form-input" placeholder="(blank = $MINIONS_AGENT_CWD)">
41
- </div>
42
- </div>
43
- <div class="qa-form-row">
44
- <label class="qa-form-label" for="qa-session-flows">Flows</label>
45
- <textarea id="qa-session-flows" class="qa-form-input qa-form-textarea" placeholder="open the homepage, click login, verify the dashboard renders" required></textarea>
46
- </div>
47
- <div class="qa-form-row qa-form-row--inline">
48
- <div>
49
- <label class="qa-form-label" for="qa-session-mode">Mode</label>
50
- <select id="qa-session-mode" class="qa-form-input">
51
- <option value="confirm">Confirm (review draft first)</option>
52
- <option value="auto">Auto (skip approval)</option>
53
- </select>
54
- </div>
55
- <div>
56
- <label class="qa-form-label" for="qa-session-runner">Runner</label>
57
- <select id="qa-session-runner" class="qa-form-input">
58
- <option value="">Auto-detect</option>
59
- </select>
60
- </div>
61
- <div class="qa-session-capture-group">
62
- <label class="qa-form-label">Capture</label>
63
- <label class="qa-session-capture-item"><input id="qa-session-capture-screenshots" type="checkbox" checked> screenshots</label>
64
- <label class="qa-session-capture-item"><input id="qa-session-capture-video" type="checkbox"> video</label>
65
- <label class="qa-session-capture-item"><input id="qa-session-capture-logs" type="checkbox" checked> logs</label>
66
- </div>
67
- </div>
68
- <div class="qa-form-row qa-form-actions">
69
- <button type="submit" id="qa-session-submit" class="qa-btn-primary">Start QA Session</button>
70
- <span id="qa-session-form-msg" class="qa-form-msg"></span>
71
- </div>
72
- </form>
6
+ <h2>Active QA Sessions <span class="qa-section-subtitle">live, non-terminal runs — engine sets up a target, drafts a runner-native test, and (with your approval) executes it</span></h2>
7
+ <div id="qa-cc-pitch" class="qa-cc-pitch" style="border:1px solid var(--border);border-radius:var(--radius-md);background:var(--surface2);padding:var(--space-5);margin-bottom:var(--space-5);display:flex;flex-direction:column;gap:var(--space-3)">
8
+ <div style="font-weight:600;color:var(--text);display:flex;align-items:center;gap:var(--space-2)">💬 Ask Command Center to start a QA session</div>
9
+ <div style="color:var(--muted);font-size:var(--text-base);line-height:1.5">
10
+ Examples:
11
+ <ul style="margin:var(--space-2) 0 0 var(--space-5);padding:0;color:var(--muted)">
12
+ <li><code>QA the login flow on PR #1234</code></li>
13
+ <li><code>Smoke test the checkout flow on develop</code></li>
14
+ <li><code>Validate the signup journey on my current worktree</code></li>
15
+ </ul>
16
+ </div>
17
+ <div style="display:flex;gap:var(--space-3);align-items:center;margin-top:var(--space-2)">
18
+ <button type="button" id="qa-cc-pitch-open" class="qa-btn-primary" onclick="qaFocusCommandCenter()">Open Command Center →</button>
19
+ <a href="/state/docs/qa-runbook-lifecycle.md" target="_blank" rel="noopener" class="qa-cc-pitch-docs" style="color:var(--blue);font-size:var(--text-base)">Docs →</a>
20
+ </div>
73
21
  </div>
74
22
  <div id="qa-sessions-content" class="qa-sessions-list">
75
23
  <p class="empty">Loading sessions…</p>
76
24
  </div>
25
+ <details id="qa-sessions-recent" class="qa-disclosure" style="margin-top:var(--space-5);border-top:1px solid var(--border);padding-top:var(--space-4)">
26
+ <summary id="qa-sessions-recent-summary" style="cursor:pointer;font-size:var(--text-base);color:var(--muted)">Recent sessions (last 10)</summary>
27
+ <div id="qa-sessions-recent-content" class="qa-sessions-list" style="margin-top:var(--space-3)">
28
+ <p class="empty">No recent sessions.</p>
29
+ </div>
30
+ </details>
77
31
  </section>
78
32
  <section id="qa-targets-section" class="qa-section">
79
- <h2>Targets <span class="qa-section-subtitle">live managed/keep processes available as runbook targets — full inventory lives on <a href="/engine" class="qa-engine-link">/engine</a></span></h2>
33
+ <h2>Live Targets <span class="qa-section-subtitle">managed/keep processes available as runbook targets — full inventory on <a href="/engine" class="qa-engine-link">/engine</a></span></h2>
80
34
  <div id="qa-targets-content" class="qa-targets-list">
81
35
  <p class="empty">Loading targets…</p>
82
36
  </div>
83
37
  </section>
84
- <section id="qa-runbooks-section" class="qa-section">
85
- <h2>Validation Runbooks <span class="qa-section-subtitle">human or agent-driven smoke / E2E flows against the targets above</span></h2>
86
- <div class="qa-runbooks-actions">
87
- <button id="qa-new-runbook-btn" class="qa-btn-primary" onclick="qaShowNewRunbookForm()">+ New runbook</button>
88
- </div>
89
- <div id="qa-runbook-form-wrap" class="qa-runbook-form" style="display:none">
90
- <form id="qa-runbook-form" onsubmit="event.preventDefault();qaSaveRunbook();">
91
- <div class="qa-form-row">
92
- <label class="qa-form-label" for="qa-runbook-name">Name</label>
93
- <input id="qa-runbook-name" type="text" class="qa-form-input" placeholder="login-smoke" required>
94
- </div>
95
- <div class="qa-form-row">
96
- <label class="qa-form-label" for="qa-runbook-target">Target</label>
97
- <select id="qa-runbook-target" class="qa-form-input" required>
98
- <option value="">— select a target —</option>
99
- </select>
100
- </div>
101
- <div class="qa-form-row">
102
- <label class="qa-form-label" for="qa-runbook-steps">Steps</label>
103
- <textarea id="qa-runbook-steps" class="qa-form-input qa-form-textarea" placeholder="1. Open the app&#10;2. Click login&#10;3. Verify dashboard loads" required></textarea>
104
- </div>
105
- <div class="qa-form-row">
106
- <label class="qa-form-label">Expected artifacts</label>
107
- <div id="qa-runbook-artifacts" class="qa-artifacts-repeater">
108
- <div class="qa-artifact-row">
109
- <input type="text" class="qa-form-input qa-artifact-input" placeholder="screenshot:dashboard.png">
110
- <button type="button" class="qa-btn-ghost" onclick="qaRemoveArtifactRow(this)">−</button>
111
- </div>
112
- </div>
113
- <button type="button" class="qa-btn-ghost qa-add-artifact-btn" onclick="qaAddArtifactRow()">+ Add artifact</button>
114
- </div>
115
- <div class="qa-form-row qa-form-actions">
116
- <button type="submit" class="qa-btn-primary">Save</button>
117
- <button type="button" class="qa-btn-ghost" onclick="qaHideNewRunbookForm()">Cancel</button>
118
- <span id="qa-runbook-form-msg" class="qa-form-msg"></span>
119
- </div>
120
- </form>
121
- </div>
122
- <div id="qa-runbooks-content" class="qa-runbooks-list">
123
- <p class="empty">Loading runbooks…</p>
124
- </div>
125
- </section>
126
38
  <section id="qa-runs-section" class="qa-section">
127
- <h2>Recent Runs <span class="qa-section-subtitle">latest 50 dispatched validation runs — polled every 5s while this page is active</span></h2>
39
+ <h2>Recent Runs <span class="qa-section-subtitle">latest 20 validation runs — polled every 5s while this page is active</span></h2>
128
40
  <div id="qa-runs-content" class="qa-runs-list">
129
41
  <p class="empty">Loading runs…</p>
130
42
  </div>
131
43
  </section>
44
+ <section id="qa-runbooks-section" class="qa-section">
45
+ <details id="qa-runbooks-disclosure" class="qa-disclosure">
46
+ <summary id="qa-runbooks-disclosure-summary" style="cursor:pointer;font-size:var(--text-md);font-weight:600;color:var(--text)">Show saved runbooks (0)</summary>
47
+ <div style="margin-top:var(--space-4);display:flex;flex-direction:column;gap:var(--space-3)">
48
+ <div class="qa-cc-pitch qa-cc-pitch--small" style="border:1px solid var(--border);border-radius:var(--radius-md);background:var(--surface2);padding:var(--space-4);color:var(--muted);font-size:var(--text-base)">
49
+ 💬 Ask Command Center to create a new runbook — e.g. <code>save a runbook that opens the cart and clicks checkout</code>. <button type="button" class="qa-inline-link" onclick="qaFocusCommandCenter()">Open Command Center →</button>
50
+ </div>
51
+ <div id="qa-runbooks-content" class="qa-runbooks-list">
52
+ <p class="empty">Loading runbooks…</p>
53
+ </div>
54
+ </div>
55
+ </details>
56
+ </section>
package/dashboard.js CHANGED
@@ -5109,10 +5109,13 @@ const server = http.createServer(async (req, res) => {
5109
5109
  // Create new work item from PRD item definition (same logic as materializePlansAsWorkItems)
5110
5110
  const complexity = prdItem.estimated_complexity || 'medium';
5111
5111
  const criteria = (prdItem.acceptance_criteria || []).map(c => `- ${c}`).join('\n');
5112
+ // W-mpxqkkn300121d21 — mirror the engine materializer's resolution so a
5113
+ // dashboard retry honors structured `type` / prose `Type:` exactly the
5114
+ // same way the engine sweep does.
5112
5115
  const newItem = {
5113
5116
  id,
5114
5117
  title: `Implement: ${prdItem.name}`,
5115
- type: complexity === 'large' ? 'implement:large' : 'implement',
5118
+ type: shared.resolveWorkItemTypeFromPrdItem(prdItem),
5116
5119
  priority: prdItem.priority || 'medium',
5117
5120
  description: `${prdItem.description || ''}\n\n**Plan:** ${prdFile}\n**Plan Item:** ${prdItem.id}\n**Complexity:** ${complexity}${criteria ? '\n\n**Acceptance Criteria:**\n' + criteria : ''}`,
5118
5121
  status: WI_STATUS.PENDING,
@@ -7996,6 +7999,79 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7996
7999
  } catch (e) { _releaseCCTab(tabId); return jsonReply(res, e.statusCode || 500, { error: e.message, code: 'handler-exception', retriable: false }); }
7997
8000
  }
7998
8001
 
8002
+ // W-mpxq9sjq000z794b — Watch-driven CC invocation. Internal-only endpoint
8003
+ // wired from engine/watch-actions.js#cc-triage handler. Behavior diff vs.
8004
+ // /api/command-center:
8005
+ // - No session resume / no session persist (each call is a fresh CC turn).
8006
+ // This isolation is the whole point — a watch firing must not pollute
8007
+ // the user's interactive ccSession or surface in their tab list.
8008
+ // - Separate rate-limit lane (`cc-triage`) so a noisy watch can't starve
8009
+ // the user's CC bucket (or vice-versa).
8010
+ // - Uses CC_STATIC_SYSTEM_PROMPT verbatim (no per-turn header) and
8011
+ // prepends the standard buildCCStatePreamble so CC can read live state.
8012
+ // - Reasoning effort can be overridden per-call (defaults to engine.ccEffort).
8013
+ async function handleCommandCenterTriage(req, res) {
8014
+ if (checkRateLimit('cc-triage', 10)) return jsonReply(res, 429, { error: 'Rate limited — max 10 cc-triage requests/minute' });
8015
+ try {
8016
+ const body = await readBody(req);
8017
+ const message = body && typeof body.message === 'string' ? body.message : '';
8018
+ if (!message.trim()) return jsonReply(res, 400, { error: 'message required' });
8019
+ const watchId = body.watchId ? String(body.watchId) : '';
8020
+ const label = watchId ? `watch-cc-triage:${watchId}` : 'watch-cc-triage';
8021
+
8022
+ const model = CONFIG.engine?.ccModel || shared.ENGINE_DEFAULTS.ccModel;
8023
+ const maxTurns = CONFIG.engine?.ccMaxTurns || shared.ENGINE_DEFAULTS.ccMaxTurns;
8024
+ const engineEffort = CONFIG.engine?.ccEffort || shared.ENGINE_DEFAULTS.ccEffort;
8025
+ const requestedEffort = typeof body.effort === 'string' && body.effort.trim() ? body.effort.trim().toLowerCase() : '';
8026
+ const effort = ['low', 'medium', 'high'].includes(requestedEffort) ? requestedEffort : engineEffort;
8027
+
8028
+ const preflight = await _preflightModelCheck({ model, engineConfig: CONFIG.engine });
8029
+ if (preflight) {
8030
+ console.warn(`[${label}] Pre-flight rejected: ${preflight.errorMessage}`);
8031
+ return jsonReply(res, 503, {
8032
+ error: preflight.errorMessage || 'CC runtime unavailable',
8033
+ code: preflight.errorClass || 'runtime-unavailable',
8034
+ retriable: false,
8035
+ });
8036
+ }
8037
+
8038
+ const preamble = `## Current Minions State (${new Date().toISOString().slice(0, 16)})\n\n${buildCCStatePreamble()}`;
8039
+ const fullPrompt = `${preamble}\n\n---\n\n${message}`;
8040
+
8041
+ const result = await llm.callLLM(fullPrompt, CC_STATIC_SYSTEM_PROMPT, {
8042
+ timeout: CC_CALL_TIMEOUT_MS,
8043
+ label, model, maxTurns,
8044
+ allowedTools: 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch',
8045
+ effort, direct: true,
8046
+ engineConfig: CONFIG.engine,
8047
+ });
8048
+ llm.trackEngineUsage(label, result.usage);
8049
+
8050
+ if (!result.text || result.error) {
8051
+ const errEnvelope = result.error || (result.errorMessage
8052
+ ? { message: result.errorMessage, code: result.errorClass || 'unknown', retriable: true }
8053
+ : { message: 'cc-triage returned no output', code: 'empty-output', retriable: true });
8054
+ llm.trackEngineError(label, errEnvelope.code);
8055
+ const status = result.missingRuntime ? 503
8056
+ : errEnvelope.code === 'auth-failure' ? 503
8057
+ : 502;
8058
+ return jsonReply(res, status, {
8059
+ error: errEnvelope.message,
8060
+ code: errEnvelope.code,
8061
+ retriable: !!errEnvelope.retriable,
8062
+ });
8063
+ }
8064
+
8065
+ return jsonReply(res, 200, {
8066
+ text: result.text,
8067
+ sessionId: result.sessionId || null,
8068
+ usage: result.usage || null,
8069
+ });
8070
+ } catch (e) {
8071
+ return jsonReply(res, e.statusCode || 500, { error: e.message, code: 'handler-exception', retriable: false });
8072
+ }
8073
+ }
8074
+
7999
8075
  /** Build a lightweight input object for SSE tool events — keeps only the fields formatToolSummary needs, with truncated string values. */
8000
8076
  function _lightToolInput(input) {
8001
8077
  if (!input || typeof input !== 'object') return {};
@@ -10484,6 +10560,37 @@ What would you like to discuss or change? When you're happy, say "approve" and I
10484
10560
  }
10485
10561
  }
10486
10562
 
10563
+ // GET /api/qa/sessions/<id>/test — read-only fetch of the drafted test file
10564
+ // for the Review draft modal (W-mpxpvpwn000ra419). Sandboxed at two layers:
10565
+ // the per-segment _qaIsSafeSegment guard rejects hostile ids before they
10566
+ // reach qaSessions, and qaSessions.getSessionTestFile() repeats the path
10567
+ // resolution + prefix check internally so anything that slips through
10568
+ // (e.g. a session record whose testFile field already escapes the sandbox)
10569
+ // still returns null instead of leaking bytes. Read-only, so no CSRF gate.
10570
+ function handleQaSessionTestFile(req, res, match) {
10571
+ try {
10572
+ const qaSessions = require('./engine/qa-sessions');
10573
+ const id = decodeURIComponent(match[1] || '');
10574
+ if (!_qaIsSafeSegment(id)) return jsonReply(res, 400, { error: 'invalid session id' }, req);
10575
+ const session = qaSessions.getSession(id);
10576
+ if (!session) return jsonReply(res, 404, { error: 'qa session not found' }, req);
10577
+ const result = qaSessions.getSessionTestFile(id);
10578
+ if (!result) {
10579
+ return jsonReply(res, 404, { error: 'qa session test file not available' }, req);
10580
+ }
10581
+ return jsonReply(res, 200, {
10582
+ ok: true,
10583
+ path: session.testFile,
10584
+ content: result.content,
10585
+ language: result.language,
10586
+ truncated: !!result.truncated,
10587
+ size: result.size,
10588
+ }, req);
10589
+ } catch (e) {
10590
+ return jsonReply(res, 500, { error: e.message }, req);
10591
+ }
10592
+ }
10593
+
10487
10594
  // Shared helper for approve + edit + cancel + kill + dismiss — each takes
10488
10595
  // a sessionId from the URL and a body, delegates to qaSessions.* and maps
10489
10596
  // module errors to HTTP statuses.
@@ -10964,6 +11071,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
10964
11071
  { method: 'POST', path: /^\/api\/qa\/sessions\/([^/?]+)\/cancel$/, template: '/api/qa/sessions/<id>/cancel', desc: 'Cancel a session (non-terminal → killed). Does NOT touch the managed-spawn — use /kill for that.', params: 'reason?', handler: handleQaSessionCancel },
10965
11072
  { method: 'POST', path: /^\/api\/qa\/sessions\/([^/?]+)\/kill$/, template: '/api/qa/sessions/<id>/kill', desc: 'Kill a session and its managed-spawn (non-terminal → killed). Best-effort on the spawn kill.', params: 'reason?', handler: handleQaSessionKill },
10966
11073
  { method: 'POST', path: /^\/api\/qa\/sessions\/([^/?]+)\/dismiss$/, template: '/api/qa/sessions/<id>/dismiss', desc: 'Mark a session done without running EXECUTE (non-terminal → done).', params: 'summary?', handler: handleQaSessionDismiss },
11074
+ { method: 'GET', path: /^\/api\/qa\/sessions\/([^/?]+)\/test$/, template: '/api/qa/sessions/<id>/test', desc: 'Fetch the drafted test file for the Review draft modal. Read-only; returns { ok, path, content, language, truncated, size }. Files > 256KB return truncated:true with content:null.', handler: handleQaSessionTestFile },
10967
11075
  { method: 'GET', path: /^\/api\/qa\/sessions\/([^/?]+)$/, template: '/api/qa/sessions/<id>', desc: 'Fetch a single QA session record by id.', handler: handleQaSessionsById },
10968
11076
  // QA Runners endpoints (P-d2f5a8c9). Pluggable runner-adapter registry.
10969
11077
  { method: 'GET', path: '/api/qa/runners', desc: 'List registered QA runner adapters (built-ins + qa-runners.d/ plugins). Returns metadata only (no hooks).', handler: handleQaRunnersList },
@@ -11405,6 +11513,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
11405
11513
  { method: 'POST', path: '/api/command-center/new-session', desc: 'Clear active CC session', handler: handleCommandCenterNewSession },
11406
11514
  { method: 'POST', path: '/api/command-center/abort', desc: 'Abort an in-flight CC request for a tab', params: 'tabId?', handler: handleCommandCenterAbort },
11407
11515
  { method: 'POST', path: '/api/command-center', desc: 'Conversational command center with full minions context', params: 'message, tabId?, sessionId?, transcript?', handler: handleCommandCenter },
11516
+ { method: 'POST', path: '/api/command-center/triage', desc: 'Headless CC invocation for watch-driven triage (internal). Isolated from user ccSession.', params: 'message, watchId?, effort?', handler: handleCommandCenterTriage },
11408
11517
  { method: 'POST', path: '/api/command-center/stream', desc: 'Streaming CC — SSE with text chunks as they arrive', params: 'message, tabId?, sessionId?, transcript?', handler: handleCommandCenterStream },
11409
11518
  { method: 'GET', path: '/api/cc-sessions', desc: 'List CC session metadata for all tabs', handler: handleCCSessionsList },
11410
11519
  { method: 'POST', path: '/api/cc-sessions/warm', desc: 'Pre-warm the worker pool for a CC tab (process + MCP init, no LLM call). No-op when pool is off.', params: 'tabId', handler: handleCcSessionWarm },
@@ -6,7 +6,7 @@ How the minions engine finds work and dispatches agents automatically.
6
6
 
7
7
  ## The Tick Loop
8
8
 
9
- The engine runs a tick every 60 seconds (configurable via `config.json` → `engine.tickInterval`). Each tick:
9
+ The engine runs a tick every 10 seconds (configurable via `config.json` → `engine.tickInterval`). Each tick:
10
10
 
11
11
  ```
12
12
  tick()
@@ -17,16 +17,16 @@ tick()
17
17
  2. consolidateInbox() Merge learnings into notes.md (Haiku-powered)
18
18
  2.1 autoSweepKb() Periodic KB sweep (opt-in via engine.autoConsolidateMemory, 4h cadence)
19
19
  2.5 runCleanup() Periodic cleanup (every 10 ticks ≈ 10min)
20
- 2.52 sweepKeepProcesses() keep_processes TTL/dead-PID sweep (every 30 ticks)
21
- 2.53 sweepManagedSpawn() managed_spawn TTL/dead-PID/log-rotate sweep (every 30 ticks)
22
- 2.55 checkWatches() Persistent watch jobs (every 3 tick-equivalents)
20
+ 2.52 sweepKeepProcesses() keep_processes TTL/dead-PID sweep (every 180 ticks)
21
+ 2.53 sweepManagedSpawn() managed_spawn TTL/dead-PID/log-rotate sweep (every 180 ticks)
22
+ 2.55 checkWatches() Persistent watch jobs (every 18 tick-equivalents)
23
23
  2.6 pollPrStatus() Poll ADO + GitHub for build, review, merge status (wall-clock cadence from prPollStatusEvery × tickInterval, default ≈ 12min)
24
24
  processPendingRebases() Run any rebase work queued from the previous tick
25
25
  syncPrdFromPrs() Backfill PRD item status from active PRs
26
26
  checkPlanCompletion() Mark plans completed when all features done/in-pr
27
27
  2.7 pollPrHumanComments() Poll PR threads for human comments (wall-clock cadence from prPollCommentsEvery × tickInterval, default ≈ 12min)
28
28
  reconcilePrs() (ADO+GH) Reconciliation sweep (runs regardless of poll flags)
29
- 2.9 stallRecovery() Auto-retry failed items blocking pending deps (every 20 ticks)
29
+ 2.9 stallRecovery() Auto-retry failed items blocking pending deps (every 120 ticks)
30
30
  3a. pruneStalePrDispatches() Clear pending PR dispatches whose underlying PRs no longer warrant action
31
31
  3. discoverWork() Scan ALL linked projects for new tasks
32
32
  4. updateSnapshot() Write identity/now.md
@@ -141,7 +141,7 @@ Both write to `work-items.json` and are picked up by Source 3 on the same or nex
141
141
 
142
142
  ## PR Status Polling (`pollPrStatus`)
143
143
 
144
- **Runs:** On a wall-clock cadence derived from `prPollStatusEvery × engine.tickInterval` (default 12 × 60s, ≈ 12 minutes), independently of work discovery or file-change wakeups. ADO polling lives in `engine/ado.js`; GitHub polling lives in `engine/github.js` — both run in parallel each cycle (`Promise.allSettled`) and write to the same per-project `pull-requests.json` schema. Replaces the retired agent-based `pr-sync`.
144
+ **Runs:** On a wall-clock cadence derived from `prPollStatusEvery × engine.tickInterval` (default 72 × 10s, ≈ 12 minutes), independently of work discovery or file-change wakeups. ADO polling lives in `engine/ado.js`; GitHub polling lives in `engine/github.js` — both run in parallel each cycle (`Promise.allSettled`) and write to the same per-project `pull-requests.json` schema. Replaces the retired agent-based `pr-sync`.
145
145
 
146
146
  The engine directly polls the host REST API for **all** PR metadata: build/CI status, human review votes, and completion state. Two API calls per PR — no agent dispatch needed.
147
147
 
@@ -413,7 +413,7 @@ All discovery behavior is controlled via `config.json`:
413
413
  ```json
414
414
  {
415
415
  "engine": {
416
- "tickInterval": 60000, // ms between ticks
416
+ "tickInterval": 10000, // ms between ticks
417
417
  "maxConcurrent": 5, // max agents running at once
418
418
  "agentTimeout": 18000000, // 5 hours — hard runtime limit
419
419
  "heartbeatTimeout": 300000, // 5min — stale-orphan grace after process tracking is lost
@@ -43,7 +43,7 @@ This is the only point where you decide if the *quality* is good enough.
43
43
 
44
44
  These run continuously without you:
45
45
 
46
- - **Work discovery** — engine scans all project queues every tick (~60s)
46
+ - **Work discovery** — engine scans all project queues every tick (~10s)
47
47
  - **Agent dispatch** — engine picks the right agent, builds the prompt, spawns Claude
48
48
  - **Worktree management** — create on dispatch, pull on shared-branch, clean after merge
49
49
  - **PR status polling** — checks ADO + GitHub for build status, review votes, merge state every ~12 min
@@ -94,7 +94,7 @@ These are entirely on you:
94
94
 
95
95
  If you start the engine and dashboard, then leave:
96
96
 
97
- 1. Engine ticks every 60 seconds
97
+ 1. Engine ticks every 10 seconds
98
98
  2. Discovers pending work items, PRD gaps, PR reviews needed
99
99
  3. Dispatches agents (up to max concurrent)
100
100
  4. Agents create worktrees, write code, create PRs
package/docs/index.html CHANGED
@@ -250,7 +250,7 @@ minions update</pre>
250
250
  +-----------------------------------------------------+
251
251
  | ~/.minions/ (central hub) |
252
252
  | |
253
- | engine.js ---- 60s tick loop ----+ |
253
+ | engine.js ---- 10s tick loop ----+ |
254
254
  | | | |
255
255
  | discover work spawn agents poll PRs/comments|
256
256
  | | | | |
@@ -211,7 +211,7 @@ All knobs live under `engine.managedSpawn` in `engine/shared.js:1500` (`ENGINE_D
211
211
  | `maxSpecsPerFile` | `5` | Per-agent cap. |
212
212
  | `maxTtlMinutes` | `1440` | Hard cap (24h). |
213
213
  | `defaultTtlMinutes` | `720` | Fallback when `ttl_minutes` omitted (12h). |
214
- | `sweepEvery` | `30` | Ticks between sweeps. Default tick = 60s ⇒ ~30 min. |
214
+ | `sweepEvery` | `180` | Ticks between sweeps. Default tick = 10s ⇒ ~30 min. |
215
215
  | `defaultHealthIntervalSec` | `1` | Healthcheck cadence pre-first-healthy. |
216
216
  | `healthBackoffSec` | `30` | Healthcheck cadence post-first-healthy. |
217
217
  | `logRotateBytes` | `10485760` | Rotation threshold for `<name>.log`. |
@@ -133,7 +133,7 @@ A quick tour of the panels you will use most often during onboarding:
133
133
  | **Pull Requests** | Cached PR status (build, review, merge) | Track the PRs your agents create |
134
134
  | **Engine** | Dispatch queue, engine log, LLM call performance | Diagnose timing / token issues |
135
135
 
136
- The engine ticks every 60 seconds. If the dashboard says "Engine: stopped", run `minions restart` again — the dashboard auto-restarts a dead engine on its next health check, but a manual restart is faster.
136
+ The engine ticks every 10 seconds. If the dashboard says "Engine: stopped", run `minions restart` again — the dashboard auto-restarts a dead engine on its next health check, but a manual restart is faster.
137
137
 
138
138
  ---
139
139
 
@@ -148,7 +148,7 @@ The fastest way to give Minions a task is the Command Center (CC).
148
148
  ```
149
149
  3. CC will propose an action (usually `POST /api/work-items` or `POST /api/plan`). Confirm.
150
150
 
151
- Within one tick (≤60 s), the engine will:
151
+ Within one tick (≤10 s), the engine will:
152
152
 
153
153
  1. Pick an idle agent that matches the work type (per `routing.md`).
154
154
  2. Create a git worktree for that agent under `<your-repo>/.worktrees/<work-item-id>/`.
@@ -9,7 +9,7 @@ How the engine manages the lifecycle of a PR from creation through review, fix,
9
9
 
10
10
  ## 2. Engine discovers PR needs review
11
11
 
12
- - `discoverFromPrs()` (engine.js) runs each tick (~60s)
12
+ - `discoverFromPrs()` (engine.js) runs each tick (~10s)
13
13
  - Gates: `status === 'active'` + `reviewStatus === 'pending'` + not reviewed since last push + not dispatched + not on cooldown
14
14
  - Pre-dispatch: `checkLiveReviewStatus()` hits GitHub/ADO API to catch stale cached status
15
15
  - Routes to reviewer via `resolveAgent('review')`, dispatches with `review.md` playbook
@@ -24,7 +24,7 @@ Agent completes task
24
24
  ```
25
25
  Agent finishes task
26
26
  → writes notes/inbox/<agent>-<date>.md
27
- → engine checks inbox on next tick (~60s)
27
+ → engine checks inbox on next tick (~10s)
28
28
  → consolidateInbox() fires (threshold: 5 files)
29
29
  → spawns Claude Haiku for LLM-powered summarization
30
30
  → Haiku reads all inbox notes + existing notes.md
@@ -302,7 +302,7 @@ consolidate-now button.
302
302
  ## 9. Schedule (cron)
303
303
 
304
304
  **What it is.** A recurring trigger that creates a Work Item on a cron pattern. The engine ticks
305
- every 60 s; if a schedule's last-run was ≥ its interval ago and the cron pattern matches, the
305
+ every 10 s; if a schedule's last-run was ≥ its interval ago and the cron pattern matches, the
306
306
  engine drops a Work Item into the queue.
307
307
 
308
308
  **Where it lives.**
@@ -631,7 +631,7 @@ Skill is unrelated; skills are prompt-level, MCPs are tool-level.
631
631
 
632
632
  ## 20. Engine
633
633
 
634
- **What it is.** The orchestrator daemon. One Node process; runs a 60 s tick that drains the
634
+ **What it is.** The orchestrator daemon. One Node process; runs a 10 s tick that drains the
635
635
  **Dispatch Queue**, polls **PRs**, evaluates **Watches** every 3 ticks, runs **Schedules**,
636
636
  consolidates the **Inbox**, and so on. It's also the gatekeeper for control state
637
637
  (`paused | running | stopping`).
package/docs/watches.md CHANGED
@@ -133,7 +133,7 @@ Resolution is `path.join(shared.MINIONS_DIR, 'watches.d')` so it works in both d
133
133
 
134
134
  ## Tick Integration
135
135
 
136
- `engine.js` calls `checkWatches(config, state)` every 3 ticks (~3 min at the default 60s tick) inside its own `safe('checkWatches', ...)` block *(source: `engine.js:6386-6440`)*. The engine builds the state object from cached project files + module reads:
136
+ `engine.js` calls `checkWatches(config, state)` every `ENGINE_DEFAULTS.watchPollEvery` ticks (default 18 ⇒ ~3 min at the default 10s tick) inside its own `safe('checkWatches', ...)` block *(source: `engine.js:6386-6440`)*. The engine builds the state object from cached project files + module reads:
137
137
 
138
138
  ```
139
139
  {
package/engine/cli.js CHANGED
@@ -1238,7 +1238,7 @@ const commands = {
1238
1238
  }
1239
1239
 
1240
1240
  console.log('');
1241
- const metrics = shared.safeJson(path.join(__dirname, 'metrics.json')) || {};
1241
+ const metrics = shared.safeJson(path.join(ENGINE_DIR, 'metrics.json')) || {};
1242
1242
  const lifetimeCompleted = Object.entries(metrics).filter(([k]) => !k.startsWith('_')).reduce((sum, [, m]) => sum + (m.tasksCompleted || 0) + (m.tasksErrored || 0), 0);
1243
1243
  console.log(`Dispatch: ${dispatch.pending.length} pending | ${(dispatch.active || []).length} active | ${lifetimeCompleted} completed`);
1244
1244
  const pidSummary = summarizeActiveDispatchPids(dispatch.active || []);
@@ -452,6 +452,70 @@ function getSession(id) {
452
452
  return sessions.find(s => s && s.id === id) || null;
453
453
  }
454
454
 
455
+ // W-mpxpvpwn000ra419 — Review draft modal: pure read helper that resolves
456
+ // session.testFile against qa-tests/<id>/ with the same sandbox guards as
457
+ // dashboard.js#handleQaArtifact (the canonical traversal-defense pattern).
458
+ // Used by GET /api/qa/sessions/<id>/test to feed the read-only Review draft
459
+ // modal on the /qa page.
460
+ //
461
+ // Contract (matches the work-item description, kept tight on purpose):
462
+ // - Returns null when session missing, session.testFile is null/empty, the
463
+ // resolved path escapes qa-tests/<id>/, or the file doesn't exist on disk.
464
+ // Never throws on hostile input — _isSafeSessionId + path.resolve + the
465
+ // baseWithSep prefix check together absorb every traversal shape.
466
+ // - Returns { path, content, language, truncated:false, size } on a normal
467
+ // file. `path` is the resolved absolute path (so callers can log it); the
468
+ // dashboard handler echoes the relative session.testFile back to clients.
469
+ // - Returns { path, content:null, language, truncated:true, size } when the
470
+ // file exceeds GET_SESSION_TEST_FILE_MAX_BYTES (256KB). The modal shows a
471
+ // "file too large" placeholder and suppresses in-modal Approve so users
472
+ // can't blind-approve a megabyte test.
473
+ // - Language inferred from extension: .spec.ts/.test.ts → typescript,
474
+ // .spec.js/.test.js/.js → javascript, .ts → typescript, .py → python,
475
+ // anything else → plaintext. Used by the dashboard to pick a syntax theme.
476
+ const GET_SESSION_TEST_FILE_MAX_BYTES = 256 * 1024;
477
+
478
+ function _inferTestLanguage(relPath) {
479
+ const lower = String(relPath || '').toLowerCase();
480
+ if (lower.endsWith('.spec.ts') || lower.endsWith('.test.ts') || lower.endsWith('.ts')) return 'typescript';
481
+ if (lower.endsWith('.spec.js') || lower.endsWith('.test.js') || lower.endsWith('.js')) return 'javascript';
482
+ if (lower.endsWith('.py')) return 'python';
483
+ return 'plaintext';
484
+ }
485
+
486
+ function getSessionTestFile(sessionId) {
487
+ if (!_isSafeSessionId(sessionId)) return null;
488
+ const session = getSession(sessionId);
489
+ if (!session) return null;
490
+ const rel = session.testFile;
491
+ if (!_isNonEmptyString(rel)) return null;
492
+ // Reject early on null bytes — path.resolve does not strip them and the fs
493
+ // call would either throw or be silently truncated on some platforms.
494
+ if (rel.indexOf('\0') !== -1) return null;
495
+ // Reject absolute paths and Windows drive-letter paths before resolution.
496
+ // path.resolve(base, '/etc/passwd') returns '/etc/passwd' which would
497
+ // bypass the suffix check below if base happened to share a leading
498
+ // prefix; reject pre-resolution to make the intent obvious.
499
+ if (path.isAbsolute(rel)) return null;
500
+ if (/^[a-zA-Z]:/.test(rel)) return null;
501
+ const base = path.resolve(qaTestsDirForSession(sessionId));
502
+ const target = path.resolve(base, rel);
503
+ const baseWithSep = base.endsWith(path.sep) ? base : base + path.sep;
504
+ if (target !== base && !target.startsWith(baseWithSep)) return null;
505
+ let stat;
506
+ try { stat = fs.statSync(target); }
507
+ catch (e) { return null; }
508
+ if (!stat.isFile()) return null;
509
+ const language = _inferTestLanguage(rel);
510
+ if (stat.size > GET_SESSION_TEST_FILE_MAX_BYTES) {
511
+ return { path: target, content: null, language, truncated: true, size: stat.size };
512
+ }
513
+ let content;
514
+ try { content = fs.readFileSync(target, 'utf8'); }
515
+ catch (e) { return null; }
516
+ return { path: target, content, language, truncated: false, size: stat.size };
517
+ }
518
+
455
519
  /**
456
520
  * List sessions, newest first, optionally filtered by state, capped by limit.
457
521
  */
@@ -1247,6 +1311,7 @@ module.exports = {
1247
1311
  // CRUD
1248
1312
  createSession,
1249
1313
  getSession,
1314
+ getSessionTestFile,
1250
1315
  listSessions,
1251
1316
  setSessionWorkItem,
1252
1317
  setSessionQaRunId,