claude-code-session-manager 0.75.3 → 0.77.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 (164) hide show
  1. package/dist/assets/{AgentLibrary-CzQqcObq.js → AgentLibrary-B2ie8bbw.js} +2 -2
  2. package/dist/assets/{DataModel-Bj_WlLz8.js → DataModel-BIJPYw32.js} +1 -1
  3. package/dist/assets/{History-DnSi_OHm.js → History-CeY6dk9S.js} +2 -2
  4. package/dist/assets/{Hooks-0BB0dp3S.js → Hooks-BFH2ocKg.js} +2 -2
  5. package/dist/assets/{HostBilko-DHpwwsLQ.js → HostBilko-36gj9wLz.js} +1 -1
  6. package/dist/assets/{Library-CaJVqVvi.js → Library-C-hBct39.js} +1 -1
  7. package/dist/assets/{ListDetail-C1W2HmC2.js → ListDetail-CNq64VWV.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-5Ob9FW3z.js → MarkdownEditor-Bh3qt5-1.js} +1 -1
  9. package/dist/assets/{McpServers-JxCSfm1S.js → McpServers-DpGN0oyz.js} +1 -1
  10. package/dist/assets/{Memory-BDeqlqwH.js → Memory-D59hUjC4.js} +6 -6
  11. package/dist/assets/{Panel-Dh9ZHuEj.js → Panel-DCgbaoci.js} +1 -1
  12. package/dist/assets/{Permissions-DXy-CbEY.js → Permissions-DAmQ0DYV.js} +2 -2
  13. package/dist/assets/{Plugins-_n1Iuc8T.js → Plugins-Dyfgn6Is.js} +2 -2
  14. package/dist/assets/{ProvenanceBadge-BP_evfxE.js → ProvenanceBadge-BiYhPO1U.js} +1 -1
  15. package/dist/assets/SaveBar-RV7B6sOh.js +1 -0
  16. package/dist/assets/Scheduler-BPaNqx1b.js +14 -0
  17. package/dist/assets/{ScopeSwitcher-CAWzM6RI.js → ScopeSwitcher-P4mdLGNU.js} +1 -1
  18. package/dist/assets/{Settings-DRRozLyT.js → Settings-BL4vf5aX.js} +1 -1
  19. package/dist/assets/{SkillReferenceGraph-DGHDWlz4.js → SkillReferenceGraph-BRBDyi1_.js} +1 -1
  20. package/dist/assets/{Skills-D8L66eiX.js → Skills-BV08gDUH.js} +2 -2
  21. package/dist/assets/{SystemPrompt-CYtUsonD.js → SystemPrompt-CLftSsDw.js} +1 -1
  22. package/dist/assets/TagLibrary-Bp8jGsd5.js +1 -0
  23. package/dist/assets/{TiptapBody-B2hRgbPE.js → TiptapBody-jCpuB6E5.js} +1 -1
  24. package/dist/assets/{Toggle-BTwsbxam.js → Toggle-D2paA1xf.js} +1 -1
  25. package/dist/assets/{index-DijufvkJ.js → index-BDRSqBl3.js} +704 -704
  26. package/dist/assets/{index-CMLnzdZC.css → index-CYhdtisq.css} +1 -1
  27. package/dist/assets/{settingsSchema-D6wzxAi6.js → settingsSchema-6IOLjZZN.js} +1 -1
  28. package/dist/index.html +2 -2
  29. package/package.json +8 -2
  30. package/plugins/session-manager-dev/skills/develop/standards.md +1 -1
  31. package/scripts/lib/activeSessions.cjs +116 -6
  32. package/scripts/project-pages-logic/dist/logic.cjs +4709 -0
  33. package/scripts/render-project-pages/dist/renderer.cjs +18900 -0
  34. package/scripts/render-project-pages.cjs +70 -0
  35. package/scripts/scheduler-mcp-server.cjs +269 -96
  36. package/scripts/validate-project-pages-summary.cjs +62 -0
  37. package/src/main/__tests__/agentModelResolve.test.cjs +66 -0
  38. package/src/main/__tests__/epicStatusMirror.test.cjs +110 -0
  39. package/src/main/__tests__/health-delegation-chain.test.cjs +106 -0
  40. package/src/main/__tests__/prdAdminRoutes.test.cjs +295 -0
  41. package/src/main/__tests__/prdAgentType.test.cjs +103 -0
  42. package/src/main/__tests__/prdCreate.test.cjs +247 -0
  43. package/src/main/__tests__/prdFrontmatterAgentType.test.cjs +117 -0
  44. package/src/main/__tests__/prdFrontmatterQuietMachine.test.cjs +108 -0
  45. package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +485 -0
  46. package/src/main/__tests__/projectPages.test.cjs +73 -1
  47. package/src/main/__tests__/rcaReport.test.cjs +54 -0
  48. package/src/main/__tests__/runVerify.test.cjs +94 -0
  49. package/src/main/__tests__/scheduler-autofix-select.test.cjs +58 -3
  50. package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +103 -0
  51. package/src/main/__tests__/scheduler-commit-guard-noop.test.cjs +41 -0
  52. package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +10 -0
  53. package/src/main/__tests__/scheduler-foreign-wip-manifest.test.cjs +78 -0
  54. package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +242 -0
  55. package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +31 -0
  56. package/src/main/__tests__/scheduler-launch-failure.test.cjs +201 -0
  57. package/src/main/__tests__/scheduler-leftover-fields.test.cjs +52 -0
  58. package/src/main/__tests__/scheduler-looks-done.test.cjs +241 -0
  59. package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +135 -0
  60. package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +222 -0
  61. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +207 -1
  62. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +212 -0
  63. package/src/main/__tests__/scheduler-stranded-investigation.test.cjs +185 -0
  64. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +194 -0
  65. package/src/main/__tests__/seedAgentPersonas.test.cjs +75 -14
  66. package/src/main/__tests__/seedSchedulerMcp.test.cjs +66 -0
  67. package/src/main/__tests__/uniquePrdNumbers.test.cjs +14 -5
  68. package/src/main/bilkoHost.cjs +4 -3
  69. package/src/main/chatRunner.cjs +6 -1
  70. package/src/main/config.cjs +25 -33
  71. package/src/main/health.cjs +153 -2
  72. package/src/main/index.cjs +64 -5
  73. package/src/main/ipcSchemas.cjs +69 -1
  74. package/src/main/lib/__tests__/activeIndexRebuild.test.cjs +179 -0
  75. package/src/main/lib/__tests__/childWithLog.test.cjs +141 -0
  76. package/src/main/lib/__tests__/delegationReadiness.test.cjs +391 -42
  77. package/src/main/lib/__tests__/ephemeralCwd.test.cjs +91 -0
  78. package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +5 -3
  79. package/src/main/lib/__tests__/fixChainDepth.test.cjs +40 -0
  80. package/src/main/lib/__tests__/gitWorktree.test.cjs +290 -5
  81. package/src/main/lib/__tests__/gitWorktreeSalvage.test.cjs +107 -0
  82. package/src/main/lib/__tests__/gitWorktreeSalvageDelta.test.cjs +153 -0
  83. package/src/main/lib/__tests__/jobWorktree.test.cjs +6 -4
  84. package/src/main/lib/__tests__/landedSinceRun.test.cjs +73 -0
  85. package/src/main/lib/__tests__/launchFailure.test.cjs +220 -0
  86. package/src/main/lib/__tests__/loadGate.test.cjs +159 -0
  87. package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +102 -0
  88. package/src/main/lib/__tests__/opsOwnership.test.cjs +7 -0
  89. package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +151 -0
  90. package/src/main/lib/__tests__/opsRootResolve.test.cjs +149 -0
  91. package/src/main/lib/__tests__/prdDeclaredPaths.test.cjs +82 -0
  92. package/src/main/lib/__tests__/projectRootResolve.test.cjs +148 -0
  93. package/src/main/lib/__tests__/queueHealth.test.cjs +58 -0
  94. package/src/main/lib/__tests__/quietMachineLease.test.cjs +39 -0
  95. package/src/main/lib/__tests__/reaperHelpers.test.cjs +133 -0
  96. package/src/main/lib/__tests__/schedulerBatchDepends.test.cjs +19 -9
  97. package/src/main/lib/__tests__/schedulerBatchFairness.test.cjs +213 -0
  98. package/src/main/lib/__tests__/schedulerBatchLaunchHold.test.cjs +125 -0
  99. package/src/main/lib/__tests__/schedulerBatchProjectCap.test.cjs +127 -0
  100. package/src/main/lib/__tests__/schedulerBatchQuietMachine.test.cjs +109 -0
  101. package/src/main/lib/__tests__/schedulerMcpServerHeadlessRefusal.test.cjs +71 -0
  102. package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +217 -0
  103. package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +350 -0
  104. package/src/main/lib/activeIndexMerge.cjs +15 -0
  105. package/src/main/lib/activeIndexRebuild.cjs +133 -0
  106. package/src/main/lib/agentModelResolve.cjs +58 -0
  107. package/src/main/lib/buildTarget.cjs +3 -2
  108. package/src/main/lib/childWithLog.cjs +69 -2
  109. package/src/main/lib/claudeBin.cjs +54 -1
  110. package/src/main/lib/crossProjectFeedback.cjs +8 -1
  111. package/src/main/lib/definitionOfDone.cjs +3 -2
  112. package/src/main/lib/delegationReadiness.cjs +514 -26
  113. package/src/main/lib/ephemeralCwd.cjs +78 -0
  114. package/src/main/lib/epicDelegationStats.cjs +2 -1
  115. package/src/main/lib/epicMint.cjs +17 -1
  116. package/src/main/lib/epicStatusMirror.cjs +95 -0
  117. package/src/main/lib/epicValidationHook.cjs +2 -1
  118. package/src/main/lib/epicWorktreeMint.cjs +5 -2
  119. package/src/main/lib/fixChainDepth.cjs +45 -0
  120. package/src/main/lib/gitWorktree.cjs +520 -21
  121. package/src/main/lib/jobWorktree.cjs +2 -0
  122. package/src/main/lib/landedSinceRun.cjs +55 -0
  123. package/src/main/lib/launchFailure.cjs +357 -0
  124. package/src/main/lib/loadGate.cjs +134 -0
  125. package/src/main/lib/mcpToolCatalog.cjs +370 -0
  126. package/src/main/lib/opsErrorLog.cjs +12 -1
  127. package/src/main/lib/opsOwnership.cjs +106 -0
  128. package/src/main/lib/prdAdminRoutes.cjs +43 -3
  129. package/src/main/lib/prdAgentType.cjs +84 -0
  130. package/src/main/lib/prdCreate.cjs +103 -15
  131. package/src/main/lib/prdDeclaredPaths.cjs +70 -0
  132. package/src/main/lib/prdFrontmatter.cjs +17 -3
  133. package/src/main/lib/prdLocations.cjs +13 -6
  134. package/src/main/lib/projectHomeAdminRoutes.cjs +402 -0
  135. package/src/main/lib/projectPageSummarySchema.cjs +181 -0
  136. package/src/main/lib/projectRootResolve.cjs +134 -0
  137. package/src/main/lib/promptSessionSchema.cjs +7 -0
  138. package/src/main/lib/queueHealth.cjs +38 -0
  139. package/src/main/lib/queueStore.cjs +40 -7
  140. package/src/main/lib/quietMachineLease.cjs +48 -0
  141. package/src/main/lib/rcaReport.cjs +54 -4
  142. package/src/main/lib/reaperHelpers.cjs +64 -1
  143. package/src/main/lib/scheduleJobSchema.cjs +31 -0
  144. package/src/main/lib/scheduleJobTransitions.cjs +6 -2
  145. package/src/main/lib/schedulerBatch.cjs +301 -55
  146. package/src/main/lib/schedulerConfig.cjs +99 -0
  147. package/src/main/projectBrief.cjs +3 -2
  148. package/src/main/projectPages.cjs +162 -3
  149. package/src/main/promptSessionTranscript.cjs +0 -0
  150. package/src/main/pty.cjs +5 -0
  151. package/src/main/queueOps.cjs +15 -8
  152. package/src/main/runVerify.cjs +50 -9
  153. package/src/main/scheduler/prdParser.cjs +18 -1
  154. package/src/main/scheduler.cjs +1701 -130
  155. package/src/main/seedAgentPersonas.cjs +62 -21
  156. package/src/main/seedSchedulerMcp.cjs +58 -4
  157. package/src/main/templates/project-pages-catalog.json +741 -0
  158. package/src/main/templates/project-pages-pipeline.md +417 -0
  159. package/src/preload/api.d.ts +187 -3
  160. package/src/preload/index.cjs +9 -0
  161. package/src/seed/agents/project-home-builder.md +59 -0
  162. package/dist/assets/SaveBar-D-gCUx4n.js +0 -1
  163. package/dist/assets/Scheduler-Bpd4OGju.js +0 -14
  164. package/dist/assets/TagLibrary-E5CLeuVk.js +0 -1
