@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.
- package/README.md +6 -6
- package/dashboard/js/qa.js +392 -198
- package/dashboard/js/render-dispatch.js +3 -2
- package/dashboard/js/render-prd.js +1 -1
- package/dashboard/js/settings.js +1 -1
- package/dashboard/pages/qa.html +37 -112
- package/dashboard.js +110 -1
- package/docs/auto-discovery.md +7 -7
- package/docs/human-vs-automated.md +2 -2
- package/docs/index.html +1 -1
- package/docs/managed-spawn.md +1 -1
- package/docs/onboarding.md +2 -2
- package/docs/pr-review-fix-loop.md +1 -1
- package/docs/self-improvement.md +1 -1
- package/docs/slim-ux/concepts.md +2 -2
- package/docs/watches.md +1 -1
- package/engine/cli.js +1 -1
- package/engine/qa-sessions.js +65 -0
- package/engine/shared.js +113 -6
- package/engine/watch-actions.js +301 -0
- package/engine/watches.js +3 -2
- package/engine.js +18 -13
- package/package.json +1 -1
- package/playbooks/plan-to-prd.md +25 -0
- package/prompts/cc-system.md +9 -0
|
@@ -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.
|
|
406
|
-
//
|
|
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 + ' →</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>' +
|
package/dashboard/js/settings.js
CHANGED
|
@@ -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 ||
|
|
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') +
|
package/dashboard/pages/qa.html
CHANGED
|
@@ -1,131 +1,56 @@
|
|
|
1
1
|
<section>
|
|
2
2
|
<h2>QA</h2>
|
|
3
|
-
<p class="empty" style="margin:4px 0 12px 0">
|
|
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">
|
|
7
|
-
<div class="qa-
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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">
|
|
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 2. Click login 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
|
|
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:
|
|
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 },
|
package/docs/auto-discovery.md
CHANGED
|
@@ -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
|
|
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
|
|
21
|
-
2.53 sweepManagedSpawn() managed_spawn TTL/dead-PID/log-rotate sweep (every
|
|
22
|
-
2.55 checkWatches() Persistent watch jobs (every
|
|
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
|
|
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
|
|
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":
|
|
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 (~
|
|
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
|
|
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 ----
|
|
253
|
+
| engine.js ---- 10s tick loop ----+ |
|
|
254
254
|
| | | |
|
|
255
255
|
| discover work spawn agents poll PRs/comments|
|
|
256
256
|
| | | | |
|
package/docs/managed-spawn.md
CHANGED
|
@@ -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` | `
|
|
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`. |
|
package/docs/onboarding.md
CHANGED
|
@@ -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
|
|
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 (≤
|
|
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 (~
|
|
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
|
package/docs/self-improvement.md
CHANGED
|
@@ -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 (~
|
|
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
|
package/docs/slim-ux/concepts.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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(
|
|
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 || []);
|
package/engine/qa-sessions.js
CHANGED
|
@@ -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,
|