claude-code-session-manager 0.121.0 → 0.122.0
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/dist/assets/{AgentLibrary-FO2ei2wB.js → AgentLibrary-BmJ8hIPw.js} +1 -1
- package/dist/assets/{Button-CeIOXUPU.js → Button-BS4kmz4p.js} +1 -1
- package/dist/assets/{DataModel-D0p__Hil.js → DataModel-DZHBZrfL.js} +1 -1
- package/dist/assets/History-pg00b2Gd.js +2 -0
- package/dist/assets/{Hooks-DkTnRHyT.js → Hooks-BTEs0UZY.js} +1 -1
- package/dist/assets/{Library-l7KS6pkE.js → Library-DuaJ-VSn.js} +1 -1
- package/dist/assets/{ListDetail-DDw8fuRT.js → ListDetail-Cud72s-O.js} +1 -1
- package/dist/assets/{McpServers-UUNIxk_3.js → McpServers-Dy-wnG_X.js} +1 -1
- package/dist/assets/{Memory-CNYUoQ8C.js → Memory-Bs1T1Jo7.js} +1 -1
- package/dist/assets/{Panel-Bb54soc2.js → Panel-8UhLXEOW.js} +1 -1
- package/dist/assets/{Permissions-D-dNdV3f.js → Permissions-BfwzvmBS.js} +1 -1
- package/dist/assets/Plugins-McjyvvKh.js +2 -0
- package/dist/assets/{ProvenanceBadge-DEAkDNjA.js → ProvenanceBadge-D0Ov_6WJ.js} +1 -1
- package/dist/assets/{RemoteMachines-xCEgvTGU.js → RemoteMachines-gOlFTvWq.js} +1 -1
- package/dist/assets/{SaveBar-BSEs7ecA.js → SaveBar-Cd6hJR6K.js} +1 -1
- package/dist/assets/Scheduler-BZXKqYJP.js +16 -0
- package/dist/assets/{ScopeSwitcher-DcaFyAV-.js → ScopeSwitcher-C24ysV1K.js} +1 -1
- package/dist/assets/{Settings-vcdWy_Jx.js → Settings-DWnN2qxn.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-CLrJWTPd.js → SkillReferenceGraph-BsWuiTbw.js} +1 -1
- package/dist/assets/{Skills-CYM4rBEC.js → Skills-6pHn6glu.js} +1 -1
- package/dist/assets/{SystemPrompt-BWNIu-iY.js → SystemPrompt-DT0LBH-K.js} +1 -1
- package/dist/assets/{TagLibrary-CYdwHK6m.js → TagLibrary-DbJ0zbYL.js} +1 -1
- package/dist/assets/{TiptapBody-Kt90lHrD.js → TiptapBody-C4kIvIMH.js} +1 -1
- package/dist/assets/{Toggle-D1OXWYOz.js → Toggle-pP19zlyk.js} +1 -1
- package/dist/assets/{ViewTabs-BeQR3CKh.js → ViewTabs-CpzN8e5I.js} +1 -1
- package/dist/assets/{historyTotals-B0qafrRo.js → historyTotals-Caq78bb5.js} +1 -1
- package/dist/assets/{index-Bq9uH08R.js → index-Bjtguv6I.js} +396 -396
- package/dist/assets/{settingsSchema-0ZksSAPB.js → settingsSchema-CC2ySu3d.js} +1 -1
- package/dist/assets/{skillFrontmatter-C0nh8yfa.js → skillFrontmatter-hDhNo8jE.js} +1 -1
- package/dist/assets/{useAgentsDirWatch-DPkoBybS.js → useAgentsDirWatch-BuOtY25U.js} +1 -1
- package/dist/assets/{useRemotePeers-BDJMUihy.js → useRemotePeers-CNQmRBpt.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/plugins/session-manager-dev/skills/develop/SKILL.md +7 -7
- package/plugins/session-manager-dev/skills/develop/standards.md +4 -10
- package/scripts/scheduler-mcp-server.cjs +2 -2
- package/src/main/build-info.json +4 -4
- package/src/main/chatRunner.cjs +13 -37
- package/src/main/docEdit.cjs +1 -0
- package/src/main/index.cjs +12 -13
- package/src/main/ipcSchemas.cjs +2 -2
- package/src/main/lib/classifyPromptTicket.cjs +1 -0
- package/src/main/lib/claude/effectiveModelInfo.cjs +9 -21
- package/src/main/lib/claude/personaToolScope.cjs +7 -1
- package/src/main/lib/claude/runClaudeP.cjs +1 -0
- package/src/main/lib/epic/delegationReadiness.cjs +75 -8
- package/src/main/lib/mcpToolCatalog.cjs +0 -1
- package/src/main/lib/persona-macro/shippedPersonaSeeds.cjs +11 -0
- package/src/main/lib/prd/prdCreate.cjs +3 -1
- package/src/main/lib/prd/prdSizing.cjs +1 -8
- package/src/main/lib/runSeeders.cjs +21 -0
- package/src/main/lib/scheduler/loopEfficiency.cjs +109 -0
- package/src/main/lib/scheduler/queueHistory.cjs +44 -1
- package/src/main/lib/scheduler/queueStore.cjs +4 -72
- package/src/main/lib/scheduler/rcaReport.cjs +1 -1
- package/src/main/lib/scheduler/refireCap.cjs +40 -0
- package/src/main/lib/scheduler/reviewNotice.cjs +1 -1
- package/src/main/lib/scheduler/schedulerConfig.cjs +1 -2
- package/src/main/lib/scheduler/schedulerPaths.cjs +0 -2
- package/src/main/lib/scheduler/sessionSlots.cjs +1 -1
- package/src/main/lib/telemetry/auditLog.cjs +65 -14
- package/src/main/loopEfficiencyHandlers.cjs +160 -0
- package/src/main/queueOps.cjs +4 -9
- package/src/main/runVerify.cjs +22 -454
- package/src/main/scheduler/reviewLadder.cjs +12 -2
- package/src/main/scheduler.cjs +103 -177
- package/src/main/seedDevPlugin.cjs +49 -22
- package/src/main/seedStatus.cjs +3 -2
- package/src/main/supervisor.cjs +1 -0
- package/src/main/templates/PRD_AUTHORING.md +16 -31
- package/src/preload/api.d.ts +26 -6
- package/src/preload/index.cjs +1 -0
- package/src/seed/agents/dev-lead.md +2 -9
- package/dist/assets/History-DVqQ5azN.js +0 -2
- package/dist/assets/Plugins-DhcIXRhQ.js +0 -2
- package/dist/assets/Scheduler-BIKJIU-C.js +0 -16
package/src/main/runVerify.cjs
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* runVerify.cjs — post-run reconciliation verifier.
|
|
3
3
|
*
|
|
4
4
|
* Called by scheduler.cjs after every agent exit=0, before marking a job
|
|
5
|
-
* "completed".
|
|
6
|
-
*
|
|
5
|
+
* "completed". The verdict depends only on outcomes — sentinel, commit
|
|
6
|
+
* evidence, exit code, log readability, dependency prerequisites. Transcript
|
|
7
|
+
* content is NEVER scanned for error text (pattern scan retired).
|
|
7
8
|
*
|
|
8
9
|
* Pure: no IPC, no queue writes. Writes a verdicts.json sidecar to runDir as
|
|
9
10
|
* a by-product so the renderer and any offline tooling can read the result
|
|
@@ -13,7 +14,7 @@
|
|
|
13
14
|
* verifyRun({ runDir, prdPath, queueEntry, allJobs }) → Promise<Verdict>
|
|
14
15
|
*
|
|
15
16
|
* Verdict shape:
|
|
16
|
-
* { verdict: 'clean'|'halt'|'deps_unmet'|'
|
|
17
|
+
* { verdict: 'clean'|'halt'|'deps_unmet'|'verify_unavailable'
|
|
17
18
|
* |'no_verdict_sentinel'|'abandoned_background_task',
|
|
18
19
|
* reason: string,
|
|
19
20
|
* downgradeTo: 'pending'|'needs_review'|null }
|
|
@@ -22,8 +23,8 @@
|
|
|
22
23
|
* clean → null (caller stamps 'completed')
|
|
23
24
|
* halt → 'pending' (re-fires automatically next slot)
|
|
24
25
|
* deps_unmet → 'pending' (re-fires when deps clear)
|
|
25
|
-
* transcript_errors →
|
|
26
|
-
* verify_unavailable → 'needs_review'
|
|
26
|
+
* transcript_errors → historical rows only; no longer emitted
|
|
27
|
+
* verify_unavailable → 'needs_review' (unreadable log / verifier threw)
|
|
27
28
|
* no_verdict_sentinel → 'needs_review'
|
|
28
29
|
* abandoned_background_task → 'needs_review'
|
|
29
30
|
*
|
|
@@ -48,29 +49,7 @@ const VERDICTS_SCHEMA_VERSION = 1;
|
|
|
48
49
|
// Bound on the `gh pr view` postcondition check below (§ merge-main exemption).
|
|
49
50
|
const GH_CHECK_TIMEOUT_MS = 15_000;
|
|
50
51
|
|
|
51
|
-
// ───
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Detect high-signal error patterns in a single tool_result content string.
|
|
55
|
-
* Returns { verdict, pattern } on match, null if clean.
|
|
56
|
-
*
|
|
57
|
-
* Checked in priority order (cheapest first):
|
|
58
|
-
* 1. FAIL/FATAL at line start
|
|
59
|
-
* 2. Traceback + Error within 10 lines (Python exception)
|
|
60
|
-
* 3. ModuleNotFoundError / ImportError (missing venv / broken deps)
|
|
61
|
-
*/
|
|
62
|
-
/**
|
|
63
|
-
* True when a tool_result content is a Claude Code harness tool error rather
|
|
64
|
-
* than task output — emitted when the model calls a tool that doesn't exist or
|
|
65
|
-
* isn't allowed (e.g. `<tool_use_error>Error: No such tool available: bash`).
|
|
66
|
-
* The harness rejects the call; the model recovers by retrying with a valid
|
|
67
|
-
* tool. Never a task failure, so the verifier must not downgrade on it.
|
|
68
|
-
*/
|
|
69
|
-
function isHarnessToolError(content) {
|
|
70
|
-
if (typeof content !== 'string' || !content) return false;
|
|
71
|
-
return content.includes('<tool_use_error>')
|
|
72
|
-
|| /\bNo such tool available\b/.test(content);
|
|
73
|
-
}
|
|
52
|
+
// ─── policy-denial detector ──────────────────────────────────────────────────
|
|
74
53
|
|
|
75
54
|
/**
|
|
76
55
|
* A PreToolUse hook denial (`guard-destructive-git.cjs`, `guard-prd-writes.cjs`,
|
|
@@ -85,51 +64,6 @@ function isPolicyDenial(content) {
|
|
|
85
64
|
return /^(?:Error:\s*)?Blocked:/.test(content);
|
|
86
65
|
}
|
|
87
66
|
|
|
88
|
-
function detectPattern(content) {
|
|
89
|
-
if (typeof content !== 'string' || !content) return null;
|
|
90
|
-
|
|
91
|
-
// (1) FAIL/FATAL at the start of a line (case-sensitive, multiline).
|
|
92
|
-
if (/^FAIL\b/m.test(content) || /^FATAL\b/m.test(content)) {
|
|
93
|
-
return { verdict: 'transcript_errors', pattern: 'FAIL/FATAL at line start' };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// (2) Python Traceback + exception line within next 10 lines. Both anchored
|
|
97
|
-
// to line starts: reviewer prose quoting "will crash with ImportError" or
|
|
98
|
-
// embedding "...Error:" mid-sentence must not match (feedback 2026-06-10-01).
|
|
99
|
-
//
|
|
100
|
-
// The TERMINATING exception decides the class: a Traceback ending in
|
|
101
|
-
// ModuleNotFoundError/ImportError is the missing-dependency class ("the
|
|
102
|
-
// verification couldn't run", same as detector 3), NOT a logic failure — so
|
|
103
|
-
// it routes through the weaker verify_unavailable path (env-recovery escape
|
|
104
|
-
// hatch + success demotion). A Traceback ending in any other exception
|
|
105
|
-
// (KeyError, AssertionError, …) stays transcript_errors — that is the real
|
|
106
|
-
// false-PASS class the verifier exists to catch (2026-05-23 incident).
|
|
107
|
-
// (feedback 2026-06-10 addendum: interpreter-search setup probes that ended
|
|
108
|
-
// in ModuleNotFoundError were 3/3 false positives.)
|
|
109
|
-
const lines = content.split('\n');
|
|
110
|
-
for (let i = 0; i < lines.length; i++) {
|
|
111
|
-
if (/^\s*Traceback \(most recent call last\):/.test(lines[i])) {
|
|
112
|
-
for (let j = i + 1; j < Math.min(i + 11, lines.length); j++) {
|
|
113
|
-
const m = lines[j].match(/^\s*([A-Za-z_][\w.]*(?:Error|Exception))\s*:/);
|
|
114
|
-
if (m) {
|
|
115
|
-
if (m[1] === 'ModuleNotFoundError' || m[1] === 'ImportError') {
|
|
116
|
-
return { verdict: 'verify_unavailable', pattern: `Traceback → ${m[1]}` };
|
|
117
|
-
}
|
|
118
|
-
return { verdict: 'transcript_errors', pattern: 'Traceback + Error within 10 lines' };
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// (3) Import / module errors (verification was skipped). Line-anchored:
|
|
125
|
-
// real interpreter output starts the line with the exception name
|
|
126
|
-
// ("ModuleNotFoundError: No module named 'x'"); prose never does.
|
|
127
|
-
if (/^\s*(?:ModuleNotFoundError|ImportError)\s*(?::|$)/m.test(content)) {
|
|
128
|
-
return { verdict: 'verify_unavailable', pattern: 'ModuleNotFoundError/ImportError' };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
67
|
|
|
134
68
|
// ─── log parser ───────────────────────────────────────────────────────────────
|
|
135
69
|
|
|
@@ -274,166 +208,17 @@ function hasAbandonedBackgroundTask(events) {
|
|
|
274
208
|
return false;
|
|
275
209
|
}
|
|
276
210
|
|
|
277
|
-
// ───
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Return the description of the tool_use that produced a given tool_result
|
|
281
|
-
* (matched by tool_use_id). Returns '' if not found.
|
|
282
|
-
*/
|
|
283
|
-
function toolUseDesc(events, toolUseId) {
|
|
284
|
-
if (!toolUseId) return '';
|
|
285
|
-
for (const ev of events) {
|
|
286
|
-
if (ev.kind === 'tool_use' && ev.toolUseId === toolUseId) return ev.description ?? '';
|
|
287
|
-
}
|
|
288
|
-
return '';
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Return the tool name of the tool_use that produced a given tool_result.
|
|
293
|
-
* Returns '' if not found.
|
|
294
|
-
*/
|
|
295
|
-
function toolUseName(events, toolUseId) {
|
|
296
|
-
if (!toolUseId) return '';
|
|
297
|
-
for (const ev of events) {
|
|
298
|
-
if (ev.kind === 'tool_use' && ev.toolUseId === toolUseId) return ev.toolName ?? '';
|
|
299
|
-
}
|
|
300
|
-
return '';
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Check whether the next ≤5 tool_use calls after `fromSeq` include a package
|
|
305
|
-
* install command (pip install, pip3 install, uv sync, uv pip install).
|
|
306
|
-
*/
|
|
307
|
-
function hasInstallRecovery(events, fromSeq) {
|
|
308
|
-
let count = 0;
|
|
309
|
-
for (const ev of events) {
|
|
310
|
-
if (ev.seq <= fromSeq) continue;
|
|
311
|
-
if (ev.kind !== 'tool_use') continue;
|
|
312
|
-
if (++count > 5) break;
|
|
313
|
-
const combined = `${ev.command ?? ''} ${ev.description ?? ''}`;
|
|
314
|
-
if (/pip\d*\s+install/i.test(combined) || /uv\s+(?:sync|pip\s+install)/i.test(combined)) {
|
|
315
|
-
return true;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
return false;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Matches a leading `sleep <N> &&` or `sleep <N> ;` wrapper (the shell idiom
|
|
322
|
-
// used to poll a still-pending remote job) so a retry that only drops/changes
|
|
323
|
-
// its own timing wrapper still counts as the same underlying command. Narrow
|
|
324
|
-
// on purpose: it strips nothing else, so two genuinely different commands
|
|
325
|
-
// never collapse into a false pairing (incident: RCA 745-pr188-ci-lint-docs-integrity).
|
|
326
|
-
const SLEEP_PREFIX_RE = /^\s*sleep\s+\d+\s*(&&|;)\s*/;
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Strip a leading `sleep <N> (&&|;)` wrapper from a tool_use description so
|
|
330
|
-
* self-recovery comparisons ignore only that timing-poll idiom.
|
|
331
|
-
*/
|
|
332
|
-
function normalizeDescForRecovery(desc) {
|
|
333
|
-
return (desc ?? '').replace(SLEEP_PREFIX_RE, '');
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Check whether the error at `errorSeq` is self-recovered within the next 30
|
|
338
|
-
* events.
|
|
339
|
-
*
|
|
340
|
-
* Recovery heuristic: another tool_use with the same non-empty `description`
|
|
341
|
-
* (after stripping a leading `sleep N &&`/`sleep N;` wrapper from both sides)
|
|
342
|
-
* appears within 30 events of the error, AND its corresponding tool_result is
|
|
343
|
-
* free of error patterns AND is not is_error=true.
|
|
344
|
-
*/
|
|
345
|
-
function isSelfRecovered(events, errorSeq, desc) {
|
|
346
|
-
if (!desc) return false;
|
|
347
|
-
const normalizedDesc = normalizeDescForRecovery(desc);
|
|
348
|
-
if (!normalizedDesc) return false;
|
|
349
|
-
|
|
350
|
-
// Build a quick lookup from tool_use_id → tool_result event.
|
|
351
|
-
const resultByUseId = new Map();
|
|
352
|
-
for (const ev of events) {
|
|
353
|
-
if (ev.kind === 'tool_result') resultByUseId.set(ev.toolUseId, ev);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
let seen = 0;
|
|
357
|
-
for (const ev of events) {
|
|
358
|
-
if (ev.seq <= errorSeq) continue;
|
|
359
|
-
if (++seen > 30) break;
|
|
360
|
-
if (ev.kind !== 'tool_use' || normalizeDescForRecovery(ev.description) !== normalizedDesc) continue;
|
|
361
|
-
const result = resultByUseId.get(ev.toolUseId);
|
|
362
|
-
if (result && !result.isError && !detectPattern(result.content)) return true;
|
|
363
|
-
}
|
|
364
|
-
return false;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// ─── soak / deps parsing ─────────────────────────────────────────────────────
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Extract soak days from a text string.
|
|
371
|
-
*
|
|
372
|
-
* Accepted forms (case-insensitive):
|
|
373
|
-
* "30+ days", "30 days", "≥ 180 days", "180-day soak",
|
|
374
|
-
* "N days of soak", "N days each", "soak of N days"
|
|
375
|
-
*
|
|
376
|
-
* Returns { soakDays, soakPhrase } or null.
|
|
377
|
-
* Refuses to parse if the unit is not days (surfaces as verify_unavailable
|
|
378
|
-
* at the dep-check call site).
|
|
379
|
-
*/
|
|
380
|
-
function extractSoakFromBody(body) {
|
|
381
|
-
const PATTERNS = [
|
|
382
|
-
/(\d+)\+?\s*-?day\s+soak/i,
|
|
383
|
-
/soak\s+of\s+(\d+)\+?\s*days?/i,
|
|
384
|
-
/≥\s*(\d+)\s+days?/i,
|
|
385
|
-
/(\d+)\+?\s*days?\s+(?:of\s+)?soak/i,
|
|
386
|
-
/(\d+)\+?\s*days?\s+each/i,
|
|
387
|
-
/only\s+after\s+\S+.*?(?:run\s+)?(\d+)\+?\s*days?/i,
|
|
388
|
-
];
|
|
389
|
-
for (const re of PATTERNS) {
|
|
390
|
-
const m = body.match(re);
|
|
391
|
-
if (m && m[1]) {
|
|
392
|
-
const n = parseInt(m[1], 10);
|
|
393
|
-
if (!isNaN(n) && n > 0 && n <= 3650) return { soakDays: n, soakPhrase: m[0].trim() };
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
return null;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* Parse dependency slug fragments from a PRD body:
|
|
401
|
-
* - Lines under "# Dependencies", "# Depends on", "# Pre-condition", "# Blocked on"
|
|
402
|
-
* - Inline "This PRD is blocked on: - M2 — ..."
|
|
403
|
-
*
|
|
404
|
-
* Returns a deduplicated array of lowercase fragment strings.
|
|
405
|
-
*/
|
|
406
|
-
function parsePrdBodyDepFragments(body) {
|
|
407
|
-
const frags = [];
|
|
408
|
-
|
|
409
|
-
const SECTION_RE = /^#+\s*(?:depends?\s+on|dependencies|pre-?conditions?|blocked\s+on)[^\n]*\n([\s\S]*?)(?=\n#|$)/im;
|
|
410
|
-
const INLINE_RE = /(?:this\s+prd\s+is\s+blocked\s+on|depends?\s+on)\s*:?\s+([\s\S]*?)(?=\n\n|$)/i;
|
|
411
|
-
|
|
412
|
-
const sections = [];
|
|
413
|
-
const sm = body.match(SECTION_RE);
|
|
414
|
-
if (sm) sections.push(sm[1]);
|
|
415
|
-
const im = body.match(INLINE_RE);
|
|
416
|
-
if (im) sections.push(im[1]);
|
|
417
|
-
|
|
418
|
-
for (const section of sections) {
|
|
419
|
-
for (const m of section.matchAll(/^\s*[-*]\s*([A-Za-z][A-Za-z0-9._-]+)/gm)) {
|
|
420
|
-
const frag = m[1].toLowerCase();
|
|
421
|
-
if (frag.length >= 2) frags.push(frag);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
return [...new Set(frags)];
|
|
425
|
-
}
|
|
211
|
+
// ─── dependency check ────────────────────────────────────────────────────────
|
|
426
212
|
|
|
427
213
|
/**
|
|
428
214
|
* Verify that all declared dependencies for `queueEntry` are satisfied.
|
|
429
215
|
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
* 2. PRD body deps section (prose slug fragments + optional soak period).
|
|
216
|
+
* One layer: queue.json's `dependsOn` array — the same and only dependency
|
|
217
|
+
* signal the dispatcher (findBlockingDep) uses. PRD body prose is NOT parsed.
|
|
433
218
|
*
|
|
434
|
-
* @returns {{ ok: boolean, reason?: string
|
|
219
|
+
* @returns {{ ok: boolean, reason?: string }}
|
|
435
220
|
*/
|
|
436
|
-
function checkDeps(queueEntry, allJobs
|
|
221
|
+
function checkDeps(queueEntry, allJobs) {
|
|
437
222
|
const bySlug = new Map(allJobs.map((j) => [j.slug, j]));
|
|
438
223
|
// A PRD's filename carries the allocator's `NN-` prefix, but `dependsOn:` is
|
|
439
224
|
// authored with the bare name (the author cannot know the number that will
|
|
@@ -477,49 +262,6 @@ function checkDeps(queueEntry, allJobs, prdBody) {
|
|
|
477
262
|
}
|
|
478
263
|
}
|
|
479
264
|
|
|
480
|
-
// 2. PRD body deps (prose).
|
|
481
|
-
if (prdBody) {
|
|
482
|
-
const frags = parsePrdBodyDepFragments(prdBody);
|
|
483
|
-
const soak = extractSoakFromBody(prdBody);
|
|
484
|
-
let latestFinishedMs = null;
|
|
485
|
-
|
|
486
|
-
for (const frag of frags) {
|
|
487
|
-
// Substring match: "m7" matches "44-signal-builder-m7-final-cutover".
|
|
488
|
-
const matches = allJobs.filter(
|
|
489
|
-
(j) => j.slug !== queueEntry.slug && j.slug.toLowerCase().includes(frag),
|
|
490
|
-
);
|
|
491
|
-
if (matches.length === 0) continue; // unresolvable fragment — skip
|
|
492
|
-
|
|
493
|
-
for (const dep of matches) {
|
|
494
|
-
if (!isSatisfiedDepRow(dep)) {
|
|
495
|
-
return {
|
|
496
|
-
ok: false,
|
|
497
|
-
reason: `PRD body dep "${frag}" → "${dep.slug}" is ${dep.status} (need completed)`,
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
if (dep.finishedAt) {
|
|
501
|
-
const t = new Date(dep.finishedAt).getTime();
|
|
502
|
-
if (!isNaN(t) && (latestFinishedMs === null || t > latestFinishedMs)) {
|
|
503
|
-
latestFinishedMs = t;
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// Soak period check.
|
|
510
|
-
if (soak && latestFinishedMs !== null) {
|
|
511
|
-
const floorMs = latestFinishedMs + soak.soakDays * 86_400_000;
|
|
512
|
-
if (Date.now() < floorMs) {
|
|
513
|
-
const floorDate = new Date(floorMs).toISOString().slice(0, 10);
|
|
514
|
-
return {
|
|
515
|
-
ok: false,
|
|
516
|
-
reason: `Soak not elapsed: "${soak.soakPhrase}" requires waiting until ${floorDate}`,
|
|
517
|
-
floorDate,
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
265
|
return { ok: true };
|
|
524
266
|
}
|
|
525
267
|
|
|
@@ -895,12 +637,7 @@ function artifactsCheck({ cwd, paths, startedAt, finishedAt, fsImpl = fs }) {
|
|
|
895
637
|
* @param {boolean} [params.committedDuringRun] True when HEAD moved during the run,
|
|
896
638
|
* confirming the job's commit landed.
|
|
897
639
|
* Default false for back-compat.
|
|
898
|
-
*
|
|
899
|
-
* with no SCHEDULER_VERDICT: FAIL is
|
|
900
|
-
* sufficient to override weak verdicts
|
|
901
|
-
* (transcript_errors/verify_unavailable)
|
|
902
|
-
* even without a PASS sentinel. Only
|
|
903
|
-
* set by the boot reverify self-heal
|
|
640
|
+
* (`allowPreSentinelHeal` is still passed by one caller but is ignored: the pattern verdicts it healed no longer exist.)
|
|
904
641
|
* pass for pre-sentinel legacy runs.
|
|
905
642
|
* @param {Function} [params.ghExecImpl] Test-only override for the `gh pr view`
|
|
906
643
|
* subprocess call used by the -merge-main
|
|
@@ -931,7 +668,7 @@ function artifactsCheck({ cwd, paths, startedAt, finishedAt, fsImpl = fs }) {
|
|
|
931
668
|
* @returns {Promise<{verdict:string, reason:string, downgradeTo:string|null}>}
|
|
932
669
|
*/
|
|
933
670
|
async function verifyRun({
|
|
934
|
-
runDir, prdPath, queueEntry, allJobs = [], committedDuringRun = false,
|
|
671
|
+
runDir, prdPath, queueEntry, allJobs = [], committedDuringRun = false,
|
|
935
672
|
ghExecImpl, priorLandedCommit = null, jobLandedCommitThisRun = null, exitCode = null, worktreeDir = null,
|
|
936
673
|
}) {
|
|
937
674
|
const { slug } = queueEntry;
|
|
@@ -971,11 +708,9 @@ async function verifyRun({
|
|
|
971
708
|
|
|
972
709
|
// ── 1. Dependency check ────────────────────────────────────────────────
|
|
973
710
|
// Run first: if deps are unmet we can skip the expensive log parse.
|
|
974
|
-
const depsResult = checkDeps(queueEntry, allJobs
|
|
711
|
+
const depsResult = checkDeps(queueEntry, allJobs);
|
|
975
712
|
if (!depsResult.ok) {
|
|
976
|
-
return conclude('deps_unmet', depsResult.reason, 'pending'
|
|
977
|
-
soakFloorDate: depsResult.floorDate ?? null,
|
|
978
|
-
});
|
|
713
|
+
return conclude('deps_unmet', depsResult.reason, 'pending');
|
|
979
714
|
}
|
|
980
715
|
|
|
981
716
|
// ── 2. Parse log ───────────────────────────────────────────────────────
|
|
@@ -1021,108 +756,20 @@ async function verifyRun({
|
|
|
1021
756
|
}
|
|
1022
757
|
}
|
|
1023
758
|
|
|
1024
|
-
// ── 4.
|
|
1025
|
-
|
|
1026
|
-
|
|
759
|
+
// ── 4. Policy-denial annotations ──────────────────────────────────────
|
|
760
|
+
// The verdict depends only on outcomes (sentinel, commit evidence, exit,
|
|
761
|
+
// log readability) — transcript content is never scanned for error text.
|
|
762
|
+
// A PreToolUse denial is still recorded as a non-blocking annotation.
|
|
1027
763
|
const issues = [];
|
|
1028
|
-
// Non-blocking notes: signals worth recording but not strong enough to
|
|
1029
|
-
// downgrade (e.g. a missing-dependency probe in a run that still succeeded).
|
|
1030
764
|
const annotations = [];
|
|
1031
|
-
const runSucceeded = !!resultEvent && resultEvent.subtype === 'success';
|
|
1032
|
-
|
|
1033
765
|
for (let i = 0; i < events.length; i++) {
|
|
1034
766
|
const ev = events[i];
|
|
1035
|
-
if (ev.kind !== 'tool_result') continue;
|
|
1036
|
-
|
|
1037
|
-
// Harness tool errors (`<tool_use_error>…`) are emitted when the model
|
|
1038
|
-
// requests a tool that isn't available — e.g. a wrong-case name like
|
|
1039
|
-
// "bash" instead of "Bash", or a tool outside the allowlist. The harness
|
|
1040
|
-
// rejects the call and the model retries with a valid tool; the task is
|
|
1041
|
-
// unaffected. These are never task failures, so they are exempt from both
|
|
1042
|
-
// the is_error scan and the content pattern scan (false-positive class
|
|
1043
|
-
// seen in 58-web-remote-correctness-batch, 2026-06-10).
|
|
1044
|
-
if (isHarnessToolError(ev.content)) continue;
|
|
1045
|
-
|
|
1046
|
-
// A PreToolUse policy denial (a guard hook blocked the call before it
|
|
1047
|
-
// ran) is exempt from both the is_error scan and the content-pattern
|
|
1048
|
-
// scan for the same reason as a harness tool error above — the denied
|
|
1049
|
-
// call never executed. Unlike a harness tool error, record it as an
|
|
1050
|
-
// annotation so the denial stays visible in the verdict record instead
|
|
1051
|
-
// of vanishing silently.
|
|
767
|
+
if (ev.kind !== 'tool_result' || ev.parentToolUseId) continue;
|
|
1052
768
|
if (isPolicyDenial(ev.content)) {
|
|
1053
769
|
annotations.push({
|
|
1054
770
|
verdict: 'policy_denial',
|
|
1055
771
|
reason: `PreToolUse policy denial at event ${i}: ${ev.content.slice(0, 200)}`,
|
|
1056
772
|
});
|
|
1057
|
-
continue;
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
// A tool_result carrying a non-null parent_tool_use_id happened INSIDE a Task
|
|
1061
|
-
// subagent's own execution, not in the main agent's. Subagents do ordinary
|
|
1062
|
-
// exploratory work (greps that exit 1 on no-match, ls on a path that may not
|
|
1063
|
-
// exist) whose failures say nothing about whether the parent run succeeded —
|
|
1064
|
-
// the same false-positive class the Task exemption below already covers for the
|
|
1065
|
-
// content-pattern scan, which sits after this branch and so never reached
|
|
1066
|
-
// is_error. The subagent's OWN final Task tool_result has parentToolUseId ===
|
|
1067
|
-
// null and is still scanned, so a genuinely failed subagent still surfaces.
|
|
1068
|
-
// (Incident: 955-route-epic-create-through-ipc, 2026-08-03 — a fully green,
|
|
1069
|
-
// committed run parked in needs_review over a code-reviewer subagent's
|
|
1070
|
-
// no-match grep at event 450/499.)
|
|
1071
|
-
if (ev.parentToolUseId) continue;
|
|
1072
|
-
|
|
1073
|
-
// is_error:true in the final 20% of the transcript.
|
|
1074
|
-
if (ev.isError && i >= last20pctStart) {
|
|
1075
|
-
const desc = toolUseDesc(events, ev.toolUseId);
|
|
1076
|
-
if (!isSelfRecovered(events, ev.seq, desc)) {
|
|
1077
|
-
issues.push({
|
|
1078
|
-
verdict: 'transcript_errors',
|
|
1079
|
-
reason: `is_error=true in final 20% of transcript (event ${i}/${total})`,
|
|
1080
|
-
priority: 2,
|
|
1081
|
-
});
|
|
1082
|
-
continue; // is_error already covers any content patterns
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
if (!ev.content) continue;
|
|
1087
|
-
|
|
1088
|
-
// Subagent (Task) results are structured prose — review findings that
|
|
1089
|
-
// *describe* exceptions ("will crash with ImportError") are the dominant
|
|
1090
|
-
// false-positive source (feedback 2026-06-10-01). Real runtime errors
|
|
1091
|
-
// surface through Bash/test tool_results, which are still scanned.
|
|
1092
|
-
if (toolUseName(events, ev.toolUseId) === 'Task') continue;
|
|
1093
|
-
|
|
1094
|
-
const hit = detectPattern(ev.content);
|
|
1095
|
-
if (!hit) continue;
|
|
1096
|
-
|
|
1097
|
-
const desc = toolUseDesc(events, ev.toolUseId);
|
|
1098
|
-
|
|
1099
|
-
if (hit.verdict === 'verify_unavailable') {
|
|
1100
|
-
// ModuleNotFoundError/ImportError: first check for pip/uv install in
|
|
1101
|
-
// the next ≤5 tool_use calls (the agent may have self-healed).
|
|
1102
|
-
if (!hasInstallRecovery(events, ev.seq) && !isSelfRecovered(events, ev.seq, desc)) {
|
|
1103
|
-
const note = `${hit.pattern} at event ${i}, no install recovery found`;
|
|
1104
|
-
if (runSucceeded) {
|
|
1105
|
-
// "Verification couldn't run" is the weakest signal. When the run
|
|
1106
|
-
// still reached a genuine result:success, the agent resolved its
|
|
1107
|
-
// environment (often an interpreter/venv search the recovery
|
|
1108
|
-
// heuristics above don't model) and finished — record it as an
|
|
1109
|
-
// annotation, do NOT downgrade. transcript_errors (real logic/test
|
|
1110
|
-
// failures) are never demoted this way, so the false-PASS guard is
|
|
1111
|
-
// intact. (feedback 2026-06-10 addendum.)
|
|
1112
|
-
annotations.push({ verdict: 'verify_unavailable', reason: note });
|
|
1113
|
-
} else {
|
|
1114
|
-
issues.push({ verdict: 'verify_unavailable', reason: note, priority: 1 });
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
} else {
|
|
1118
|
-
// transcript_errors (FAIL/FATAL/Traceback): self-recovery escape hatch.
|
|
1119
|
-
if (!isSelfRecovered(events, ev.seq, desc)) {
|
|
1120
|
-
issues.push({
|
|
1121
|
-
verdict: 'transcript_errors',
|
|
1122
|
-
reason: `${hit.pattern} at event ${i} (no self-recovery)`,
|
|
1123
|
-
priority: 2,
|
|
1124
|
-
});
|
|
1125
|
-
}
|
|
1126
773
|
}
|
|
1127
774
|
}
|
|
1128
775
|
|
|
@@ -1156,37 +803,6 @@ async function verifyRun({
|
|
|
1156
803
|
);
|
|
1157
804
|
}
|
|
1158
805
|
|
|
1159
|
-
// Ground truth outranks heuristics: an exit-0 run that landed a commit
|
|
1160
|
-
// ATTRIBUTABLE TO THIS RUN (spawnJob's own guardHeadBefore/headAtExit
|
|
1161
|
-
// delta, or the repo-wide committedDuringRun window scan) and never
|
|
1162
|
-
// printed an explicit SCHEDULER_VERDICT: FAIL demotes every
|
|
1163
|
-
// transcript_errors/verify_unavailable hit from `issues` to `annotations`
|
|
1164
|
-
// — a tool error the agent already recovered from three calls later, or
|
|
1165
|
-
// an environment probe, says nothing once the work is materially proven
|
|
1166
|
-
// to have landed. Deliberately narrower than the sentinel-override below:
|
|
1167
|
-
// it needs no PASS claim at all, only the ABSENCE of an explicit FAIL, so
|
|
1168
|
-
// it also catches the majority no-sentinel-at-all park class that the
|
|
1169
|
-
// PASS+commitEvidence override (further below) can never reach. Never
|
|
1170
|
-
// demotes pass_no_commit/no_verdict_sentinel/abandoned_background_task —
|
|
1171
|
-
// those are added below this point and are about the ABSENCE of a
|
|
1172
|
-
// sentinel/commit, which this same evidence does not resolve.
|
|
1173
|
-
// (Incident: 1218-fo-01 — a `git apply` "patch does not apply" tool error
|
|
1174
|
-
// at event 179/209 was retried and succeeded 3 events later, the run
|
|
1175
|
-
// landed commit d1edf15, and the row was parked needs_review anyway,
|
|
1176
|
-
// spawning a $3.74 fix-plan investigation into work that needed no fix.)
|
|
1177
|
-
const groundTruthOutranksHeuristics = exitCode === 0 && commitEvidence && sentinel !== 'fail';
|
|
1178
|
-
if (groundTruthOutranksHeuristics) {
|
|
1179
|
-
for (let k = issues.length - 1; k >= 0; k--) {
|
|
1180
|
-
if (issues[k].verdict === 'transcript_errors' || issues[k].verdict === 'verify_unavailable') {
|
|
1181
|
-
annotations.push({
|
|
1182
|
-
verdict: issues[k].verdict,
|
|
1183
|
-
reason: `${issues[k].reason} (demoted: exit 0 + commit landed this run outranks transcript heuristics)`,
|
|
1184
|
-
});
|
|
1185
|
-
issues.splice(k, 1);
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
806
|
const sentinelFields = sentinel ? { sentinel } : {};
|
|
1191
807
|
const extras = (annotations.length || sentinel)
|
|
1192
808
|
? { ...(annotations.length ? { annotations } : {}), ...sentinelFields }
|
|
@@ -1384,51 +1000,10 @@ async function verifyRun({
|
|
|
1384
1000
|
return conclude('clean', reason, null, extras);
|
|
1385
1001
|
}
|
|
1386
1002
|
|
|
1387
|
-
//
|
|
1388
|
-
// no_verdict_sentinel; ties keep the loop's original order via stable sort).
|
|
1003
|
+
// Highest-priority outcome issue wins (stable sort keeps loop order on ties).
|
|
1389
1004
|
issues.sort((a, b) => b.priority - a.priority);
|
|
1390
1005
|
const top = issues[0];
|
|
1391
1006
|
|
|
1392
|
-
// Sentinel override: SCHEDULER_VERDICT: PASS + a commit that landed during
|
|
1393
|
-
// the run is authoritative evidence the job succeeded. Suppresses incidental
|
|
1394
|
-
// transcript noise (grep results with "Error", TDD red-phase reproductions,
|
|
1395
|
-
// Traceback in debug output) for the two weakest verdict classes.
|
|
1396
|
-
// MUST NOT apply to halt or deps_unmet — those keep their existing semantics.
|
|
1397
|
-
if (
|
|
1398
|
-
sentinel === 'pass'
|
|
1399
|
-
&& commitEvidence
|
|
1400
|
-
&& (top.verdict === 'transcript_errors' || top.verdict === 'verify_unavailable')
|
|
1401
|
-
) {
|
|
1402
|
-
return conclude('clean',
|
|
1403
|
-
`SCHEDULER_VERDICT: PASS + commit landed overrides ${top.verdict}`,
|
|
1404
|
-
null,
|
|
1405
|
-
{
|
|
1406
|
-
...(annotations.length ? { annotations } : {}),
|
|
1407
|
-
sentinel,
|
|
1408
|
-
sentinelOverride: top.verdict,
|
|
1409
|
-
...(commitEvidenceSource ? { commitEvidenceSource } : {}),
|
|
1410
|
-
},
|
|
1411
|
-
);
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
// Pre-sentinel heal: job predates SCHEDULER_VERDICT emission. A commit in
|
|
1415
|
-
// the run window with no explicit FAIL sentinel is weak but sufficient to
|
|
1416
|
-
// override the two weakest verdict classes during the self-heal pass.
|
|
1417
|
-
// Only applies when the caller opts in (allowPreSentinelHeal=true) — live
|
|
1418
|
-
// runs never set this, so only the boot reverify self-heal uses it.
|
|
1419
|
-
if (
|
|
1420
|
-
allowPreSentinelHeal
|
|
1421
|
-
&& committedDuringRun
|
|
1422
|
-
&& sentinel !== 'fail'
|
|
1423
|
-
&& (top.verdict === 'transcript_errors' || top.verdict === 'verify_unavailable')
|
|
1424
|
-
) {
|
|
1425
|
-
return conclude('clean',
|
|
1426
|
-
`pre-sentinel heal: committed in run window, no SCHEDULER_VERDICT: FAIL, overrides ${top.verdict}`,
|
|
1427
|
-
null,
|
|
1428
|
-
{ ...(annotations.length ? { annotations } : {}), preSentinelHeal: top.verdict },
|
|
1429
|
-
);
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
1007
|
return conclude(top.verdict, top.reason, 'needs_review', extras);
|
|
1433
1008
|
|
|
1434
1009
|
} catch (e) {
|
|
@@ -1443,14 +1018,7 @@ async function verifyRun({
|
|
|
1443
1018
|
module.exports = {
|
|
1444
1019
|
verifyRun,
|
|
1445
1020
|
// Exposed for unit tests.
|
|
1446
|
-
detectPattern,
|
|
1447
|
-
isHarnessToolError,
|
|
1448
1021
|
isPolicyDenial,
|
|
1449
|
-
isSelfRecovered,
|
|
1450
|
-
normalizeDescForRecovery,
|
|
1451
|
-
toolUseName,
|
|
1452
|
-
extractSoakFromBody,
|
|
1453
|
-
parsePrdBodyDepFragments,
|
|
1454
1022
|
checkDeps,
|
|
1455
1023
|
parseLog,
|
|
1456
1024
|
scanSentinel,
|
|
@@ -35,6 +35,13 @@ const { DEFAULT_PROJECT_CWD } = require('../lib/scheduler/schedulerBatch.cjs');
|
|
|
35
35
|
* exactly (always returns null), mirroring SM_RCA_DISABLE/SM_DOD_DISABLE.
|
|
36
36
|
*/
|
|
37
37
|
/** A row parked on a question only the human can answer — no automatic rung may touch it. */
|
|
38
|
+
// PRD refire-cap: a row parked by the post-run re-fire cap stays parked for the
|
|
39
|
+
// Epic's grouped notice. Every automated rung (reset, re-verify, auto-fix,
|
|
40
|
+
// auto-skip) consults this ONE predicate.
|
|
41
|
+
function isRefireCapped(job) {
|
|
42
|
+
return !!job && job.verifierVerdict === 'refire_capped';
|
|
43
|
+
}
|
|
44
|
+
|
|
38
45
|
function isNeedsHuman(job) {
|
|
39
46
|
return !!job && job.verifierVerdict === 'needs_human';
|
|
40
47
|
}
|
|
@@ -399,6 +406,7 @@ function isStrandedAutoFixPark(job, jobsInProject) {
|
|
|
399
406
|
* Exported for tests.
|
|
400
407
|
*/
|
|
401
408
|
function isEligibleForNeedsReviewAutoResolve(job, jobsInProject = []) {
|
|
409
|
+
if (isRefireCapped(job)) return false;
|
|
402
410
|
return isExhaustedAutoFix(job) || isGuardParkedWithoutAutoFix(job) || isStrandedAutoFixPark(job, jobsInProject);
|
|
403
411
|
}
|
|
404
412
|
|
|
@@ -534,7 +542,7 @@ function isFailedUnverifiedShaped(job) {
|
|
|
534
542
|
}
|
|
535
543
|
|
|
536
544
|
function isRescanCandidate(job) {
|
|
537
|
-
if (!job || isCancelledJob(job)) return false;
|
|
545
|
+
if (!job || isCancelledJob(job) || isRefireCapped(job)) return false;
|
|
538
546
|
// Default-ELIGIBLE: every needs_review row is a heal candidate unless its
|
|
539
547
|
// verdict is in RESCAN_EXCLUDED_VERDICTS. No runId requirement here — a row
|
|
540
548
|
// without one still gets the evidence rung and the unresolvable annotation.
|
|
@@ -645,7 +653,7 @@ const STUCK_FAILED_ESCALATE_MS = process.env.SM_STUCK_FAILED_ESCALATE_HOURS
|
|
|
645
653
|
/**
|
|
646
654
|
* Kill-switch gate for the stuck-failed escalation below
|
|
647
655
|
* (SM_STUCK_FAILED_ESCALATE_DISABLE=1), mirroring the
|
|
648
|
-
*
|
|
656
|
+
* SM_RCA_DISABLE convention. A tiny wrapper
|
|
649
657
|
* so the disable path is unit-testable without invoking the 10-minute
|
|
650
658
|
* setInterval body directly.
|
|
651
659
|
*/
|
|
@@ -862,6 +870,7 @@ function selectAutoFixTargets(jobs, { fixSlugExists, resolveJobRunId = resolveRu
|
|
|
862
870
|
const slugsInQueue = new Set(jobs.map((j) => j.slug));
|
|
863
871
|
return jobs.filter((job) => {
|
|
864
872
|
if (isCancelledJob(job)) return false;
|
|
873
|
+
if (isRefireCapped(job)) return false;
|
|
865
874
|
if (job.status !== 'needs_review') return false;
|
|
866
875
|
if (isNeedsHuman(job)) return false;
|
|
867
876
|
// A job parked here because it was blocked by a sibling's foreign WIP
|
|
@@ -1005,6 +1014,7 @@ function selectGateShadowTarget(jobs, headByCwd, { authorityDisabled = false, ba
|
|
|
1005
1014
|
|
|
1006
1015
|
module.exports = {
|
|
1007
1016
|
isNeedsHuman,
|
|
1017
|
+
isRefireCapped,
|
|
1008
1018
|
isCancelledJob,
|
|
1009
1019
|
selectResumeRecoveryTarget,
|
|
1010
1020
|
MECHANICALLY_RESOLVABLE_VERDICTS,
|