@@ -40,6 +40,10 @@
40
40
  * killedByWatchdog — string | null
41
41
  * error — Error | undefined (child 'error' event or sync spawn throw)
42
42
  * spawnFailed — boolean, true when the error came from a synchronous spawn throw
43
+ * leakedDescendants — { pid, pcpu, etimes, comm }[], the process-group survivors
44
+ * sweepChildProcessGroup found still alive right before it
45
+ * swept them. [] in the normal case (nothing leaked, or
46
+ * non-Linux, or enumeration failed). See sweepChildProcessGroup.
43
47
  * safeLog — same safeLog (usable inside onExit for final log lines)
44
48
  *
45
49
  * Returns { child, cancel } where cancel() clears all timers + SIGKILLs.
@@ -47,7 +51,11 @@
47
51
  */
48
52
 
49
53
  const fs = require('node:fs');
50
- const { spawn } = require('node:child_process');
54
+ const { spawn, execFileSync } = require('node:child_process');
55
+
56
+ // Grace period between the post-exit SIGTERM sweep and the follow-up SIGKILL
57
+ // sweep of a detached child's process group. See sweepChildProcessGroup below.
58
+ const POST_EXIT_GROUP_SWEEP_GRACE_MS = 5000;
51
59
 
52
60
  // ---------- Phase 1 ----------
