@yemi33/minions 0.1.2436 → 0.1.2437
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/dashboard/js/command-center.js +8 -0
- package/dashboard-build.js +55 -25
- package/dashboard.js +92 -37
- package/engine/kb-sweep.js +90 -38
- package/engine/live-checkout.js +34 -7
- package/package.json +1 -1
- package/prompts/cc-system.md +8 -7
|
@@ -1300,6 +1300,14 @@ async function _ccDoSend(message, skipUserMsg, forceTabId, intentMetadata) {
|
|
|
1300
1300
|
var pendingEventName = '';
|
|
1301
1301
|
|
|
1302
1302
|
async function _handleEvent(evt) {
|
|
1303
|
+
// W-ms4z164i01fo438d — terminal frames are idempotent per connection.
|
|
1304
|
+
// A failed turn used to put two `event: error` frames on the wire and
|
|
1305
|
+
// render two identical error bubbles; the backend now emits one, and
|
|
1306
|
+
// this guard keeps the UI correct against any future double-emit or a
|
|
1307
|
+
// terminal frame that arrives twice. `terminalEventSeen` is scoped to
|
|
1308
|
+
// one _ccConsumeStream call, so a reconnect that replays donePayload
|
|
1309
|
+
// still renders it exactly once on the new connection.
|
|
1310
|
+
if (terminalEventSeen && evt && (evt.type === 'done' || evt.type === 'error')) return;
|
|
1303
1311
|
// First event after a reconnect: the server replays the full accumulated
|
|
1304
1312
|
// snapshot (all prior tools, then the whole text as one chunk — see the
|
|
1305
1313
|
// reconnect branch in dashboard.js). Swap the pre-reconnect segments out
|
package/dashboard-build.js
CHANGED
|
@@ -10,6 +10,54 @@ const { safeRead } = shared;
|
|
|
10
10
|
const MINIONS_DIR = __dirname;
|
|
11
11
|
const DASHBOARD_SHARED_JS = ['pr-merge-state', 'pr-filters', 'cc-suggestions', 'model-display', 'watches-source', 'project-git-summary', 'welcome-popup'];
|
|
12
12
|
|
|
13
|
+
// ── Canonical classic-dashboard assembly manifest ──────────────────────────
|
|
14
|
+
// Single source of truth, consumed by BOTH assemblers: buildDashboardHtml()
|
|
15
|
+
// below and dashboard.js#buildDashboardHtml() (the one the real server ships).
|
|
16
|
+
// These used to be duplicated array literals inside each function; they drifted
|
|
17
|
+
// (dashboard.js lost 'memory-panel' and never gained sub-fragment substitution
|
|
18
|
+
// at all), which silently shipped an /engine page with no Memory panel and no
|
|
19
|
+
// working "Capture heap snapshot" button. Keep them here and import them —
|
|
20
|
+
// never re-declare a local copy. Pinned by test/unit/dashboard-assembly-parity.test.js.
|
|
21
|
+
const DASHBOARD_PAGES = ['home', 'work', 'prs', 'plans', 'inbox', 'tools', 'schedule', 'watches', 'pipelines', 'meetings', 'qa', 'engine'];
|
|
22
|
+
|
|
23
|
+
// Sub-fragments substituted into a parent page at assembly time via
|
|
24
|
+
// <!-- __MARKER__ --> tokens. Lets large panels live in their own
|
|
25
|
+
// fragment file without inflating the parent page. P-d4e5f6a7 introduced
|
|
26
|
+
// engine-memory-panel.html as the first such sub-fragment; add more here
|
|
27
|
+
// by mapping page -> { marker -> fragment basename }.
|
|
28
|
+
const DASHBOARD_PAGE_SUB_FRAGMENTS = {
|
|
29
|
+
engine: { '<!-- __ENGINE_MEMORY_PANEL__ -->': 'engine-memory-panel' },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// JS module bundle (order matters: utils → state → renderers → commands → refresh).
|
|
33
|
+
// Every dashboard/js/*.js file belongs here unless it is lazy-loaded through a
|
|
34
|
+
// dedicated route (only memory-search.js today, served at /assets/memory-search.js).
|
|
35
|
+
const DASHBOARD_JS_FILES = [
|
|
36
|
+
'utils', 'state', 'features-client', 'render-utils', 'charter-editor', 'detail-panel', 'live-stream',
|
|
37
|
+
'render-agents', 'render-dispatch', 'render-work-items', 'render-prd',
|
|
38
|
+
'render-prs', 'render-plans', 'render-inbox', 'render-kb', 'render-skills',
|
|
39
|
+
'render-other', 'render-managed', 'memory-panel', 'render-schedules', 'render-watches', 'render-pipelines', 'render-meetings', 'render-pinned',
|
|
40
|
+
'command-parser', 'command-input', 'command-center', 'command-history',
|
|
41
|
+
'confirm-dialog', 'modal', 'modal-qa', 'settings', 'qa', 'fre', 'refresh',
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Read a page fragment and substitute any sub-fragment markers declared for it.
|
|
46
|
+
* Shared by both assemblers so a sub-fragment can never be wired into one only.
|
|
47
|
+
*/
|
|
48
|
+
function readPageFragment(dashDir, page) {
|
|
49
|
+
let content = safeRead(path.join(dashDir, 'pages', page + '.html'));
|
|
50
|
+
const subs = DASHBOARD_PAGE_SUB_FRAGMENTS[page];
|
|
51
|
+
if (subs) {
|
|
52
|
+
for (const [marker, basename] of Object.entries(subs)) {
|
|
53
|
+
const fragment = safeRead(path.join(dashDir, 'pages', basename + '.html'));
|
|
54
|
+
// Function replacer: fragment text may contain `$&`/`$1` patterns.
|
|
55
|
+
content = content.replace(marker, () => fragment);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return content;
|
|
59
|
+
}
|
|
60
|
+
|
|
13
61
|
function buildDashboardHtml() {
|
|
14
62
|
const dashDir = path.join(MINIONS_DIR, 'dashboard');
|
|
15
63
|
const layoutPath = path.join(dashDir, 'layout.html');
|
|
@@ -21,37 +69,15 @@ function buildDashboardHtml() {
|
|
|
21
69
|
const layout = safeRead(layoutPath);
|
|
22
70
|
const css = safeRead(path.join(dashDir, 'styles.css'));
|
|
23
71
|
|
|
24
|
-
const pages =
|
|
25
|
-
// Sub-fragments substituted into a parent page at assembly time via
|
|
26
|
-
// <!-- __MARKER__ --> tokens. Lets large panels live in their own
|
|
27
|
-
// fragment file without inflating the parent page. P-d4e5f6a7 introduced
|
|
28
|
-
// engine-memory-panel.html as the first such sub-fragment; add more here
|
|
29
|
-
// by mapping marker -> fragment basename.
|
|
30
|
-
const pageSubFragments = {
|
|
31
|
-
engine: { '<!-- __ENGINE_MEMORY_PANEL__ -->': 'engine-memory-panel' },
|
|
32
|
-
};
|
|
72
|
+
const pages = DASHBOARD_PAGES;
|
|
33
73
|
let pageHtml = '';
|
|
34
74
|
for (const p of pages) {
|
|
35
|
-
|
|
36
|
-
const subs = pageSubFragments[p];
|
|
37
|
-
if (subs) {
|
|
38
|
-
for (const [marker, basename] of Object.entries(subs)) {
|
|
39
|
-
const fragment = safeRead(path.join(dashDir, 'pages', basename + '.html'));
|
|
40
|
-
content = content.replace(marker, () => fragment);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
75
|
+
const content = readPageFragment(dashDir, p);
|
|
43
76
|
const activeClass = p === 'home' ? ' active' : '';
|
|
44
77
|
pageHtml += ` <div class="page${activeClass}" id="page-${p}">\n${content}\n </div>\n\n`;
|
|
45
78
|
}
|
|
46
79
|
|
|
47
|
-
const jsFiles =
|
|
48
|
-
'utils', 'state', 'features-client', 'render-utils', 'charter-editor', 'detail-panel', 'live-stream',
|
|
49
|
-
'render-agents', 'render-dispatch', 'render-work-items', 'render-prd',
|
|
50
|
-
'render-prs', 'render-plans', 'render-inbox', 'render-kb', 'render-skills',
|
|
51
|
-
'render-other', 'render-managed', 'memory-panel', 'render-schedules', 'render-watches', 'render-pipelines', 'render-meetings', 'render-pinned',
|
|
52
|
-
'command-parser', 'command-input', 'command-center', 'command-history',
|
|
53
|
-
'confirm-dialog', 'modal', 'modal-qa', 'settings', 'qa', 'fre', 'refresh'
|
|
54
|
-
];
|
|
80
|
+
const jsFiles = DASHBOARD_JS_FILES;
|
|
55
81
|
let jsHtml = '';
|
|
56
82
|
for (const f of DASHBOARD_SHARED_JS) {
|
|
57
83
|
const content = safeRead(path.join(dashDir, 'shared', f + '.js'));
|
|
@@ -175,6 +201,10 @@ module.exports = {
|
|
|
175
201
|
buildDashboardHtml,
|
|
176
202
|
buildSlimHtml,
|
|
177
203
|
DASHBOARD_SHARED_JS,
|
|
204
|
+
DASHBOARD_PAGES,
|
|
205
|
+
DASHBOARD_PAGE_SUB_FRAGMENTS,
|
|
206
|
+
DASHBOARD_JS_FILES,
|
|
207
|
+
readPageFragment,
|
|
178
208
|
SLIM_JS_ORDER,
|
|
179
209
|
_resetSlimPartsCacheForTest,
|
|
180
210
|
};
|
package/dashboard.js
CHANGED
|
@@ -68,7 +68,12 @@ const prFixTarget = require('./engine/pr-fix-target');
|
|
|
68
68
|
const prTrack = require('./engine/pr-track');
|
|
69
69
|
const resolveArea = require('./engine/resolve-area');
|
|
70
70
|
const features = require('./engine/features');
|
|
71
|
-
const {
|
|
71
|
+
const {
|
|
72
|
+
DASHBOARD_SHARED_JS,
|
|
73
|
+
DASHBOARD_PAGES,
|
|
74
|
+
DASHBOARD_JS_FILES,
|
|
75
|
+
readPageFragment,
|
|
76
|
+
} = require('./dashboard-build');
|
|
72
77
|
const { normalizeExecutionModel } = require('./engine/execution-model');
|
|
73
78
|
const ccWorkerPool = require('./engine/cc-worker-pool');
|
|
74
79
|
const diagnosticsMemory = require('./engine/diagnostics-memory');
|
|
@@ -1920,30 +1925,23 @@ function buildDashboardHtml() {
|
|
|
1920
1925
|
// Assemble page fragments. Each wrapper gets a `.page-toast` inline slot at
|
|
1921
1926
|
// the top — showToast('cmd-toast', …) auto-routes here when a page is active,
|
|
1922
1927
|
// so feedback lands near the action instead of the floating top-right toast.
|
|
1923
|
-
|
|
1928
|
+
// Page list, sub-fragment map and fragment reader all come from
|
|
1929
|
+
// dashboard-build.js so this assembler cannot drift from the standalone one.
|
|
1924
1930
|
const pageToast = ' <div class="cmd-toast cmd-toast-inline page-toast" style="margin:6px 16px"></div>\n';
|
|
1925
1931
|
let pageHtml = '';
|
|
1926
|
-
for (const p of
|
|
1927
|
-
const content =
|
|
1932
|
+
for (const p of DASHBOARD_PAGES) {
|
|
1933
|
+
const content = readPageFragment(dashDir, p);
|
|
1928
1934
|
const activeClass = p === 'home' ? ' active' : '';
|
|
1929
1935
|
pageHtml += ` <div class="page${activeClass}" id="page-${p}">\n${pageToast}${content}\n </div>\n\n`;
|
|
1930
1936
|
}
|
|
1931
1937
|
|
|
1932
1938
|
// Assemble JS modules (order matters: utils → state → renderers → commands → refresh)
|
|
1933
|
-
const jsFiles = [
|
|
1934
|
-
'utils', 'state', 'features-client', 'render-utils', 'charter-editor', 'detail-panel', 'live-stream',
|
|
1935
|
-
'render-agents', 'render-dispatch', 'render-work-items', 'render-prd',
|
|
1936
|
-
'render-prs', 'render-plans', 'render-inbox', 'render-kb', 'render-skills',
|
|
1937
|
-
'render-other', 'render-managed', 'render-schedules', 'render-watches', 'render-pipelines', 'render-meetings', 'render-pinned',
|
|
1938
|
-
'command-parser', 'command-input', 'command-center', 'command-history',
|
|
1939
|
-
'confirm-dialog', 'modal', 'modal-qa', 'settings', 'qa', 'fre', 'refresh'
|
|
1940
|
-
];
|
|
1941
1939
|
let jsHtml = '';
|
|
1942
1940
|
for (const f of DASHBOARD_SHARED_JS) {
|
|
1943
1941
|
const content = safeRead(path.join(dashDir, 'shared', f + '.js'));
|
|
1944
1942
|
jsHtml += `\n// ─── shared/${f}.js ────────────────────────────────────────\n${content}\n`;
|
|
1945
1943
|
}
|
|
1946
|
-
for (const f of
|
|
1944
|
+
for (const f of DASHBOARD_JS_FILES) {
|
|
1947
1945
|
const content = safeRead(path.join(dashDir, 'js', f + '.js'));
|
|
1948
1946
|
jsHtml += `\n// ─── ${f}.js ────────────────────────────────────────\n${content}\n`;
|
|
1949
1947
|
}
|
|
@@ -3800,6 +3798,49 @@ const CC_ERROR_CODES = Object.freeze([
|
|
|
3800
3798
|
// surfaces this typed code via _buildCcErrorEnvelope, never a 500.
|
|
3801
3799
|
'invalid-image',
|
|
3802
3800
|
]);
|
|
3801
|
+
// W-ms4z164i01fo438d — a `done` or `error` frame is the LAST thing a CC or
|
|
3802
|
+
// doc-chat SSE consumer will ever see, so neither writer may shed one under
|
|
3803
|
+
// backpressure. Shared by writeCcEvent and writeDocEvent so the rule lives in
|
|
3804
|
+
// one place instead of being restated per stream.
|
|
3805
|
+
function _isTerminalSseFrameType(type) {
|
|
3806
|
+
return type === 'done' || type === 'error';
|
|
3807
|
+
}
|
|
3808
|
+
|
|
3809
|
+
// W-mpmwxni2000c25c7-d / W-ms4z164i01fo438d — render exactly ONE SSE frame.
|
|
3810
|
+
// Terminal error frames get a named `event: error` line so consumers using
|
|
3811
|
+
// addEventListener('error', …) (and tests matching the raw wire) can target
|
|
3812
|
+
// them; the JSON payload still carries `type: 'error'` so data-line-only
|
|
3813
|
+
// parsers keep working. Shared by writeCcEvent and writeDocEvent.
|
|
3814
|
+
//
|
|
3815
|
+
// One call == one frame. A caller that emits a terminal error through a writer
|
|
3816
|
+
// AND raw-writes its own `event: error` frame puts two frames on the wire, and
|
|
3817
|
+
// the CC client backfills `type` from the `event:` line — so both render as
|
|
3818
|
+
// error bubbles. That was the duplicate-error-bubble bug; keep this the single
|
|
3819
|
+
// wire-rendering seam.
|
|
3820
|
+
function _sseFrame(payload) {
|
|
3821
|
+
const eventLine = (payload && payload.type === 'error') ? 'event: error\n' : '';
|
|
3822
|
+
return eventLine + 'data: ' + JSON.stringify(payload) + '\n\n';
|
|
3823
|
+
}
|
|
3824
|
+
|
|
3825
|
+
// W-ms4z164i01fo438d — canonical terminal-error payload for a failed CC stream
|
|
3826
|
+
// turn. Stored on `liveState.donePayload` (so the reconnect branch replays the
|
|
3827
|
+
// exact same terminal frame to a reattaching client) and handed to the writer
|
|
3828
|
+
// once. `message` is the canonical envelope field; `error` is kept as a legacy
|
|
3829
|
+
// alias for clients predating the typed envelope. `stderr` carries the tail
|
|
3830
|
+
// that used to ride only on the now-removed raw frame.
|
|
3831
|
+
function _buildCcTerminalErrorPayload(envelope, stderrTail) {
|
|
3832
|
+
const message = (envelope && envelope.message) || 'Command Center reported an unknown error.';
|
|
3833
|
+
return {
|
|
3834
|
+
type: 'error',
|
|
3835
|
+
message,
|
|
3836
|
+
error: message,
|
|
3837
|
+
code: (envelope && envelope.code) || 'crash',
|
|
3838
|
+
retriable: !!(envelope && envelope.retriable),
|
|
3839
|
+
sessionId: null,
|
|
3840
|
+
...(stderrTail ? { stderr: String(stderrTail).slice(0, 500) } : {}),
|
|
3841
|
+
};
|
|
3842
|
+
}
|
|
3843
|
+
|
|
3803
3844
|
function _buildCcErrorEnvelope({ message, code, retryable, ...extra } = {}) {
|
|
3804
3845
|
const normalizedCode = CC_ERROR_CODES.includes(code) ? code : 'crash';
|
|
3805
3846
|
return {
|
|
@@ -10318,7 +10359,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10318
10359
|
} catch { /* listener registration is best-effort */ }
|
|
10319
10360
|
const writeDocEvent = (payload) => {
|
|
10320
10361
|
const type = payload && payload.type;
|
|
10321
|
-
const isTerminal = type
|
|
10362
|
+
const isTerminal = _isTerminalSseFrameType(type);
|
|
10322
10363
|
const _logFail = (reason) => {
|
|
10323
10364
|
try {
|
|
10324
10365
|
shared.log('warn', `[doc-sse-fail] ${JSON.stringify({ doc: docKey || 'unknown', type, reason, destroyed: !!res.destroyed, writableEnded: !!res.writableEnded, streamEnded: _docStreamEnded })}`);
|
|
@@ -10330,14 +10371,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10330
10371
|
}
|
|
10331
10372
|
let wire;
|
|
10332
10373
|
try {
|
|
10333
|
-
// W-mpmwxni2000c25c7-d / W-mqevl09s000i9989 —
|
|
10334
|
-
//
|
|
10335
|
-
//
|
|
10336
|
-
//
|
|
10337
|
-
//
|
|
10338
|
-
wire = (
|
|
10339
|
-
? `event: error\ndata: ${JSON.stringify(payload)}\n\n`
|
|
10340
|
-
: `data: ${JSON.stringify(payload)}\n\n`;
|
|
10374
|
+
// W-mpmwxni2000c25c7-d / W-mqevl09s000i9989 — one frame per call via
|
|
10375
|
+
// the shared _sseFrame renderer (mirrors handleCommandCenterStream):
|
|
10376
|
+
// terminal errors go out as a named `event: error` frame, and the JSON
|
|
10377
|
+
// payload still carries `type: 'error'` for the data-line parser in
|
|
10378
|
+
// modal-qa.js.
|
|
10379
|
+
wire = _sseFrame(payload);
|
|
10341
10380
|
} catch {
|
|
10342
10381
|
_logFail('json-serialize-failed');
|
|
10343
10382
|
return false;
|
|
@@ -11629,6 +11668,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
11629
11668
|
const writeCcEvent = (payload) => {
|
|
11630
11669
|
const type = payload && payload.type;
|
|
11631
11670
|
const isUserFacing = type === 'chunk' || type === 'done' || type === 'tool' || type === 'tool-update' || type === 'error';
|
|
11671
|
+
const isTerminal = _isTerminalSseFrameType(type);
|
|
11632
11672
|
const _logFail = (reason, extra) => {
|
|
11633
11673
|
if (!isUserFacing) return;
|
|
11634
11674
|
try {
|
|
@@ -11651,14 +11691,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
11651
11691
|
}
|
|
11652
11692
|
let wire;
|
|
11653
11693
|
try {
|
|
11654
|
-
// W-mpmwxni2000c25c7-d
|
|
11655
|
-
//
|
|
11656
|
-
//
|
|
11657
|
-
//
|
|
11658
|
-
//
|
|
11659
|
-
|
|
11660
|
-
const eventLine = (type === 'error') ? 'event: error\n' : '';
|
|
11661
|
-
wire = eventLine + 'data: ' + JSON.stringify(payload) + '\n\n';
|
|
11694
|
+
// W-mpmwxni2000c25c7-d / W-ms4z164i01fo438d — one frame per call via
|
|
11695
|
+
// the shared _sseFrame renderer. This is the ONLY place a CC terminal
|
|
11696
|
+
// error reaches the wire: callers must not also `res.write` their own
|
|
11697
|
+
// `event: error` frame, because the client backfills `type` from the
|
|
11698
|
+
// `event:` line and would render a second error bubble.
|
|
11699
|
+
wire = _sseFrame(payload);
|
|
11662
11700
|
}
|
|
11663
11701
|
catch (err) {
|
|
11664
11702
|
_logFail('json-serialize-failed', { error: String((err && err.message) || err).slice(0, 200) });
|
|
@@ -11676,7 +11714,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
11676
11714
|
// bump _ccTelemetry counters so the [cc-stream] outcome log line stays
|
|
11677
11715
|
// truthful about what the orchestrator produced — only the wire was
|
|
11678
11716
|
// shed, the work happened.
|
|
11679
|
-
|
|
11717
|
+
// W-ms4z164i01fo438d — terminal `done` / `error` frames are NEVER shed
|
|
11718
|
+
// (mirrors writeDocEvent). The terminal error used to also go out via a
|
|
11719
|
+
// raw res.write that bypassed this cap entirely; now that the writer is
|
|
11720
|
+
// the single emitter, shedding it would drop the failure off the wire.
|
|
11721
|
+
if (!isTerminal && _queuedBytes > SSE_MAX_QUEUE_BYTES) {
|
|
11680
11722
|
try {
|
|
11681
11723
|
shared.log('warn', `[cc-sse-shed] tab=${tabId || _ccTelemetry.tabId || 'unknown'} type=${type} queuedBytes=${_queuedBytes} wireBytes=${wire.length}`);
|
|
11682
11724
|
} catch { /* telemetry is best-effort */ }
|
|
@@ -12117,12 +12159,17 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
12117
12159
|
trackErr('command-center', envelope.code);
|
|
12118
12160
|
const stderrTail = (result.stderr || '').trim().split('\n').filter(Boolean).slice(-3).join(' | ');
|
|
12119
12161
|
console.error(`[CC-stream] Failed code=${envelope.code} retriable=${envelope.retriable}: ${(result.stderr || '').slice(0, 500)}; stdout_tail=${(result.raw || '').slice(-500)}`);
|
|
12120
|
-
//
|
|
12121
|
-
//
|
|
12122
|
-
//
|
|
12123
|
-
//
|
|
12124
|
-
|
|
12125
|
-
|
|
12162
|
+
// W-ms4z164i01fo438d — emit exactly ONE terminal frame. writeCcEvent
|
|
12163
|
+
// prefixes `event: error\n` for `type: 'error'`, so this single call
|
|
12164
|
+
// satisfies both the named-SSE-frame contract and the `data:`-only
|
|
12165
|
+
// parser. This block previously ALSO did a raw
|
|
12166
|
+
// `res.write('event: error\ndata: …')` before calling the writer,
|
|
12167
|
+
// which put two `event: error` frames on the wire and rendered two
|
|
12168
|
+
// identical error bubbles (each with its own Retry / New Session
|
|
12169
|
+
// controls) in the Command Center. donePayload keeps carrying the
|
|
12170
|
+
// canonical envelope so the reconnect branch above replays the same
|
|
12171
|
+
// single terminal frame to a reattaching client.
|
|
12172
|
+
liveState.donePayload = _buildCcTerminalErrorPayload(envelope, stderrTail);
|
|
12126
12173
|
if (liveState.writer) liveState.writer(liveState.donePayload);
|
|
12127
12174
|
if (liveState.endResponse) liveState.endResponse();
|
|
12128
12175
|
_scheduleCcLiveCleanup(tabId);
|
|
@@ -16340,6 +16387,9 @@ function _installCrashHandlers() {
|
|
|
16340
16387
|
// Production entry points use the closures directly; tests import via require('./dashboard').
|
|
16341
16388
|
module.exports = {
|
|
16342
16389
|
getMcpServers,
|
|
16390
|
+
// W-ms5dlone012i457a-d — lets tests assert the HTML the SERVER actually ships,
|
|
16391
|
+
// not just dashboard-build.js's standalone copy (the two silently drifted).
|
|
16392
|
+
_buildDashboardHtmlForTest: buildDashboardHtml,
|
|
16343
16393
|
_setPrRefVerifierForTest, // issue #246 — inject a fake loose-PR-ref verifier in handler tests
|
|
16344
16394
|
_prRefVerifyCache, // W-mqtzrix100060c9f — test seam for cache size inspection
|
|
16345
16395
|
_PR_REF_VERIFY_TTL_MS, // W-mqtzrix100060c9f — exported for test assertions
|
|
@@ -16390,6 +16440,11 @@ module.exports = {
|
|
|
16390
16440
|
// P-9c5f1a83 — CC image-attachment validation surface (test seams)
|
|
16391
16441
|
_validateCcImages,
|
|
16392
16442
|
_buildCcErrorEnvelope,
|
|
16443
|
+
// W-ms4z164i01fo438d — SSE terminal-frame seam (exported for testing).
|
|
16444
|
+
// See test/unit/cc-error-envelopes.test.js.
|
|
16445
|
+
_sseFrame,
|
|
16446
|
+
_isTerminalSseFrameType,
|
|
16447
|
+
_buildCcTerminalErrorPayload,
|
|
16393
16448
|
CC_ERROR_CODES,
|
|
16394
16449
|
CC_IMAGE_MAX_COUNT,
|
|
16395
16450
|
CC_IMAGE_MAX_DECODED_BYTES,
|
package/engine/kb-sweep.js
CHANGED
|
@@ -27,6 +27,16 @@ const AUTO_SWEEP_INTERVAL_MS = 4 * 60 * 60 * 1000;
|
|
|
27
27
|
const AUTO_SWEEP_FAILURE_RETRY_MS = 15 * 60 * 1000;
|
|
28
28
|
const KB_SWEPT_PATH = path.join(ENGINE_DIR, 'kb-swept.json');
|
|
29
29
|
const COMPRESS_THRESHOLD_BYTES = 5000;
|
|
30
|
+
// Hard input ceiling for the per-entry LLM rewrite pass (W-ms25l4pr000kb9db).
|
|
31
|
+
// The rewrite pass sends an entry's FULL body to the LLM; a pathological entry
|
|
32
|
+
// (e.g. a live-checkout dirty alert that embedded a whole `git status` dump —
|
|
33
|
+
// observed at 10 MB each) can OOM/timeout/hang the call and, without per-pass
|
|
34
|
+
// isolation, abort the entire sweep before the cheap age-expiry pass reclaims
|
|
35
|
+
// space. Entries whose on-disk size exceeds this ceiling are skipped from the
|
|
36
|
+
// rewrite (left for age-expiry / structural consolidation to reclaim) so one
|
|
37
|
+
// giant entry can never choke the sweep. 20x the compress threshold (~100 KB)
|
|
38
|
+
// is far above every normal entry yet well below the multi-MB monsters.
|
|
39
|
+
const REWRITE_MAX_INPUT_BYTES = COMPRESS_THRESHOLD_BYTES * 20;
|
|
30
40
|
const LLM_BATCH_SIZE = 30;
|
|
31
41
|
const NORMALIZE_CONCURRENCY = 5;
|
|
32
42
|
const SWEPT_FLAG_KEY = '_swept'; // frontmatter key — entries with this skip the rewrite pass
|
|
@@ -610,6 +620,13 @@ ${body}`;
|
|
|
610
620
|
const fp = path.join(KB_DIR, e.category, e.file);
|
|
611
621
|
const content = safeRead(fp);
|
|
612
622
|
if (content == null) continue;
|
|
623
|
+
// Oversized-entry guard (W-ms25l4pr000kb9db): never send a multi-MB entry to
|
|
624
|
+
// the LLM — it risks OOM/timeout/hang. Skip it; age-expiry / consolidation
|
|
625
|
+
// reclaim it instead.
|
|
626
|
+
if (content.length > REWRITE_MAX_INPUT_BYTES) {
|
|
627
|
+
log('warn', `[kb-sweep] rewrite skip ${e.category}/${e.file}: ${content.length} bytes exceeds ${REWRITE_MAX_INPUT_BYTES}B ceiling — left for age-expiry/consolidation`);
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
613
630
|
const { fm, body } = _parseFrontmatter(content);
|
|
614
631
|
// Skip already-processed unless the file was modified after the sweep flag was set
|
|
615
632
|
if (fm[SWEPT_FLAG_KEY]) {
|
|
@@ -984,9 +1001,19 @@ async function _runKbSweepImpl(opts = {}) {
|
|
|
984
1001
|
}
|
|
985
1002
|
if (manifest.length < 2) { summary.summary = 'nothing to sweep (< 2 unpinned entries)'; summary.durationMs = Date.now() - t0; return summary; }
|
|
986
1003
|
|
|
1004
|
+
// Every pass below is wrapped in its own try/catch (W-ms25l4pr000kb9db). A
|
|
1005
|
+
// throw/OOM/hang in one pass must NOT abort the whole sweep and starve the
|
|
1006
|
+
// later reclaimers — in particular the cheap age-expiry pass and
|
|
1007
|
+
// _pruneOldSwept must always get to run. Each pass degrades to "no-op on this
|
|
1008
|
+
// pass, carry the prior survivors forward".
|
|
1009
|
+
|
|
987
1010
|
// 1. Hash-based dedup (cheap, catches cross-batch duplicates)
|
|
988
|
-
|
|
989
|
-
|
|
1011
|
+
let afterHash = manifest;
|
|
1012
|
+
try {
|
|
1013
|
+
const { survivors, archived } = _hashDedup(manifest, opts);
|
|
1014
|
+
afterHash = survivors;
|
|
1015
|
+
summary.hashDuplicatesArchived = archived;
|
|
1016
|
+
} catch (e) { log('warn', `[kb-sweep] hash-dedup pass failed: ${e && e.message}`); }
|
|
990
1017
|
|
|
991
1018
|
// 1.5. Structural consolidation (W-mr973knu) — PRIMARY balloon fix. Collapse
|
|
992
1019
|
// large groups of near-identical boilerplate notes (agent-failure dumps, no-op
|
|
@@ -994,47 +1021,70 @@ async function _runKbSweepImpl(opts = {}) {
|
|
|
994
1021
|
// week). Runs on recent entries too, so it shrinks the KB continuously rather
|
|
995
1022
|
// than merely aging files out. Downstream passes operate on the survivors so
|
|
996
1023
|
// the LLM/rewrite passes no longer waste calls on the collapsed notes.
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
//
|
|
1015
|
-
//
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1024
|
+
let afterConsolidate = afterHash;
|
|
1025
|
+
try {
|
|
1026
|
+
const consolidateResult = _structuralConsolidate(afterHash, opts);
|
|
1027
|
+
summary.notesConsolidated = consolidateResult.consolidated;
|
|
1028
|
+
summary.consolidationGroups = consolidateResult.groupsCollapsed;
|
|
1029
|
+
summary.digestsWritten = consolidateResult.digestsWritten;
|
|
1030
|
+
afterConsolidate = consolidateResult.survivors.filter(
|
|
1031
|
+
e => e._digest || fs.existsSync(path.join(KB_DIR, e.category, e.file)),
|
|
1032
|
+
);
|
|
1033
|
+
} catch (e) { log('warn', `[kb-sweep] structural-consolidation pass failed: ${e && e.message}`); }
|
|
1034
|
+
|
|
1035
|
+
// 2. Age-based TTL expiry (W-mr48yth5) — MOVED AHEAD of the LLM passes
|
|
1036
|
+
// (W-ms25l4pr000kb9db). This is the cheapest reducer AND the one that reclaims
|
|
1037
|
+
// the most space from oversized transient entries, so it MUST run before the
|
|
1038
|
+
// LLM batch sweep + rewrite pass — either of which can choke on a multi-MB
|
|
1039
|
+
// entry. Running pure aging first guarantees stale entries are reclaimed even
|
|
1040
|
+
// if a later LLM/rewrite pass throws or hangs. Archives transient-category
|
|
1041
|
+
// entries older than their per-category TTL to knowledge/_swept/; durable
|
|
1042
|
+
// categories (no TTL) and consolidation digests are untouched.
|
|
1043
|
+
let afterAge = afterConsolidate;
|
|
1044
|
+
try {
|
|
1045
|
+
const survivingForAge = afterConsolidate.filter(
|
|
1046
|
+
e => e._digest || fs.existsSync(path.join(KB_DIR, e.category, e.file)),
|
|
1047
|
+
);
|
|
1048
|
+
const ageResult = _expireByAge(survivingForAge, opts);
|
|
1049
|
+
summary.ageExpired = ageResult.expired;
|
|
1050
|
+
afterAge = ageResult.survivors;
|
|
1051
|
+
} catch (e) { log('warn', `[kb-sweep] age-expiry pass failed: ${e && e.message}`); }
|
|
1052
|
+
|
|
1053
|
+
// 3. LLM batch sweep — within-batch dupes + reclassify + remove stale.
|
|
1054
|
+
// Only runs against survivors, but we need indices that match the LIST sent to
|
|
1055
|
+
// the LLM. Deferred LLM_SCAN_FLAG_KEY stamping (below) needs llmManifest +
|
|
1056
|
+
// llmScannedIdx in scope, so declare them outside the try.
|
|
1057
|
+
let llmManifest = afterAge.filter(e => e._digest || fs.existsSync(path.join(KB_DIR, e.category, e.file)));
|
|
1058
|
+
let llmScannedIdx = [];
|
|
1059
|
+
try {
|
|
1060
|
+
const { plan, scannedIdx } = await _llmBatchSweep(llmManifest, callLLM, trackEngineUsage, opts);
|
|
1061
|
+
llmScannedIdx = scannedIdx;
|
|
1062
|
+
const llmActions = _applyLlmPlan(plan, llmManifest, llmScannedIdx, opts);
|
|
1063
|
+
summary.llmDuplicatesArchived = llmActions.merged;
|
|
1064
|
+
summary.staleRemoved = llmActions.removed;
|
|
1065
|
+
summary.reclassified = llmActions.reclassified;
|
|
1066
|
+
} catch (e) { log('warn', `[kb-sweep] LLM batch-sweep pass failed: ${e && e.message}`); }
|
|
1067
|
+
|
|
1068
|
+
// 4. Per-entry rewrite (compress + normalize). Filter to entries that survived
|
|
1069
|
+
// the earlier passes (still on disk). Oversized entries are skipped inside
|
|
1070
|
+
// _rewritePass (REWRITE_MAX_INPUT_BYTES) so they never choke the LLM.
|
|
1071
|
+
try {
|
|
1072
|
+
const stillOnDisk = afterAge.filter(e => fs.existsSync(path.join(KB_DIR, e.category, e.file)));
|
|
1073
|
+
const rewriteResult = await _rewritePass(stillOnDisk, callLLM, trackEngineUsage, opts);
|
|
1074
|
+
summary.rewritten = rewriteResult.processed;
|
|
1075
|
+
summary.rewriteBytesBefore = rewriteResult.bytesBefore;
|
|
1076
|
+
summary.rewriteBytesAfter = rewriteResult.bytesAfter;
|
|
1077
|
+
} catch (e) { log('warn', `[kb-sweep] rewrite pass failed: ${e && e.message}`); }
|
|
1030
1078
|
|
|
1031
1079
|
// Stamp LLM_SCAN_FLAG_KEY on entries analyzed by _llmBatchSweep this run —
|
|
1032
1080
|
// deferred until after the rewrite pass so its content/mtime update doesn't
|
|
1033
1081
|
// race the freshness marker we're about to write (see _markLlmScanned doc).
|
|
1034
|
-
_markLlmScanned(llmManifest, llmScannedIdx, opts);
|
|
1082
|
+
try { _markLlmScanned(llmManifest, llmScannedIdx, opts); }
|
|
1083
|
+
catch (e) { log('warn', `[kb-sweep] mark-llm-scanned failed: ${e && e.message}`); }
|
|
1035
1084
|
|
|
1036
|
-
//
|
|
1037
|
-
summary.sweptArchivePruned = _pruneOldSwept();
|
|
1085
|
+
// 5. Prune old swept files (>30 days)
|
|
1086
|
+
try { summary.sweptArchivePruned = _pruneOldSwept(); }
|
|
1087
|
+
catch (e) { log('warn', `[kb-sweep] prune-old-swept failed: ${e && e.message}`); }
|
|
1038
1088
|
|
|
1039
1089
|
// Final tallies — re-walk surviving entries for accurate bytesAfter
|
|
1040
1090
|
const finalEntries = await queries.getKnowledgeBaseEntries();
|
|
@@ -1318,5 +1368,7 @@ module.exports = {
|
|
|
1318
1368
|
LLM_SCAN_FLAG_KEY,
|
|
1319
1369
|
CONSOLIDATION_DIGEST_PREFIX,
|
|
1320
1370
|
COMPRESS_THRESHOLD_BYTES,
|
|
1371
|
+
REWRITE_MAX_INPUT_BYTES,
|
|
1321
1372
|
SWEPT_FLAG_KEY,
|
|
1373
|
+
_rewritePass,
|
|
1322
1374
|
};
|
package/engine/live-checkout.js
CHANGED
|
@@ -287,12 +287,32 @@ function _collectAutoCleanablePaths(dirtyFiles) {
|
|
|
287
287
|
// alert stays human-scannable and bounded. Pure/deterministic — exported for
|
|
288
288
|
// unit testing.
|
|
289
289
|
const DIRTY_ALERT_MAX_LINES = 200;
|
|
290
|
-
|
|
290
|
+
// Companion byte budget for the embedded dirty-file block. The line cap alone
|
|
291
|
+
// still leaves a hole: 200 arbitrarily-long paths (deeply-nested Windows paths,
|
|
292
|
+
// rename `->` pairs, quoted UTF-8 escapes) could each be hundreds of bytes, so
|
|
293
|
+
// bound the total bytes too. Belt-and-suspenders so a single alert body can
|
|
294
|
+
// never embed a multi-MB git-status dump under any input. 16 KiB is far above a
|
|
295
|
+
// realistic ≤200-file sample yet keeps the whole note human-scannable.
|
|
296
|
+
const DIRTY_ALERT_MAX_BYTES = 16 * 1024;
|
|
297
|
+
function capDirtyFileLines(dirtyFiles, maxLines = DIRTY_ALERT_MAX_LINES, maxBytes = DIRTY_ALERT_MAX_BYTES) {
|
|
291
298
|
const lines = Array.isArray(dirtyFiles) ? dirtyFiles : [];
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
299
|
+
const lineCap = Number.isInteger(maxLines) && maxLines > 0 ? maxLines : DIRTY_ALERT_MAX_LINES;
|
|
300
|
+
const byteCap = Number.isInteger(maxBytes) && maxBytes > 0 ? maxBytes : DIRTY_ALERT_MAX_BYTES;
|
|
301
|
+
// First cap by line count, then enforce the byte budget over the kept lines
|
|
302
|
+
// (each line plus its joining newline). Either cap can trigger the notice.
|
|
303
|
+
const byLine = lines.slice(0, lineCap);
|
|
304
|
+
const kept = [];
|
|
305
|
+
let bytes = 0;
|
|
306
|
+
let byteLimited = false;
|
|
307
|
+
for (const line of byLine) {
|
|
308
|
+
const lineBytes = Buffer.byteLength(String(line), 'utf8') + 1;
|
|
309
|
+
if (bytes + lineBytes > byteCap) { byteLimited = true; break; }
|
|
310
|
+
bytes += lineBytes;
|
|
311
|
+
kept.push(line);
|
|
312
|
+
}
|
|
313
|
+
const omitted = lines.length - kept.length;
|
|
314
|
+
if (omitted <= 0 && !byteLimited) return lines.slice();
|
|
315
|
+
kept.push(`... and ${omitted} more (run \`git status --porcelain=v1 -b\` locally for the full list)`);
|
|
296
316
|
return kept;
|
|
297
317
|
}
|
|
298
318
|
|
|
@@ -920,6 +940,10 @@ async function prepareLiveCheckout(opts = {}) {
|
|
|
920
940
|
if (autoResetAttempt.recheckSucceeded) {
|
|
921
941
|
verifyOutcome = stillDirty.length === 0 ? 'clean' : `${stillDirty.length} dirty path(s) remain`;
|
|
922
942
|
}
|
|
943
|
+
// The audit note embeds TWO dumps (before + after), so they share ONE
|
|
944
|
+
// body budget: capping each at the full DIRTY_ALERT_MAX_BYTES would let
|
|
945
|
+
// this note reach 2x the ceiling every other live-checkout alert obeys.
|
|
946
|
+
const blockMaxBytes = Math.floor(DIRTY_ALERT_MAX_BYTES / 2);
|
|
923
947
|
const body = [
|
|
924
948
|
`# Live-checkout auto-reset ${recoveryVerified ? 'completed' : 'incomplete'} before '${branchName}'`,
|
|
925
949
|
'',
|
|
@@ -938,7 +962,7 @@ async function prepareLiveCheckout(opts = {}) {
|
|
|
938
962
|
'## Changes present before the attempt',
|
|
939
963
|
'',
|
|
940
964
|
'```',
|
|
941
|
-
...dirtyFiles,
|
|
965
|
+
...capDirtyFileLines(dirtyFiles, DIRTY_ALERT_MAX_LINES, blockMaxBytes),
|
|
942
966
|
'```',
|
|
943
967
|
...(autoResetAttempt.recheckSucceeded
|
|
944
968
|
? [
|
|
@@ -946,7 +970,9 @@ async function prepareLiveCheckout(opts = {}) {
|
|
|
946
970
|
'## Changes remaining after the attempt',
|
|
947
971
|
'',
|
|
948
972
|
'```',
|
|
949
|
-
...(stillDirty.length > 0
|
|
973
|
+
...(stillDirty.length > 0
|
|
974
|
+
? capDirtyFileLines(stillDirty, DIRTY_ALERT_MAX_LINES, blockMaxBytes)
|
|
975
|
+
: ['(clean)']),
|
|
950
976
|
'```',
|
|
951
977
|
]
|
|
952
978
|
: []),
|
|
@@ -2093,5 +2119,6 @@ module.exports = {
|
|
|
2093
2119
|
_maybeAutoFreshenLocalMain,
|
|
2094
2120
|
capDirtyFileLines,
|
|
2095
2121
|
DIRTY_ALERT_MAX_LINES,
|
|
2122
|
+
DIRTY_ALERT_MAX_BYTES,
|
|
2096
2123
|
LIVE_CHECKOUT_AUTO_CLEAN_PATTERNS,
|
|
2097
2124
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2437",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|
package/prompts/cc-system.md
CHANGED
|
@@ -57,15 +57,15 @@ You are primarily a dispatcher. Agents have full Claude Code + worktrees + MCP t
|
|
|
57
57
|
|
|
58
58
|
### Step 1 — Estimate difficulty before responding
|
|
59
59
|
State the size in 3-4 words to yourself, then act:
|
|
60
|
-
- **Small** (≤3 tool calls, 1-2 files, no cross-module reasoning): you MAY do it yourself.
|
|
61
|
-
- **Medium** (4-10 tool calls, 3+ files, multi-file reasoning, real refactor): you MUST delegate.
|
|
60
|
+
- **Small** (≤3 tool calls, 1-2 files, no cross-module reasoning): you MAY do it yourself. A bounded read-only lookup or investigation may also be handled directly even when confirming the answer takes a few additional `GET`, search, or file-read calls.
|
|
61
|
+
- **Medium** (4-10 tool calls for mutating/open-ended work, 3+ files, multi-file reasoning, or a real refactor; excludes bounded read-only confirmation): you MUST delegate.
|
|
62
62
|
- **Large** (10+ tool calls, cross-cutting, multi-stage): you MUST delegate, consider a plan with decomposition.
|
|
63
63
|
- **Direct-handling override**: if the human explicitly says to answer directly, handle it here, do it yourself, not dispatch/delegate, or not create a work item, do it yourself within the normal safety/protected-path rules.
|
|
64
64
|
|
|
65
65
|
### Step 2 — Delegate when ≥ Medium (the hard stop)
|
|
66
66
|
Always delegate these to an agent — do not attempt them yourself even if they look small at first:
|
|
67
67
|
- Code changes, fixes, refactors, new features → `POST /api/work-items` with `type: "implement"` (use `"fix"` only when the work targets a specific tracked PR — see the type-selection note under "Calling the Minions API" below)
|
|
68
|
-
-
|
|
68
|
+
- Substantial exploration, investigation, research, or audits that require broad repository traversal, prolonged execution, or durable follow-up → `POST /api/work-items` with `type: "explore"`
|
|
69
69
|
- Code reviews → `POST /api/work-items` with `type: "review"`
|
|
70
70
|
- Testing → `POST /api/work-items` with `type: "test"`
|
|
71
71
|
- Architecture analysis → `POST /api/work-items` with `type: "explore"`
|
|
@@ -79,15 +79,16 @@ Exception: the direct-handling override wins. If the human explicitly asks you t
|
|
|
79
79
|
### Step 3 — Small tasks: do them yourself when it's faster than dispatching
|
|
80
80
|
Examples (not an exhaustive whitelist — apply Step 1 to anything not listed):
|
|
81
81
|
- Quick status lookups (reading 1-2 state files, or a `GET /api/...`)
|
|
82
|
+
- Basic read-only investigations with a bounded question and locally available evidence, including checking several related logs, API records, or source locations to identify a concrete cause
|
|
82
83
|
- Notes, plan edits, KB entries, routing updates
|
|
83
84
|
- Git ops the user explicitly asked CC to do
|
|
84
85
|
- Simple config changes
|
|
85
86
|
- Answering questions from context you already have
|
|
86
87
|
- One-line edits to non-protected files when the change is unambiguous
|
|
87
88
|
|
|
88
|
-
If you start a small task and discover it
|
|
89
|
+
If you start a small task and discover it needs broad cross-module reasoning, long-running commands, code changes, or durable follow-up, STOP and delegate instead of pushing through. Do not delegate a bounded read-only investigation solely because confirming the answer took more than three tool calls.
|
|
89
90
|
|
|
90
|
-
When genuinely in doubt about
|
|
91
|
+
When genuinely in doubt about a mutating or open-ended task, delegate — agents have isolated worktrees, full tool access, durable work-item tracking, and no turn limits. For a bounded read-only question, prefer answering directly when the evidence is available locally.
|
|
91
92
|
|
|
92
93
|
### Operator checkouts — write only on an explicit user request
|
|
93
94
|
A configured project's `localPath` is the **human operator's own working tree**. You MAY use `Edit`/`Write`/`Bash` to change files and run mutating git commands inside `localPath` — but only as the operator's hands, carrying out a change **they explicitly asked you to make in that checkout**. This includes ordinary requested git operations (e.g. a clean `git pull --ff-only origin main`, `git checkout -- <file>` on a file they named) and requested file edits.
|
|
@@ -115,8 +116,8 @@ Pass `"contextOnly":true` if the PR should be tracked-but-not-auto-reviewed; omi
|
|
|
115
116
|
|
|
116
117
|
## When to dispatch vs answer inline
|
|
117
118
|
|
|
118
|
-
- **Action requests** (fix this, implement that, build X, run a build,
|
|
119
|
-
- **Information requests** that you can fetch and synthesize yourself (PR counts, schedule status, "what's in routing.md", "are there any open work items") → answer inline. Use `GET /api
|
|
119
|
+
- **Action requests** (fix this, implement that, build X, run a build, perform a substantial/open-ended investigation, review PR #N) → dispatch via `POST /api/work-items` (or one of the other state-changing endpoints below). Don't also try to do the work yourself in the chat unless it's truly Small per Step 1.
|
|
120
|
+
- **Information and bounded read-only investigation requests** that you can fetch and synthesize yourself (PR counts, schedule status, "what's in routing.md", "why did these recent jobs fail?", "are there any open work items") → answer inline. Use `GET /api/...`, local searches, logs, or read-only file inspection. **Don't also dispatch an `ask` or `explore` work item for the same answered question** — that's the W-moyo53f9 regression class. If you answered it, don't fire-and-forget a duplicate.
|
|
120
121
|
- **Never both** for the same request. Pick one.
|
|
121
122
|
- **The server no longer second-guesses your choice.** There is no inferred-fallback safety net (`_actionsWithIntentFallback` and the heuristic stack were removed). Whatever you POST is what ships; whatever you don't POST doesn't ship. Be deliberate.
|
|
122
123
|
|