claude-code-session-manager 0.120.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.
Files changed (79) hide show
  1. package/dist/assets/{AgentLibrary-LBg3mVL8.js → AgentLibrary-BmJ8hIPw.js} +1 -1
  2. package/dist/assets/{Button-C4BN5ISR.js → Button-BS4kmz4p.js} +1 -1
  3. package/dist/assets/{DataModel-CHhjk61s.js → DataModel-DZHBZrfL.js} +1 -1
  4. package/dist/assets/History-pg00b2Gd.js +2 -0
  5. package/dist/assets/{Hooks-ByWVgXIf.js → Hooks-BTEs0UZY.js} +1 -1
  6. package/dist/assets/{Library-QvYVAEmY.js → Library-DuaJ-VSn.js} +1 -1
  7. package/dist/assets/{ListDetail-FvNe6fo5.js → ListDetail-Cud72s-O.js} +1 -1
  8. package/dist/assets/{McpServers-C7EmPm8B.js → McpServers-Dy-wnG_X.js} +1 -1
  9. package/dist/assets/{Memory-nyLR4Yw4.js → Memory-Bs1T1Jo7.js} +1 -1
  10. package/dist/assets/{Panel-thElCbLX.js → Panel-8UhLXEOW.js} +1 -1
  11. package/dist/assets/{Permissions-FQ9GM6Ca.js → Permissions-BfwzvmBS.js} +1 -1
  12. package/dist/assets/Plugins-McjyvvKh.js +2 -0
  13. package/dist/assets/{ProvenanceBadge-lAViG8yV.js → ProvenanceBadge-D0Ov_6WJ.js} +1 -1
  14. package/dist/assets/{RemoteMachines-AWpARk7N.js → RemoteMachines-gOlFTvWq.js} +1 -1
  15. package/dist/assets/{SaveBar-c15EEGB_.js → SaveBar-Cd6hJR6K.js} +1 -1
  16. package/dist/assets/Scheduler-BZXKqYJP.js +16 -0
  17. package/dist/assets/{ScopeSwitcher-BLep1rtB.js → ScopeSwitcher-C24ysV1K.js} +1 -1
  18. package/dist/assets/{Settings-D1kTUIDB.js → Settings-DWnN2qxn.js} +1 -1
  19. package/dist/assets/{SkillReferenceGraph-_40_f0t3.js → SkillReferenceGraph-BsWuiTbw.js} +1 -1
  20. package/dist/assets/{Skills-DaTkSOuU.js → Skills-6pHn6glu.js} +1 -1
  21. package/dist/assets/{SystemPrompt-DxNf-soM.js → SystemPrompt-DT0LBH-K.js} +1 -1
  22. package/dist/assets/{TagLibrary-L6KpBV-x.js → TagLibrary-DbJ0zbYL.js} +1 -1
  23. package/dist/assets/{TiptapBody-C_XjtJ82.js → TiptapBody-C4kIvIMH.js} +1 -1
  24. package/dist/assets/{Toggle-H0rERKNI.js → Toggle-pP19zlyk.js} +1 -1
  25. package/dist/assets/{ViewTabs-ZUWphDat.js → ViewTabs-CpzN8e5I.js} +1 -1
  26. package/dist/assets/{historyTotals-BxFKOztb.js → historyTotals-Caq78bb5.js} +1 -1
  27. package/dist/assets/{index-B5wj0DEX.js → index-Bjtguv6I.js} +434 -434
  28. package/dist/assets/{settingsSchema-BRcYkXJN.js → settingsSchema-CC2ySu3d.js} +1 -1
  29. package/dist/assets/{skillFrontmatter-DFGp4DST.js → skillFrontmatter-hDhNo8jE.js} +1 -1
  30. package/dist/assets/{useAgentsDirWatch-VvrgalfC.js → useAgentsDirWatch-BuOtY25U.js} +1 -1
  31. package/dist/assets/{useRemotePeers-mvXKNEE3.js → useRemotePeers-CNQmRBpt.js} +1 -1
  32. package/dist/index.html +1 -1
  33. package/package.json +1 -1
  34. package/plugins/session-manager-dev/skills/develop/SKILL.md +7 -7
  35. package/plugins/session-manager-dev/skills/develop/standards.md +4 -10
  36. package/scripts/scheduler-mcp-server.cjs +2 -2
  37. package/src/main/build-info.json +4 -4
  38. package/src/main/chatRunner.cjs +56 -43
  39. package/src/main/docEdit.cjs +1 -0
  40. package/src/main/index.cjs +12 -13
  41. package/src/main/ipcSchemas.cjs +17 -3
  42. package/src/main/lib/classifyPromptTicket.cjs +1 -0
  43. package/src/main/lib/claude/effectiveModelInfo.cjs +9 -21
  44. package/src/main/lib/claude/personaToolScope.cjs +7 -1
  45. package/src/main/lib/claude/runClaudeP.cjs +1 -0
  46. package/src/main/lib/epic/delegationReadiness.cjs +75 -8
  47. package/src/main/lib/mcpToolCatalog.cjs +0 -1
  48. package/src/main/lib/persona-macro/macroLibrary.cjs +75 -0
  49. package/src/main/lib/persona-macro/shippedPersonaSeeds.cjs +11 -0
  50. package/src/main/lib/prd/prdCreate.cjs +28 -1
  51. package/src/main/lib/prd/prdSizing.cjs +1 -8
  52. package/src/main/lib/remoteTranscriptTail.cjs +69 -2
  53. package/src/main/lib/runSeeders.cjs +21 -0
  54. package/src/main/lib/scheduler/loopEfficiency.cjs +109 -0
  55. package/src/main/lib/scheduler/queueHistory.cjs +44 -1
  56. package/src/main/lib/scheduler/queueStore.cjs +4 -72
  57. package/src/main/lib/scheduler/rcaReport.cjs +1 -1
  58. package/src/main/lib/scheduler/refireCap.cjs +40 -0
  59. package/src/main/lib/scheduler/reviewNotice.cjs +1 -1
  60. package/src/main/lib/scheduler/schedulerConfig.cjs +1 -2
  61. package/src/main/lib/scheduler/schedulerPaths.cjs +0 -2
  62. package/src/main/lib/scheduler/sessionSlots.cjs +1 -1
  63. package/src/main/lib/telemetry/auditLog.cjs +65 -14
  64. package/src/main/loopEfficiencyHandlers.cjs +160 -0
  65. package/src/main/queueOps.cjs +4 -9
  66. package/src/main/runVerify.cjs +22 -454
  67. package/src/main/scheduler/reviewLadder.cjs +12 -2
  68. package/src/main/scheduler.cjs +103 -177
  69. package/src/main/seedDevPlugin.cjs +49 -22
  70. package/src/main/seedStatus.cjs +3 -2
  71. package/src/main/supervisor.cjs +1 -0
  72. package/src/main/templates/PRD_AUTHORING.md +16 -31
  73. package/src/main/webRemote.cjs +82 -91
  74. package/src/preload/api.d.ts +39 -6
  75. package/src/preload/index.cjs +3 -0
  76. package/src/seed/agents/dev-lead.md +2 -9
  77. package/dist/assets/History-CL-T6Lv3.js +0 -2
  78. package/dist/assets/Plugins-D9KE7XTI.js +0 -2
  79. package/dist/assets/Scheduler-FvduP1FG.js +0 -16
@@ -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". Pattern-matches tool_result content from the run log and checks
6
- * dependency prerequisites to catch false-positives (clean-exit ≠ done).
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'|'transcript_errors'|'verify_unavailable'
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 → 'needs_review' (requires human flip)
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
- // ─── content pattern detectors ───────────────────────────────────────────────
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
- // ─── self-recovery helpers ────────────────────────────────────────────────────
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
- * Two layers:
431
- * 1. queue.json's `dependsOn` array (explicit slug list, often empty).
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, floorDate?: string }}
219
+ * @returns {{ ok: boolean, reason?: string }}
435
220
  */
436
- function checkDeps(queueEntry, allJobs, prdBody) {
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
- * @param {boolean} [params.allowPreSentinelHeal] When true, a commit-in-window
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, allowPreSentinelHeal = 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, prdBody);
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. Tool-result error scan ─────────────────────────────────────────
1025
- const total = events.length;
1026
- const last20pctStart = Math.floor(total * 0.8);
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
- // Pick highest-priority issue (transcript_errors > verify_unavailable ==
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
- * SM_REVERIFY_PERIODIC_DISABLE / SM_RCA_DISABLE convention. A tiny wrapper
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,