53
61
 
@@ -119,6 +127,63 @@ function openLog(logPath) {
119
127
  * }} opts
120
128
  * @returns {{ child: import('child_process').ChildProcess | null, cancel: () => void }}
121
129
  */
130
+ // Post-exit process-group sweep.
131
+ //
132
+ // Incident (2026-08-31, ~/Projects/starry-night-ships): scheduler jobs spawn
133
+ // detached: true, and every watchdog path (result-tail, deadman, idle-tail)
134
+ // kills the whole process group via ctx.killTree — but only on an *overrun*
135
+ // exit. When the executor child exits normally, nothing swept the group, so
136
+ // anything the agent had backgrounded to work around the foreground call cap
137
+ // (the `bash -c '<gate> > /tmp/x.log; echo $? > /tmp/x.done'` shape) reparented
138
+ // to init and kept running for nobody: three orphaned test batteries at PPID 1
139
+ // were found, one burning ~5 cores for 56 minutes. The invariant this restores:
140
+ // a job's descendants must not outlive the job. Killing the job's own process
141
+ // group at the job's own exit is ownership-based and needs no /proc heuristics
142
+ // to distinguish an abandoned descendant from a live one.
143
+ // Enumerate the still-alive members of process group `pgid` (Linux only).
144
+ // Read-only — never throws, never blocks the caller's exit path. Used to
145
+ // report what sweepChildProcessGroup is about to kill, since the kill itself
146
+ // is silent (ESRCH on the normal empty-group case).
147
+ function enumerateProcessGroupSurvivors(pgid) {
148
+ if (process.platform !== 'linux') return [];
149
+ if (!pgid || pgid <= 1) return [];
150
+ try {
151
+ const out = execFileSync('ps', ['-eo', 'pid,pgrp,pcpu,etimes,comm'], { encoding: 'utf8', timeout: 2000 });
152
+ const survivors = [];
153
+ for (const line of out.split('\n').slice(1)) {
154
+ const m = line.trim().match(/^(\d+)\s+(\d+)\s+([\d.]+)\s+(\d+)\s+(.+)$/);
155
+ if (!m) continue;
156
+ const [, pidStr, pgrpStr, pcpuStr, etimesStr, comm] = m;
157
+ if (Number(pgrpStr) !== pgid) continue;
158
+ survivors.push({ pid: Number(pidStr), pcpu: Number(pcpuStr), etimes: Number(etimesStr), comm });
159
+ }
160
+ return survivors;
161
+ } catch {
162
+ return [];
163
+ }
164
+ }
165
+
166
+ // Returns the leaked descendants found (see enumerateProcessGroupSurvivors),
167
+ // having enumerated them BEFORE issuing SIGTERM so the report reflects what
168
+ // was actually swept, not what happened to still be running afterward.
169
+ function sweepChildProcessGroup(child, detached) {
170
+ if (!detached) return [];
171
+ if (!child || !child.pid || child.pid <= 1) return [];
172
+ // Defensive: never target the Session Manager process's own group.
173
+ if (child.pid === process.pid) return [];
174
+
175
+ const leaked = enumerateProcessGroupSurvivors(child.pid).filter((p) => p.pid !== child.pid);
176
+
177
+ try { process.kill(-child.pid, 'SIGTERM'); } catch { /* ESRCH: empty group, the normal case */ }
178
+
179
+ const t = setTimeout(() => {
180
+ try { process.kill(-child.pid, 'SIGKILL'); } catch { /* ESRCH: empty group, the normal case */ }
181
+ }, POST_EXIT_GROUP_SWEEP_GRACE_MS);
182
+ if (t.unref) t.unref();
183
+
184
+ return leaked;
185
+ }
186
+
122
187
  function withChildAndLog({ fd, logPath, safeLog, closeFd, spawn: spawnSpec, watchdogs = [], onExit }) {
123
188
  const startedAt = Date.now();
124
189
 
@@ -173,6 +238,7 @@ function withChildAndLog({ fd, logPath, safeLog, closeFd, spawn: spawnSpec, watc
173
238
  // calls onExit (caller may still safeLog inside), then closes the fd.
174
239
  const handleDone = (exitCode, signal, error, spawnFailed) => {
175
240
  clearAllTimers();
241
+ const leakedDescendants = sweepChildProcessGroup(ctx.child, !!spawnSpec.options?.detached);
176
242
  if (onExit) {
177
243
  onExit({
178
244
  exitCode,
@@ -180,6 +246,7 @@ function withChildAndLog({ fd, logPath, safeLog, closeFd, spawn: spawnSpec, watc
180
246
  killedByWatchdog: ctx.killedByWatchdog,
181
247
  error,
182
248
  spawnFailed: spawnFailed ?? false,
249
+ leakedDescendants,
183
250
  safeLog,
184
251
  });
185
252
  }
@@ -215,4 +282,4 @@ function withChildAndLog({ fd, logPath, safeLog, closeFd, spawn: spawnSpec, watc
215
282
  return { child, cancel };
216
283
  }
217
284
 
218
- module.exports = { openLog, withChildAndLog };
285
+ module.exports = { openLog, withChildAndLog, POST_EXIT_GROUP_SWEEP_GRACE_MS };
@@ -39,4 +39,57 @@ function resolveClaudeBin() {
39
39
  return cached;
40
40
  }
41
41
 
42
- module.exports = { resolveClaudeBin };
42
+ // ---------- version probe ----------
43
+ //
44
+ // `claude --version`, cached. The scheduler's launch circuit breaker
45
+ // (lib/launchFailure.cjs) records the CLI version a launch block was armed
46
+ // under and drops the block the moment the version changes — the incident
47
+ // behind it (issue #11: an outdated CLI sending a thinking parameter the
48
+ // model rejects) is fixed by `claude update`, and the queue must notice
49
+ // that without a human pressing anything. Async, bounded, never throws:
50
+ // a probe failure yields null and the breaker simply loses the
51
+ // version-change shortcut (backoff still recovers).
52
+ const VERSION_TTL_MS = 5 * 60_000;
53
+ const VERSION_PROBE_TIMEOUT_MS = 8_000;
54
+ let versionCache = { at: 0, value: null, inflight: null };
55
+
56
+ function probeClaudeVersion({ now = Date.now(), execFileImpl } = {}) {
57
+ // Operator/test pin: skips executing the binary entirely.
58
+ if (process.env.SM_CLAUDE_VERSION) return Promise.resolve(process.env.SM_CLAUDE_VERSION);
59
+ if (versionCache.inflight) return versionCache.inflight;
60
+ if (versionCache.value !== null && now - versionCache.at < VERSION_TTL_MS) {
61
+ return Promise.resolve(versionCache.value);
62
+ }
63
+ const execFile = execFileImpl || require('node:child_process').execFile;
64
+ const bin = resolveClaudeBin();
65
+ versionCache.inflight = new Promise((resolve) => {
66
+ let settled = false;
67
+ const done = (value) => {
68
+ if (settled) return;
69
+ settled = true;
70
+ versionCache = { at: Date.now(), value, inflight: null };
71
+ resolve(value);
72
+ };
73
+ try {
74
+ // cwd is a neutral temp dir on purpose: the version does not depend on
75
+ // it, and the binary must never be handed the caller's working tree —
76
+ // a stub `claude` (tests point SM_CLAUDE_BIN at one) that writes
77
+ // markers or commits into process.cwd() once did so in the real repo.
78
+ execFile(bin, ['--version'], { timeout: VERSION_PROBE_TIMEOUT_MS, windowsHide: true, cwd: os.tmpdir() }, (err, stdout) => {
79
+ if (err) return done(null);
80
+ const m = /(\d+\.\d+\.\d+[^\s]*)/.exec(String(stdout || ''));
81
+ done(m ? m[1] : (String(stdout || '').trim() || null));
82
+ });
83
+ } catch {
84
+ done(null);
85
+ }
86
+ });
87
+ return versionCache.inflight;
88
+ }
89
+
90
+ /** Test hook — forget the cached version so the next probe re-executes. */
91
+ function resetClaudeVersionCache() {
92
+ versionCache = { at: 0, value: null, inflight: null };
93
+ }
94
+
95
+ module.exports = { resolveClaudeBin, probeClaudeVersion, resetClaudeVersionCache };
@@ -66,6 +66,7 @@ const {
66
66
  } = require('./epicMint.cjs');
67
67
  const { appendAuditEvent } = require('./auditLog.cjs');
68
68
  const { OPS_ROOT_DIR } = require('./opsOwnership.cjs');
69
+ const { resolveProjectContext } = require('./projectRootResolve.cjs');
69
70
 
70
71
  /** Only these three Epic tags make sense for an inbound report — they are
71
72
  * exactly the tags that route through /develop (tagLibrary.ts). 'build',
@@ -251,8 +252,14 @@ function validateFeedbackInput(input = {}) {
251
252
  } catch (e) {
252
253
  return bad(400, `toCwd rejected: ${e.message}`);
253
254
  }
255
+ // Normalize fromCwd (worktree -> main tree, ops-internal path -> project
256
+ // root) BEFORE validatePath/the same-project check below — an agent
257
+ // sending feedback from inside its own Epic's worktree pwd must be
258
+ // compared/chained against its REAL project, not the ephemeral checkout.
259
+ // See projectRootResolve.cjs's header.
260
+ const normalizedFromCwd = resolveProjectContext({ cwd: input.fromCwd }).cwd || input.fromCwd;
254
261
  try {
255
- fromCwd = config.validatePath(path.resolve(input.fromCwd));
262
+ fromCwd = config.validatePath(path.resolve(normalizedFromCwd));
256
263
  } catch (e) {
257
264
  return bad(400, `fromCwd rejected: ${e.message}`);
258
265
  }
@@ -328,7 +328,7 @@ const RISK_HEURISTICS = [
328
328
  * @param {string} headingText Case-insensitive heading to find.
329
329
  * @returns {string}
330
330
  */
331
- function _extractSection(body, headingText) {
331
+ function extractSection(body, headingText) {
332
332
  const escapedHeading = headingText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
333
333
  const headingRe = new RegExp(`^#+\\s+${escapedHeading}`, 'i');
334
334
  const lines = body.split('\n');
@@ -398,7 +398,7 @@ function flagRiskySurfaces(jobs, { prdsDir, gitTimeoutMs = 10_000 } = {}) {
398
398
  try {
399
399
  const raw = fs.readFileSync(path.join(resolvedPrdsDir, `${job.slug}.md`), 'utf8');
400
400
  const body = splitFrontmatter(raw).body;
401
- candidateText = _extractSection(body, 'Implementation notes');
401
+ candidateText = extractSection(body, 'Implementation notes');
402
402
  } catch { /* no candidate text — skip */ }
403
403
  }
404
404
 
@@ -573,6 +573,7 @@ module.exports = {
573
573
  reportPathFor,
574
574
  reportExists,
575
575
  extractAcCommand,
576
+ extractSection,
576
577
  reverifyAc,
577
578
  reverifyBatch,
578
579
  flagRiskySurfaces,