session-orchestrator 5.1.0 → 5.3.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 (484) hide show
  1. package/.agents/skills/architecture/SKILL.md +3 -1
  2. package/.agents/skills/autopilot/SKILL.md +6 -1
  3. package/.agents/skills/autopilot/agents/openai.yaml +5 -0
  4. package/.agents/skills/bootstrap/SKILL.md +7 -1
  5. package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
  6. package/.agents/skills/brainstorm/SKILL.md +8 -1
  7. package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
  8. package/.agents/skills/claude-md-drift-check/SKILL.md +3 -1
  9. package/.agents/skills/close/SKILL.md +21 -0
  10. package/.agents/skills/close/agents/openai.yaml +5 -0
  11. package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
  12. package/.agents/skills/debug/SKILL.md +7 -1
  13. package/.agents/skills/debug/agents/openai.yaml +5 -0
  14. package/.agents/skills/discovery/SKILL.md +7 -2
  15. package/.agents/skills/discovery/agents/openai.yaml +5 -0
  16. package/.agents/skills/dispatcher/SKILL.md +7 -1
  17. package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
  18. package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
  19. package/.agents/skills/ecosystem-health/SKILL.md +3 -1
  20. package/.agents/skills/eli5/SKILL.md +7 -1
  21. package/.agents/skills/eli5/agents/openai.yaml +5 -0
  22. package/.agents/skills/eval/SKILL.md +7 -2
  23. package/.agents/skills/eval/agents/openai.yaml +5 -0
  24. package/.agents/skills/evolve/SKILL.md +8 -3
  25. package/.agents/skills/evolve/agents/openai.yaml +5 -0
  26. package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
  27. package/.agents/skills/gitlab-ops/SKILL.md +3 -1
  28. package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
  29. package/.agents/skills/go/SKILL.md +22 -0
  30. package/.agents/skills/go/agents/openai.yaml +5 -0
  31. package/.agents/skills/grill/SKILL.md +7 -1
  32. package/.agents/skills/grill/agents/openai.yaml +5 -0
  33. package/.agents/skills/harness-audit/SKILL.md +20 -0
  34. package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
  35. package/.agents/skills/hook-development/SKILL.md +3 -1
  36. package/.agents/skills/mcp-builder/SKILL.md +3 -1
  37. package/.agents/skills/memory-cleanup/SKILL.md +6 -1
  38. package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
  39. package/.agents/skills/mode-selector/SKILL.md +3 -1
  40. package/.agents/skills/npm-publish/SKILL.md +4 -2
  41. package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
  42. package/.agents/skills/persona-panel/SKILL.md +6 -1
  43. package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
  44. package/.agents/skills/plan/SKILL.md +8 -2
  45. package/.agents/skills/plan/agents/openai.yaml +5 -0
  46. package/.agents/skills/playwright-driver/SKILL.md +3 -1
  47. package/.agents/skills/portfolio/SKILL.md +21 -0
  48. package/.agents/skills/portfolio/agents/openai.yaml +5 -0
  49. package/.agents/skills/quality-gates/SKILL.md +3 -1
  50. package/.agents/skills/reconcile/SKILL.md +6 -1
  51. package/.agents/skills/reconcile/agents/openai.yaml +5 -0
  52. package/.agents/skills/release/SKILL.md +22 -0
  53. package/.agents/skills/release/agents/openai.yaml +5 -0
  54. package/.agents/skills/remote-offload/SKILL.md +3 -1
  55. package/.agents/skills/repo-audit/SKILL.md +6 -1
  56. package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
  57. package/.agents/skills/session/SKILL.md +21 -0
  58. package/.agents/skills/session/agents/openai.yaml +5 -0
  59. package/.agents/skills/session-end/SKILL.md +3 -1
  60. package/.agents/skills/session-plan/SKILL.md +3 -1
  61. package/.agents/skills/session-start/SKILL.md +3 -1
  62. package/.agents/skills/spinout/SKILL.md +6 -1
  63. package/.agents/skills/spinout/agents/openai.yaml +5 -0
  64. package/.agents/skills/sunset-review/SKILL.md +7 -1
  65. package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
  66. package/.agents/skills/templates-ack/SKILL.md +21 -0
  67. package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
  68. package/.agents/skills/test/SKILL.md +21 -0
  69. package/.agents/skills/test/agents/openai.yaml +5 -0
  70. package/.agents/skills/test-runner/SKILL.md +3 -1
  71. package/.agents/skills/tmux-layout/SKILL.md +3 -1
  72. package/.agents/skills/using-orchestrator/SKILL.md +3 -1
  73. package/.agents/skills/ux-grill/SKILL.md +7 -1
  74. package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
  75. package/.agents/skills/vault-mirror/SKILL.md +3 -1
  76. package/.agents/skills/vault-sync/SKILL.md +3 -1
  77. package/.agents/skills/wave-executor/SKILL.md +3 -1
  78. package/.agents/skills/write-executable-plan/SKILL.md +3 -1
  79. package/.claude-plugin/marketplace.json +1 -1
  80. package/.claude-plugin/plugin.json +1 -1
  81. package/.codex-plugin/plugin.json +4 -4
  82. package/.codex-plugin/skills/autopilot/SKILL.md +5 -4
  83. package/.codex-plugin/skills/bootstrap/SKILL.md +8 -4
  84. package/.codex-plugin/skills/brainstorm/SKILL.md +11 -4
  85. package/.codex-plugin/skills/close/SKILL.md +3 -3
  86. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -1
  87. package/.codex-plugin/skills/debug/SKILL.md +11 -4
  88. package/.codex-plugin/skills/discovery/SKILL.md +8 -4
  89. package/.codex-plugin/skills/dispatcher/SKILL.md +4 -4
  90. package/.codex-plugin/skills/eli5/SKILL.md +9 -4
  91. package/.codex-plugin/skills/eval/SKILL.md +9 -4
  92. package/.codex-plugin/skills/evolve/SKILL.md +9 -4
  93. package/.codex-plugin/skills/go/SKILL.md +3 -3
  94. package/.codex-plugin/skills/grill/SKILL.md +11 -4
  95. package/.codex-plugin/skills/harness-audit/SKILL.md +4 -3
  96. package/.codex-plugin/skills/memory-cleanup/SKILL.md +9 -4
  97. package/.codex-plugin/skills/npm-publish/SKILL.md +1 -1
  98. package/.codex-plugin/skills/persona-panel/SKILL.md +5 -5
  99. package/.codex-plugin/skills/plan/SKILL.md +8 -4
  100. package/.codex-plugin/skills/portfolio/SKILL.md +3 -3
  101. package/.codex-plugin/skills/reconcile/SKILL.md +9 -4
  102. package/.codex-plugin/skills/release/SKILL.md +3 -3
  103. package/.codex-plugin/skills/repo-audit/SKILL.md +6 -4
  104. package/.codex-plugin/skills/session/SKILL.md +1 -1
  105. package/.codex-plugin/skills/spinout/SKILL.md +4 -4
  106. package/.codex-plugin/skills/sunset-review/SKILL.md +5 -4
  107. package/.codex-plugin/skills/test/SKILL.md +3 -3
  108. package/.codex-plugin/skills/ux-grill/SKILL.md +11 -4
  109. package/.cursor/commands/autopilot.md +4 -4
  110. package/.cursor/commands/bootstrap.md +5 -4
  111. package/.cursor/commands/brainstorm.md +5 -4
  112. package/.cursor/commands/close.md +4 -3
  113. package/.cursor/commands/debug.md +4 -4
  114. package/.cursor/commands/discovery.md +4 -4
  115. package/.cursor/commands/dispatcher.md +4 -4
  116. package/.cursor/commands/eli5.md +4 -4
  117. package/.cursor/commands/eval.md +4 -4
  118. package/.cursor/commands/evolve.md +4 -4
  119. package/.cursor/commands/go.md +4 -3
  120. package/.cursor/commands/grill.md +4 -4
  121. package/.cursor/commands/harness-audit.md +3 -3
  122. package/.cursor/commands/memory-cleanup.md +4 -4
  123. package/.cursor/commands/persona-panel.md +4 -4
  124. package/.cursor/commands/plan.md +5 -4
  125. package/.cursor/commands/portfolio.md +3 -3
  126. package/.cursor/commands/reconcile.md +4 -4
  127. package/.cursor/commands/release.md +4 -3
  128. package/.cursor/commands/repo-audit.md +4 -4
  129. package/.cursor/commands/session.md +1 -1
  130. package/.cursor/commands/spinout.md +4 -4
  131. package/.cursor/commands/sunset-review.md +4 -4
  132. package/.cursor/commands/test.md +3 -3
  133. package/.cursor/commands/ux-grill.md +4 -4
  134. package/.cursor/rules/000-session-orchestrator.mdc +0 -2
  135. package/.cursor/rules/010-session-workflow.mdc +2 -2
  136. package/.cursor/rules/050-plan.mdc +1 -1
  137. package/.cursor/skills/bootstrap/SKILL.md +1 -0
  138. package/.cursor/skills/close/SKILL.md +13 -0
  139. package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
  140. package/.cursor/skills/debug/SKILL.md +0 -1
  141. package/.cursor/skills/discovery/SKILL.md +0 -1
  142. package/.cursor/skills/dispatcher/SKILL.md +0 -1
  143. package/.cursor/skills/eli5/SKILL.md +0 -1
  144. package/.cursor/skills/eval/SKILL.md +1 -1
  145. package/.cursor/skills/evolve/SKILL.md +0 -1
  146. package/.cursor/skills/go/SKILL.md +13 -0
  147. package/.cursor/skills/grill/SKILL.md +0 -1
  148. package/.cursor/skills/harness-audit/SKILL.md +12 -0
  149. package/.cursor/skills/npm-publish/SKILL.md +1 -0
  150. package/.cursor/skills/portfolio/SKILL.md +12 -0
  151. package/.cursor/skills/release/SKILL.md +13 -0
  152. package/.cursor/skills/repo-audit/SKILL.md +0 -1
  153. package/.cursor/skills/sunset-review/SKILL.md +0 -1
  154. package/.cursor/skills/test/SKILL.md +12 -0
  155. package/.cursor/skills/ux-grill/SKILL.md +0 -1
  156. package/.cursor-plugin/plugin.json +1 -1
  157. package/.orchestrator/policy/blocked-commands.json +13 -4
  158. package/AGENTS.md +3 -2
  159. package/CHANGELOG.md +197 -0
  160. package/README.md +11 -9
  161. package/SECURITY.md +12 -0
  162. package/agents/dialectic-deriver.md +13 -10
  163. package/agents/eval-judge.md +67 -45
  164. package/agents/skill-applied-judge.md +34 -19
  165. package/commands/session.md +17 -3
  166. package/docs/baseline.md +12 -6
  167. package/docs/ci-setup.md +53 -0
  168. package/docs/codex-setup.md +15 -3
  169. package/docs/components.md +13 -6
  170. package/docs/events-schema.md +59 -9
  171. package/docs/install.md +16 -0
  172. package/docs/persona-panel.md +1 -1
  173. package/docs/pi-setup.md +1 -1
  174. package/docs/rule-authoring.md +135 -14
  175. package/docs/scope-collision-guard.md +2 -0
  176. package/docs/session-config-reference.md +106 -11
  177. package/docs/session-config-template.md +31 -2
  178. package/docs/telemetry.md +2 -0
  179. package/hooks/_lib/hook-import-set.json +125 -8
  180. package/hooks/_lib/subagent-paths.mjs +15 -0
  181. package/hooks/_lib/subagent-transcript.mjs +582 -31
  182. package/hooks/_lib/vcs-create-matcher.mjs +217 -62
  183. package/hooks/config-protection.mjs +11 -3
  184. package/hooks/cwd-change-restore.mjs +11 -3
  185. package/hooks/enforce-commands.mjs +70 -23
  186. package/hooks/enforce-scope.mjs +143 -33
  187. package/hooks/hooks-codex.json +1 -1
  188. package/hooks/hooks.json +1 -1
  189. package/hooks/loop-guard.mjs +11 -3
  190. package/hooks/on-session-end.mjs +72 -25
  191. package/hooks/on-session-start.mjs +48 -11
  192. package/hooks/on-stop.mjs +211 -23
  193. package/hooks/operator-steer.mjs +11 -3
  194. package/hooks/post-bash-issue-budget-refund.mjs +18 -8
  195. package/hooks/post-bash-write-verify.mjs +6 -2
  196. package/hooks/post-edit-import-probe.mjs +17 -9
  197. package/hooks/post-edit-validate.mjs +13 -5
  198. package/hooks/post-subagent-discovery-validator.mjs +98 -13
  199. package/hooks/post-tool-batch-wave-signal.mjs +200 -38
  200. package/hooks/post-tool-failure-corrective-context.mjs +11 -5
  201. package/hooks/post-tooluse-frontend-slop.mjs +10 -4
  202. package/hooks/pre-auq-clarity.mjs +18 -2
  203. package/hooks/pre-bash-destructive-guard.mjs +80 -9
  204. package/hooks/pre-bash-issue-budget.mjs +119 -28
  205. package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
  206. package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
  207. package/hooks/pre-bash-staging-fence.mjs +335 -31
  208. package/hooks/pre-bash-templates-first.mjs +19 -14
  209. package/hooks/pre-task-scope-disjoint.mjs +385 -5
  210. package/hooks/skill-invocation-telemetry.mjs +2 -1
  211. package/hooks/subagent-telemetry.mjs +15 -19
  212. package/hooks/wave-scope-commit-guard.mjs +197 -100
  213. package/monitors/monitors.json +1 -1
  214. package/output-styles/wave-summary.md +1 -1
  215. package/package.json +2 -1
  216. package/pi/prompts/autopilot.md +3 -3
  217. package/pi/prompts/bootstrap.md +3 -3
  218. package/pi/prompts/brainstorm.md +3 -3
  219. package/pi/prompts/close.md +2 -2
  220. package/pi/prompts/debug.md +3 -3
  221. package/pi/prompts/discovery.md +3 -3
  222. package/pi/prompts/dispatcher.md +3 -3
  223. package/pi/prompts/eli5.md +3 -3
  224. package/pi/prompts/eval.md +3 -3
  225. package/pi/prompts/evolve.md +3 -3
  226. package/pi/prompts/go.md +2 -2
  227. package/pi/prompts/grill.md +3 -3
  228. package/pi/prompts/harness-audit.md +2 -3
  229. package/pi/prompts/memory-cleanup.md +3 -3
  230. package/pi/prompts/persona-panel.md +3 -3
  231. package/pi/prompts/plan.md +3 -3
  232. package/pi/prompts/portfolio.md +2 -2
  233. package/pi/prompts/reconcile.md +3 -3
  234. package/pi/prompts/release.md +3 -3
  235. package/pi/prompts/repo-audit.md +3 -4
  236. package/pi/prompts/session.md +2 -2
  237. package/pi/prompts/spinout.md +3 -3
  238. package/pi/prompts/sunset-review.md +3 -3
  239. package/pi/prompts/templates-ack.md +1 -1
  240. package/pi/prompts/test.md +3 -3
  241. package/pi/prompts/ux-grill.md +3 -3
  242. package/rules/README.md +1 -1
  243. package/rules/opt-in-domain/prompt-caching.md +1 -1
  244. package/rules/opt-in-stack/backend-data.md +1 -1
  245. package/rules/opt-in-stack/backend.md +3 -3
  246. package/rules/opt-in-stack/frontend.md +1 -1
  247. package/rules/opt-in-stack/security-web.md +3 -3
  248. package/rules/opt-in-stack/swift.md +1 -1
  249. package/scripts/archive-closed-prds.mjs +2 -2
  250. package/scripts/auq-audit.mjs +2 -3
  251. package/scripts/autopilot.mjs +23 -2
  252. package/scripts/backfill-abandoned-sessions.mjs +171 -15
  253. package/scripts/backfill-evidence-digest.mjs +2 -1
  254. package/scripts/backfill-learnings-from-vault.mjs +2 -2
  255. package/scripts/check-package-manager.mjs +2 -2
  256. package/scripts/check-sessions-integrity.mjs +300 -0
  257. package/scripts/ci/assert-vitest-green.mjs +2 -1
  258. package/scripts/dialectic-deriver.mjs +50 -13
  259. package/scripts/emit-session.mjs +77 -32
  260. package/scripts/eval-session.mjs +65 -3
  261. package/scripts/export-hw-learnings.mjs +2 -1
  262. package/scripts/express-path.mjs +1 -1
  263. package/scripts/gc-stale-worktrees.mjs +2 -1
  264. package/scripts/generate-agents-skills.mjs +102 -29
  265. package/scripts/generate-codex-skills.mjs +48 -4
  266. package/scripts/generate-cursor-adapter.mjs +220 -11
  267. package/scripts/generate-hook-import-set.mjs +12 -27
  268. package/scripts/generate-pi-prompts.mjs +183 -13
  269. package/scripts/github-protection-audit.mjs +2 -3
  270. package/scripts/lib/agent-frontmatter.mjs +23 -1
  271. package/scripts/lib/agent-status.mjs +2 -31
  272. package/scripts/lib/auq/clarity.mjs +10 -2
  273. package/scripts/lib/auq/parse.mjs +12 -31
  274. package/scripts/lib/auq/schema.mjs +56 -41
  275. package/scripts/lib/auto-dialectic.mjs +304 -15
  276. package/scripts/lib/autopilot/flags.mjs +12 -1
  277. package/scripts/lib/autopilot/kill-switches.mjs +6 -3
  278. package/scripts/lib/autopilot/loop.mjs +14 -1
  279. package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
  280. package/scripts/lib/ci-status-banner.mjs +376 -16
  281. package/scripts/lib/claude-md-budget-lint.mjs +2 -5
  282. package/scripts/lib/command-blocker.mjs +408 -33
  283. package/scripts/lib/config/dialectic.mjs +12 -3
  284. package/scripts/lib/config/drift-check.mjs +19 -0
  285. package/scripts/lib/config/gate.mjs +74 -0
  286. package/scripts/lib/config/reaper.mjs +162 -0
  287. package/scripts/lib/config.mjs +14 -0
  288. package/scripts/lib/convergence-monitor.mjs +76 -13
  289. package/scripts/lib/cursor-hook-bridge.mjs +2 -2
  290. package/scripts/lib/description-surface.mjs +2 -5
  291. package/scripts/lib/dispatcher/cli.mjs +2 -1
  292. package/scripts/lib/ecosystem-health.mjs +11 -0
  293. package/scripts/lib/ecosystem-wizard.mjs +2 -1
  294. package/scripts/lib/eval/engine.mjs +421 -53
  295. package/scripts/lib/eval/judge.mjs +463 -40
  296. package/scripts/lib/eval/schema.mjs +10 -1
  297. package/scripts/lib/events-rotation.mjs +221 -25
  298. package/scripts/lib/events-schema.mjs +114 -0
  299. package/scripts/lib/events.mjs +524 -5
  300. package/scripts/lib/fetch-baseline.mjs +3 -8
  301. package/scripts/lib/frontmatter-guard.mjs +21 -10
  302. package/scripts/lib/gates/gate-baseline.mjs +27 -2
  303. package/scripts/lib/gates/gate-full.mjs +28 -3
  304. package/scripts/lib/gates/gate-helpers.mjs +243 -21
  305. package/scripts/lib/gates/gate-incremental.mjs +28 -3
  306. package/scripts/lib/gates/gate-per-file.mjs +27 -2
  307. package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +2 -1
  308. package/scripts/lib/gitlab-portfolio/cli.mjs +2 -1
  309. package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
  310. package/scripts/lib/instruction-budget-guard.mjs +332 -50
  311. package/scripts/lib/io.mjs +42 -8
  312. package/scripts/lib/is-main-module.mjs +82 -0
  313. package/scripts/lib/issue-close-strip-labels.mjs +207 -49
  314. package/scripts/lib/js-mask.mjs +197 -0
  315. package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
  316. package/scripts/lib/locks/index.mjs +32 -25
  317. package/scripts/lib/maintenance-due-banner.mjs +122 -91
  318. package/scripts/lib/orphan-reaper.mjs +1588 -0
  319. package/scripts/lib/peer-cards/merger.mjs +48 -10
  320. package/scripts/lib/peer-cards/reader.mjs +78 -2
  321. package/scripts/lib/peer-discovery.mjs +2 -5
  322. package/scripts/lib/playwright-driver/runner.mjs +2 -1
  323. package/scripts/lib/process-group.mjs +899 -0
  324. package/scripts/lib/quality-gate.mjs +107 -28
  325. package/scripts/lib/reconcile/backlog.mjs +368 -0
  326. package/scripts/lib/reconcile/engine.mjs +55 -188
  327. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +884 -0
  328. package/scripts/lib/reconcile/sanitize.mjs +69 -3
  329. package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
  330. package/scripts/lib/resource-probe/parsers.mjs +31 -0
  331. package/scripts/lib/rule-loader.mjs +41 -12
  332. package/scripts/lib/rules-sync.mjs +2 -5
  333. package/scripts/lib/scope-echo.mjs +429 -7
  334. package/scripts/lib/scope-gate.mjs +605 -1
  335. package/scripts/lib/session-close-backfill.mjs +91 -12
  336. package/scripts/lib/session-id.mjs +9 -20
  337. package/scripts/lib/session-invocation.mjs +20 -0
  338. package/scripts/lib/session-schema/constants.mjs +30 -2
  339. package/scripts/lib/session-schema/normalizer.mjs +56 -4
  340. package/scripts/lib/session-schema.mjs +8 -3
  341. package/scripts/lib/session-start-probes.mjs +95 -10
  342. package/scripts/lib/sessions-canonical.mjs +23 -0
  343. package/scripts/lib/sessions-integrity-banner.mjs +7 -1
  344. package/scripts/lib/sessions-staleness-banner.mjs +193 -51
  345. package/scripts/lib/skill-evidence-window.mjs +891 -0
  346. package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
  347. package/scripts/lib/skill-evolution/engine.mjs +18 -9
  348. package/scripts/lib/skill-judge.mjs +45 -3
  349. package/scripts/lib/state-md.mjs +84 -3
  350. package/scripts/lib/sunset/walker.mjs +31 -4
  351. package/scripts/lib/tail-window.mjs +56 -0
  352. package/scripts/lib/telemetry/schema.mjs +30 -0
  353. package/scripts/lib/telemetry/sync.mjs +61 -6
  354. package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
  355. package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
  356. package/scripts/lib/tests-src-ratio.mjs +2 -6
  357. package/scripts/lib/tmux-layout/telemetry-stats.mjs +74 -14
  358. package/scripts/lib/user-invocable-skills.mjs +205 -0
  359. package/scripts/lib/ux-grill/reconcile.mjs +48 -22
  360. package/scripts/lib/validate/check-agents-skills.mjs +26 -15
  361. package/scripts/lib/validate/check-banner-parity.mjs +2 -2
  362. package/scripts/lib/validate/check-cursor-adapter.mjs +3 -2
  363. package/scripts/lib/validate/check-dead-bridge.mjs +2 -2
  364. package/scripts/lib/validate/check-doc-cli-commands.mjs +2 -2
  365. package/scripts/lib/validate/check-entry-guard.mjs +329 -0
  366. package/scripts/lib/validate/check-guard-requires-parity.mjs +2 -2
  367. package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
  368. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +2 -2
  369. package/scripts/lib/validate/check-learning-provenance.mjs +2 -2
  370. package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
  371. package/scripts/lib/validate/check-rules.mjs +7 -5
  372. package/scripts/lib/validate/check-skill-links.mjs +35 -6
  373. package/scripts/lib/validate/check-skill-script-paths.mjs +241 -29
  374. package/scripts/lib/validate/check-test-git-config-target.mjs +26 -36
  375. package/scripts/lib/validate/check-unicode-safety.mjs +2 -2
  376. package/scripts/lib/validate/check-untracked-test-deps.mjs +9 -104
  377. package/scripts/lib/validate/check-unwired-features.mjs +220 -33
  378. package/scripts/lib/validate/check-validator-registration.mjs +36 -12
  379. package/scripts/lib/validate/check-vcs-repo-flag.mjs +2 -2
  380. package/scripts/lib/validate/confidential-names.mjs +10 -0
  381. package/scripts/lib/validate-vendored-rules.mjs +39 -12
  382. package/scripts/lib/vault-mirror/namespace.mjs +46 -8
  383. package/scripts/lib/vault-mirror/process.mjs +10 -3
  384. package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
  385. package/scripts/lib/vault-status/narrative-mirror.mjs +31 -7
  386. package/scripts/lib/vault-yaml.mjs +118 -0
  387. package/scripts/lib/wave-transcript-tail.mjs +2 -2
  388. package/scripts/lib/worktree/lifecycle.mjs +153 -1
  389. package/scripts/lock-reaper.mjs +2 -1
  390. package/scripts/materialize-wave-scope.mjs +87 -4
  391. package/scripts/migrate-sessions-jsonl.mjs +2 -1
  392. package/scripts/migrate-vault-paths.mjs +2 -3
  393. package/scripts/release-session-lock.mjs +305 -0
  394. package/scripts/release.mjs +109 -39
  395. package/scripts/relocate-vault-corpus.mjs +2 -3
  396. package/scripts/repair-invalid-sessions.mjs +2 -2
  397. package/scripts/resolve-session-invocation.mjs +59 -0
  398. package/scripts/run-quality-gate.mjs +156 -17
  399. package/scripts/session-shape.mjs +2 -2
  400. package/scripts/site-numbers.mjs +35 -11
  401. package/scripts/sweep-expired-rules.mjs +227 -0
  402. package/scripts/validate-plugin.mjs +21 -0
  403. package/scripts/validate-wave-scope.mjs +32 -105
  404. package/scripts/vault-consolidate.mjs +2 -2
  405. package/scripts/vault-mirror.mjs +11 -4
  406. package/scripts/wave-scope-binding.mjs +2 -3
  407. package/skills/_shared/bootstrap-gate.md +1 -1
  408. package/skills/_shared/monitor-patterns.md +1 -1
  409. package/skills/_shared/platform-tools.md +23 -11
  410. package/skills/_shared/research-evidence.md +53 -0
  411. package/skills/_shared/state-ownership.md +3 -0
  412. package/skills/autopilot/SKILL.md +80 -11
  413. package/skills/bootstrap/SKILL.md +51 -1
  414. package/skills/brainstorm/SKILL.md +16 -0
  415. package/skills/claude-md-drift-check/SKILL.md +1 -1
  416. package/skills/claude-md-drift-check/checker.mjs +49 -11
  417. package/{commands/close.md → skills/close/SKILL.md} +9 -3
  418. package/skills/convergence-monitoring/README.md +8 -1
  419. package/skills/convergence-monitoring/SIGNALS.md +50 -6
  420. package/skills/convergence-monitoring/SKILL.md +15 -6
  421. package/skills/debug/SKILL.md +10 -0
  422. package/skills/discovery/SKILL.md +24 -1
  423. package/skills/discovery/probes-session.md +2 -2
  424. package/skills/dispatcher/SKILL.md +38 -7
  425. package/skills/eli5/SKILL.md +11 -0
  426. package/skills/eval/SKILL.md +52 -23
  427. package/skills/eval/rubric-v1.md +1 -0
  428. package/skills/eval/rubric-v2.md +457 -0
  429. package/skills/evolve/SKILL.md +9 -2
  430. package/skills/evolve/references/evolve-dialectic-mode.md +46 -25
  431. package/skills/gitlab-ops/SKILL.md +3 -2
  432. package/{commands/go.md → skills/go/SKILL.md} +9 -1
  433. package/skills/grill/SKILL.md +19 -0
  434. package/{commands/harness-audit.md → skills/harness-audit/SKILL.md} +7 -2
  435. package/skills/hook-development/SKILL.md +46 -41
  436. package/skills/memory-cleanup/SKILL.md +7 -0
  437. package/skills/npm-publish/SKILL.md +2 -2
  438. package/skills/persona-panel/SKILL.md +56 -1
  439. package/skills/persona-panel/persona-format.md +1 -1
  440. package/skills/plan/SKILL.md +28 -1
  441. package/{commands/portfolio.md → skills/portfolio/SKILL.md} +8 -2
  442. package/skills/reconcile/SKILL.md +21 -0
  443. package/{commands/release.md → skills/release/SKILL.md} +16 -2
  444. package/skills/repo-audit/SKILL.md +7 -0
  445. package/skills/session-end/SKILL.md +13 -16
  446. package/skills/session-end/discovery-scan.md +1 -1
  447. package/skills/session-end/phase-3-6-tail.md +55 -9
  448. package/skills/session-end/plan-verification.md +2 -2
  449. package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
  450. package/skills/session-end/session-metrics-write.md +10 -0
  451. package/skills/session-plan/SKILL.md +18 -6
  452. package/skills/session-plan/references/session-plan-task-classification.md +2 -2
  453. package/skills/session-start/SKILL.md +5 -4
  454. package/skills/session-start/phase-8-5-express-path.md +6 -6
  455. package/skills/session-start/references/phase-1-5-session-continuity.md +1 -1
  456. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +1 -1
  457. package/skills/session-start/references/phase-4-ssot-environment-check.md +6 -4
  458. package/skills/spinout/SKILL.md +12 -1
  459. package/skills/sunset-review/SKILL.md +13 -0
  460. package/{commands/test.md → skills/test/SKILL.md} +10 -4
  461. package/skills/ux-grill/SKILL.md +20 -2
  462. package/skills/wave-executor/SKILL.md +14 -7
  463. package/skills/wave-executor/circuit-breaker.md +2 -0
  464. package/skills/wave-executor/references/wave-executor-state-init.md +18 -4
  465. package/skills/wave-executor/references/wave-loop-dispatch.md +5 -2
  466. package/skills/wave-executor/references/wave-loop-review.md +17 -1
  467. package/commands/autopilot.md +0 -80
  468. package/commands/bootstrap.md +0 -56
  469. package/commands/brainstorm.md +0 -48
  470. package/commands/debug.md +0 -36
  471. package/commands/discovery.md +0 -32
  472. package/commands/dispatcher.md +0 -59
  473. package/commands/eli5.md +0 -33
  474. package/commands/eval.md +0 -28
  475. package/commands/evolve.md +0 -10
  476. package/commands/grill.md +0 -45
  477. package/commands/memory-cleanup.md +0 -26
  478. package/commands/persona-panel.md +0 -121
  479. package/commands/plan.md +0 -15
  480. package/commands/reconcile.md +0 -23
  481. package/commands/repo-audit.md +0 -24
  482. package/commands/spinout.md +0 -15
  483. package/commands/sunset-review.md +0 -27
  484. package/commands/ux-grill.md +0 -51
@@ -0,0 +1,891 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * skill-evidence-window.mjs — the INPUT the skill-applied judge reasons over
4
+ * (issue #1399).
5
+ *
6
+ * `scripts/lib/skill-judge.mjs` has carried a `transcriptTail = ''` default
7
+ * since #645 and nothing in the tree ever produced that string. Measured
8
+ * 2026-09-19 at `8f15f77b`: `buildJudgePrompt(['<one skill>'], '', <32-hex
9
+ * nonce>)` is 1387 characters = 346 estimated tokens, comfortably under
10
+ * `DEFAULT_BUDGET.input` (8000) — so the budget gate PASSED the empty fence and
11
+ * the judge was dispatched to judge a transcript it had never seen. This module
12
+ * is the missing producer.
13
+ *
14
+ * Why not reuse `readTranscriptTail` (`hooks/_lib/subagent-transcript.mjs`):
15
+ * it concatenates the `text` blocks of the last N **assistant** records and
16
+ * drops `tool_use` entirely — which is exactly where a skill invocation lives
17
+ * (`{name:"Skill", input:{skill:"…"}}`). The last-N-records window also has no
18
+ * reason to contain the invocation at all: a `/session deep` fires at record 21
19
+ * of 4000.
20
+ *
21
+ * Shape of the answer: a WINDOW, not a tail. For each judged skill we locate
22
+ * its anchors (the invocation itself), then render bounded excerpts around the
23
+ * first anchor, the last anchor, and the end of the skill's attribution span,
24
+ * plus ONE shared excerpt of the session's closing records. Excerpts are joined
25
+ * with `\n[…]\n` so the judge can see that material was elided.
26
+ *
27
+ * Layering: pure cores first (`locateSkillAnchors`, `renderEvidence`), a thin
28
+ * IO layer after (`readTranscriptRecords`, `buildSkillEvidence`). The cores
29
+ * take records, never paths, so the tests need no filesystem; the IO layer
30
+ * takes `transcriptPath` / `projectsDir` as dependency-injection parameters.
31
+ *
32
+ * Substrate, measured 2026-09-19 over the 57 transcripts in
33
+ * `~/.claude/projects/<encoded-repo>/` (188 skill invocations in 46 files):
34
+ * (a) assistant `tool_use` `{name:"Skill", input:{skill:"<plugin>:<name>"}}`
35
+ * — 179 prefixed, 9 bare, hence the prefix-tolerant match below;
36
+ * (b) a user record carrying `<command-name>/<plugin>:<name></command-name>`;
37
+ * (c) the body arrives as the following `isMeta` record — WITH the header
38
+ * "Base directory for this skill:" for form (a) (169 of 188), and
39
+ * WITHOUT it for 91 of 101 slash-command bodies. Keying body detection on
40
+ * that header is what made the #1399 study builder drop them;
41
+ * (d) a failed call shows as `tool_result.is_error` on the anchor's id;
42
+ * (e) subagent invocations live in `<uuid>/subagents/agent-*.jsonl`;
43
+ * (f) assistant records carry an undocumented optional `attributionSkill`
44
+ * string (7843 occurrences) — used ONLY as an additional hint for where a
45
+ * skill's section ENDS, never as the anchor itself.
46
+ *
47
+ * Read-only by contract: nothing here writes a file.
48
+ */
49
+
50
+ import { createReadStream, existsSync, readdirSync, statSync } from 'node:fs';
51
+ import { homedir } from 'node:os';
52
+ import { join } from 'node:path';
53
+ import { createInterface } from 'node:readline';
54
+
55
+ import { readLock, isLockLive } from './session-lock.mjs';
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Constants (every ceiling named, per .claude/rules/build-value.md BV-004)
59
+ // ---------------------------------------------------------------------------
60
+
61
+ /** Default total character budget for the rendered evidence text. */
62
+ export const DEFAULT_BUDGET_CHARS = 30_500;
63
+
64
+ /** Characters reserved ONCE for the shared session-closing excerpt. */
65
+ export const DEFAULT_CLOSING_CHARS = 3000;
66
+
67
+ /** A skill rendered with less than this is not worth rendering — report instead. */
68
+ export const DEFAULT_MIN_PER_SKILL_CHARS = 1500;
69
+
70
+ /**
71
+ * Share of the per-skill pool that SUBAGENT-ONLY skills may take when
72
+ * coordinator-anchored skills are also present (#1412).
73
+ *
74
+ * Why a cap at all: with `includeSubagents: true` the record count grows ~4.4x
75
+ * (measured 2026-09-20 over 3 sessions) and admits a second class of judged
76
+ * skill. Under ONE shared pool that class both shrinks every coordinator
77
+ * skill's excerpt and can push one out over the `minPerSkillChars` floor —
78
+ * purely by its position in the requested-skills array. The cap plus the
79
+ * coordinator-first ordering below bound that: coordinator skills keep at
80
+ * least 1 - this share of the pool, and whatever does not fit is the NEWLY
81
+ * admitted material, reported in `skipped` with `truncated: true`.
82
+ *
83
+ * Ceiling: 0.25 is a proportion, not a measurement — the whole pool is 27,500
84
+ * characters by default and the largest window measured to date is 6,021, so
85
+ * the cap binds only under a caller-shrunk budget or a large judged set.
86
+ * Revisit if a real session ever reports a subagent-only skill skipped as
87
+ * `budget-insufficient` while the coordinator sections sit far under budget.
88
+ */
89
+ export const DEFAULT_SUBAGENT_POOL_SHARE = 0.25;
90
+
91
+ /** Cap for a single `tool_use` input / `tool_result` payload inside an excerpt. */
92
+ export const DEFAULT_TOOL_TEXT_MAX = 1500;
93
+
94
+ /**
95
+ * Per-block read-time cap. Skill bodies measured at median 39,667 and max
96
+ * 129,144 characters; a 24 MB transcript holds hundreds of them, so keeping
97
+ * them whole in memory buys nothing — the first heading, the only part we
98
+ * render, sits in the first few hundred characters. 4000 is 2.6x the
99
+ * `toolTextMax` cap above, so no rendered excerpt is ever clipped by THIS
100
+ * limit first. Revisit if an excerpt form ever needs more than 4000 characters
101
+ * of a single block.
102
+ */
103
+ export const DEFAULT_MAX_BLOCK_CHARS = 4000;
104
+
105
+ /** Records rendered before / after an anchor record. */
106
+ const CONTEXT_BEFORE = 1;
107
+ const CONTEXT_AFTER = 3;
108
+
109
+ /** How far past an anchor to look for its `isMeta` body record. */
110
+ const BODY_LOOKAHEAD = 4;
111
+
112
+ /** Records in the shared session-closing excerpt. */
113
+ const CLOSING_RECORDS = 12;
114
+
115
+ /** Separator between two non-adjacent excerpts. */
116
+ const ELISION = '\n[…]\n';
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Pure helpers
120
+ // ---------------------------------------------------------------------------
121
+
122
+ /**
123
+ * The bare skill name — everything after the last `:`. `session-orchestrator:plan`
124
+ * and a bare `plan` name the same skill in a transcript (9 of 188 measured
125
+ * invocations carry no plugin prefix), so both spellings must match.
126
+ *
127
+ * @param {unknown} name
128
+ * @returns {string}
129
+ */
130
+ export function bareSkillName(name) {
131
+ const s = typeof name === 'string' ? name.trim() : '';
132
+ const idx = s.lastIndexOf(':');
133
+ return idx === -1 ? s : s.slice(idx + 1);
134
+ }
135
+
136
+ /**
137
+ * Prefix-tolerant skill match. Exact equality wins; otherwise the bare names
138
+ * are compared. Ceiling: two plugins exporting the same bare name would match
139
+ * each other. Accepted — the consequence is a slightly wider evidence window,
140
+ * never a missing anchor, and the alternative (exact-only) silently loses the
141
+ * 9-of-188 bare-call form.
142
+ *
143
+ * @param {unknown} candidate
144
+ * @param {unknown} skill
145
+ * @returns {boolean}
146
+ */
147
+ export function skillMatches(candidate, skill) {
148
+ if (typeof candidate !== 'string' || typeof skill !== 'string') return false;
149
+ const a = candidate.trim();
150
+ const b = skill.trim();
151
+ if (!a || !b) return false;
152
+ if (a === b) return true;
153
+ return bareSkillName(a) === bareSkillName(b);
154
+ }
155
+
156
+ /** @param {unknown} rec @returns {Array<Record<string, unknown>>} */
157
+ function contentBlocks(rec) {
158
+ const content = rec?.message?.content;
159
+ if (Array.isArray(content)) return content;
160
+ if (typeof content === 'string') return [{ type: 'text', text: content }];
161
+ return [];
162
+ }
163
+
164
+ /** Concatenated plain text of a record's `text` blocks (thinking excluded). */
165
+ function recordText(rec) {
166
+ const parts = [];
167
+ for (const b of contentBlocks(rec)) {
168
+ if (b?.type === 'text' && typeof b.text === 'string') parts.push(b.text);
169
+ }
170
+ return parts.join('\n');
171
+ }
172
+
173
+ /** @param {string} s @param {number} max @returns {string} */
174
+ function clip(s, max) {
175
+ const str = typeof s === 'string' ? s : String(s ?? '');
176
+ if (str.length <= max) return str;
177
+ return `${str.slice(0, max)}…[+${str.length - max} chars]`;
178
+ }
179
+
180
+ /** First markdown heading of a body, else its first non-empty line. */
181
+ function firstHeading(text) {
182
+ const lines = String(text ?? '').split('\n');
183
+ for (const line of lines) {
184
+ if (/^#{1,6}\s+\S/.test(line)) return line.trim();
185
+ }
186
+ for (const line of lines) {
187
+ if (line.trim()) return clip(line.trim(), 120);
188
+ }
189
+ return '(empty body)';
190
+ }
191
+
192
+ /**
193
+ * Is this record a SUBAGENT record? `_subagent` is stamped by
194
+ * `readTranscriptRecords` for `<uuid>/subagents/agent-*.jsonl`; `isSidechain`
195
+ * is stamped by the harness itself. ONE definition, because two consumers read
196
+ * it: the anchor classifier below and the budget split in `renderEvidence`.
197
+ *
198
+ * @param {unknown} rec
199
+ * @returns {boolean}
200
+ */
201
+ function isSubagentRecord(rec) {
202
+ return rec?._subagent === true || rec?.isSidechain === true;
203
+ }
204
+
205
+ /** `<command-name>/session-orchestrator:close</command-name>` → the name. */
206
+ function commandNames(text) {
207
+ const out = [];
208
+ const re = /<command-name>\s*\/?([^<\s]+)\s*<\/command-name>/g;
209
+ let m;
210
+ while ((m = re.exec(text)) !== null) out.push(m[1]);
211
+ return out;
212
+ }
213
+
214
+ // ---------------------------------------------------------------------------
215
+ // Core 1 — locateSkillAnchors
216
+ // ---------------------------------------------------------------------------
217
+
218
+ /**
219
+ * @typedef {Object} SkillAnchor
220
+ * @property {'skill-call'|'slash-command'|'subagent-skill-call'} kind
221
+ * @property {number} recordIndex — index into `records` of the invoking record
222
+ * @property {boolean} errored — the matching `tool_result` carried `is_error: true`
223
+ * @property {number|null} bodyRecordIndex — index of the following `isMeta` body record
224
+ */
225
+
226
+ /**
227
+ * @typedef {Object} SkillLocation
228
+ * @property {boolean} found
229
+ * @property {SkillAnchor[]} anchors
230
+ * @property {number} invocations
231
+ * @property {number|null} spanEndIndex — last record with `attributionSkill === skill`
232
+ * @property {number|null} spanEndIndexMain — same, but restricted to MAIN-transcript
233
+ * records (`!isSubagentRecord`). Identical to `spanEndIndex` whenever no subagent
234
+ * records are in `records` at all; diverges only under `includeSubagents: true`. #1421.
235
+ */
236
+
237
+ /**
238
+ * Locate every invocation anchor of every requested skill. PURE — takes
239
+ * records, returns indices; reads no file and renders no text.
240
+ *
241
+ * A record is treated as a subagent record when it carries `_subagent: true`
242
+ * (stamped by `readTranscriptRecords`) or `isSidechain: true`.
243
+ *
244
+ * @param {Array<Record<string, unknown>>} records
245
+ * @param {string[]} skills
246
+ * @returns {Record<string, SkillLocation>} keyed by the REQUESTED skill spelling
247
+ */
248
+ export function locateSkillAnchors(records, skills) {
249
+ /** @type {Record<string, SkillLocation>} */
250
+ const out = Object.create(null);
251
+ const wanted = Array.isArray(skills)
252
+ ? skills.filter((s) => typeof s === 'string' && s.trim())
253
+ : [];
254
+ for (const skill of wanted) {
255
+ out[skill] = {
256
+ found: false,
257
+ anchors: [],
258
+ invocations: 0,
259
+ spanEndIndex: null,
260
+ spanEndIndexMain: null,
261
+ };
262
+ }
263
+ if (!Array.isArray(records) || wanted.length === 0) return out;
264
+
265
+ /** tool_use id → the anchor object awaiting its tool_result verdict. */
266
+ const pendingByToolUseId = new Map();
267
+
268
+ for (let i = 0; i < records.length; i += 1) {
269
+ const rec = records[i];
270
+ if (!rec || typeof rec !== 'object') continue;
271
+ const isSubagent = isSubagentRecord(rec);
272
+
273
+ for (const block of contentBlocks(rec)) {
274
+ if (block?.type === 'tool_use' && block.name === 'Skill') {
275
+ const called = block?.input?.skill;
276
+ for (const skill of wanted) {
277
+ if (!skillMatches(called, skill)) continue;
278
+ /** @type {SkillAnchor} */
279
+ const anchor = {
280
+ kind: isSubagent ? 'subagent-skill-call' : 'skill-call',
281
+ recordIndex: i,
282
+ errored: false,
283
+ bodyRecordIndex: null,
284
+ };
285
+ out[skill].anchors.push(anchor);
286
+ if (typeof block.id === 'string') {
287
+ const list = pendingByToolUseId.get(block.id) ?? [];
288
+ list.push(anchor);
289
+ pendingByToolUseId.set(block.id, list);
290
+ }
291
+ }
292
+ } else if (block?.type === 'tool_result' && block.is_error === true) {
293
+ const list = pendingByToolUseId.get(block.tool_use_id);
294
+ if (list) for (const a of list) a.errored = true;
295
+ }
296
+ }
297
+
298
+ // Slash-command form. The `<command-name>` marker lives in a plain-text
299
+ // user record; the body follows as the next `isMeta` record, usually with
300
+ // NO "Base directory for this skill:" header.
301
+ const text = recordText(rec);
302
+ if (text.includes('<command-name>')) {
303
+ for (const name of commandNames(text)) {
304
+ for (const skill of wanted) {
305
+ if (!skillMatches(name, skill)) continue;
306
+ out[skill].anchors.push({
307
+ kind: 'slash-command',
308
+ recordIndex: i,
309
+ errored: false,
310
+ bodyRecordIndex: null,
311
+ });
312
+ }
313
+ }
314
+ }
315
+
316
+ // (f) attributionSkill — the END of the skill's section, never the anchor.
317
+ // Tracked TWICE: over every record, and over MAIN-transcript records only.
318
+ // Under `includeSubagents: true` the subagent files are concatenated after
319
+ // the main ones, so a coordinator-dispatched skill whose work continues
320
+ // inside a subagent would otherwise take its span end from that subagent
321
+ // file (#1421: `wave-executor` in session 58f82af0 moved 357 → 7449).
322
+ if (typeof rec.attributionSkill === 'string') {
323
+ for (const skill of wanted) {
324
+ if (!skillMatches(rec.attributionSkill, skill)) continue;
325
+ out[skill].spanEndIndex = i;
326
+ if (!isSubagent) out[skill].spanEndIndexMain = i;
327
+ }
328
+ }
329
+ }
330
+
331
+ // Body record per anchor: the next `isMeta` record carrying text, header or not.
332
+ for (const skill of wanted) {
333
+ const loc = out[skill];
334
+ for (const anchor of loc.anchors) {
335
+ for (let k = anchor.recordIndex + 1; k <= anchor.recordIndex + BODY_LOOKAHEAD; k += 1) {
336
+ const rec = records[k];
337
+ if (!rec || typeof rec !== 'object') continue;
338
+ if (rec.isMeta !== true) continue;
339
+ if (!recordText(rec).trim()) continue;
340
+ anchor.bodyRecordIndex = k;
341
+ break;
342
+ }
343
+ }
344
+ loc.invocations = loc.anchors.length;
345
+ loc.found = loc.anchors.length > 0;
346
+ }
347
+
348
+ return out;
349
+ }
350
+
351
+ // ---------------------------------------------------------------------------
352
+ // Core 2 — renderEvidence
353
+ // ---------------------------------------------------------------------------
354
+
355
+ /**
356
+ * Render ONE record as a single evidence line. Thinking blocks are dropped
357
+ * (they are the largest and the least evidential part of a transcript);
358
+ * `tool_use` / `tool_result` payloads are clipped at `toolTextMax`; a skill
359
+ * body collapses to `[skill body: <name>] <first heading>`.
360
+ *
361
+ * @param {Record<string, unknown>} rec
362
+ * @param {number} index
363
+ * @param {{toolTextMax: number, bodyOf?: string|null}} opts
364
+ * @returns {string}
365
+ */
366
+ function renderRecord(rec, index, { toolTextMax, bodyOf = null }) {
367
+ const role = typeof rec?.type === 'string' ? rec.type : 'unknown';
368
+ if (bodyOf) {
369
+ return `#${index} ${role} [skill body: ${bodyOf}] ${firstHeading(recordText(rec))}`;
370
+ }
371
+ const parts = [];
372
+ for (const block of contentBlocks(rec)) {
373
+ const t = block?.type;
374
+ if (t === 'thinking' || t === 'redacted_thinking') continue;
375
+ if (t === 'text' && typeof block.text === 'string') {
376
+ if (block.text.trim()) parts.push(clip(block.text.trim(), toolTextMax));
377
+ } else if (t === 'tool_use') {
378
+ let input;
379
+ try {
380
+ input = JSON.stringify(block.input ?? {});
381
+ } catch {
382
+ input = '(uninspectable input)';
383
+ }
384
+ parts.push(`[tool_use ${String(block.name ?? '?')}] ${clip(input, toolTextMax)}`);
385
+ } else if (t === 'tool_result') {
386
+ const payload =
387
+ typeof block.content === 'string'
388
+ ? block.content
389
+ : Array.isArray(block.content)
390
+ ? block.content.map((c) => (typeof c?.text === 'string' ? c.text : '')).join('\n')
391
+ : '';
392
+ const flag = block.is_error === true ? ' is_error=true' : '';
393
+ parts.push(`[tool_result${flag}] ${clip(payload, toolTextMax)}`);
394
+ }
395
+ }
396
+ if (parts.length === 0) return '';
397
+ return `#${index} ${role} ${parts.join(' | ')}`;
398
+ }
399
+
400
+ /** Merge overlapping/adjacent `{start,end}` windows. */
401
+ function mergeWindows(windows) {
402
+ const sorted = windows
403
+ .filter((w) => w && w.end >= w.start)
404
+ .sort((a, b) => a.start - b.start);
405
+ const merged = [];
406
+ for (const w of sorted) {
407
+ const last = merged[merged.length - 1];
408
+ if (last && w.start <= last.end + 1) last.end = Math.max(last.end, w.end);
409
+ else merged.push({ ...w });
410
+ }
411
+ return merged;
412
+ }
413
+
414
+ /**
415
+ * Render ONE window backwards from its last record, so the budget is spent on
416
+ * the records CLOSEST to the end. Used for the shared closing excerpt, whose
417
+ * whole job is to show how the session finished: a forward fill spends the
418
+ * budget on whatever tool spam happens to sit at the window's start and never
419
+ * reaches the last record at all (observed with 12 clipped 1500-character
420
+ * `tool_use` lines against a 3000-character closing budget).
421
+ */
422
+ function renderWindowFromEnd(records, window, { toolTextMax, budget, bodyIndexes }) {
423
+ const lines = [];
424
+ let used = 0;
425
+ let truncated = false;
426
+ const start = Math.max(0, window.start);
427
+ const end = Math.min(records.length - 1, window.end);
428
+ for (let i = end; i >= start; i -= 1) {
429
+ const line = renderRecord(records[i], i, { toolTextMax, bodyOf: bodyIndexes.get(i) ?? null });
430
+ if (!line) continue;
431
+ if (used + line.length + 1 > budget) {
432
+ truncated = true;
433
+ break;
434
+ }
435
+ lines.unshift(line);
436
+ used += line.length + 1;
437
+ }
438
+ return { text: lines.join('\n'), truncated };
439
+ }
440
+
441
+ /** Render merged windows into elision-joined text, stopping at `budget`. */
442
+ function renderWindows(records, windows, { toolTextMax, budget, bodyIndexes }) {
443
+ const chunks = [];
444
+ let used = 0;
445
+ let truncated = false;
446
+ for (const w of windows) {
447
+ const lines = [];
448
+ for (let i = Math.max(0, w.start); i <= Math.min(records.length - 1, w.end); i += 1) {
449
+ const line = renderRecord(records[i], i, {
450
+ toolTextMax,
451
+ bodyOf: bodyIndexes.get(i) ?? null,
452
+ });
453
+ if (!line) continue;
454
+ if (used + line.length + 1 > budget) {
455
+ truncated = true;
456
+ break;
457
+ }
458
+ lines.push(line);
459
+ used += line.length + 1;
460
+ }
461
+ if (lines.length > 0) chunks.push(lines.join('\n'));
462
+ if (truncated) break;
463
+ }
464
+ return { text: chunks.join(ELISION), truncated };
465
+ }
466
+
467
+ /**
468
+ * @typedef {Object} EvidenceRender
469
+ * @property {string} text
470
+ * @property {number} chars
471
+ * @property {boolean} truncated
472
+ * @property {Array<{skill: string, found: boolean, invocations: number, chars: number, anchorIndexes: number[]}>} perSkill
473
+ * @property {Array<{skill: string, reason: string}>} skipped
474
+ */
475
+
476
+ /**
477
+ * Render the evidence text from the located anchors. PURE — no filesystem.
478
+ *
479
+ * Budget split: the shared closing excerpt is paid ONCE, the rest is divided
480
+ * evenly across the skills that were actually found. A skill that cannot get
481
+ * `minPerSkillChars` is NOT silently squeezed — it is reported in `skipped`
482
+ * with reason `budget-insufficient` and `truncated` is set, so the caller can
483
+ * see that the judge is reasoning over a partial set.
484
+ *
485
+ * SOURCE SPLIT (#1412): once subagent transcripts are read in, the found set
486
+ * carries two classes. Skills with at least one COORDINATOR anchor are
487
+ * allotted first and from their own pool; SUBAGENT-ONLY skills share at most
488
+ * `DEFAULT_SUBAGENT_POOL_SHARE` of the pool — unless there are no coordinator
489
+ * skills at all, in which case they get all of it (the whole point of #1412 is
490
+ * that a subagent-only skill must be judgeable). With no subagent-only skill
491
+ * present the split is a no-op and the numbers are identical to before.
492
+ *
493
+ * `mainRecordCount` bounds the shared closing excerpt to the MAIN transcript.
494
+ * Subagent records are CONCATENATED after it, so without the bound the section
495
+ * headed "session closing" renders the tail of the last subagent file instead
496
+ * — mislabelled evidence, which is worse for a judge than none.
497
+ *
498
+ * @param {Array<Record<string, unknown>>} records
499
+ * @param {Record<string, SkillLocation>} located
500
+ * @param {{budgetChars?: number, closingChars?: number, minPerSkillChars?: number, toolTextMax?: number, subagentPoolShare?: number, mainRecordCount?: number}} [opts]
501
+ * @returns {EvidenceRender}
502
+ */
503
+ export function renderEvidence(records, located, opts = {}) {
504
+ const budgetChars = Number.isFinite(opts.budgetChars) ? opts.budgetChars : DEFAULT_BUDGET_CHARS;
505
+ const closingChars = Number.isFinite(opts.closingChars)
506
+ ? opts.closingChars
507
+ : DEFAULT_CLOSING_CHARS;
508
+ const minPerSkillChars = Number.isFinite(opts.minPerSkillChars)
509
+ ? opts.minPerSkillChars
510
+ : DEFAULT_MIN_PER_SKILL_CHARS;
511
+ const toolTextMax = Number.isFinite(opts.toolTextMax) ? opts.toolTextMax : DEFAULT_TOOL_TEXT_MAX;
512
+ const subagentPoolShare = Number.isFinite(opts.subagentPoolShare)
513
+ ? Math.min(1, Math.max(0, opts.subagentPoolShare))
514
+ : DEFAULT_SUBAGENT_POOL_SHARE;
515
+
516
+ const recs = Array.isArray(records) ? records : [];
517
+ const mainRecordCount = Number.isFinite(opts.mainRecordCount)
518
+ ? Math.min(recs.length, Math.max(0, opts.mainRecordCount))
519
+ : recs.length;
520
+ const loc = located && typeof located === 'object' ? located : {};
521
+ const skills = Object.keys(loc);
522
+
523
+ /** @type {Array<{skill: string, reason: string}>} */
524
+ const skipped = [];
525
+ const foundSkills = [];
526
+ for (const skill of skills) {
527
+ if (loc[skill]?.found) foundSkills.push(skill);
528
+ else skipped.push({ skill, reason: 'not-found' });
529
+ }
530
+
531
+ // Body-record index → the skill whose body it is (for the collapsed render).
532
+ const bodyIndexes = new Map();
533
+ for (const skill of foundSkills) {
534
+ for (const a of loc[skill].anchors) {
535
+ if (typeof a.bodyRecordIndex === 'number') bodyIndexes.set(a.bodyRecordIndex, skill);
536
+ }
537
+ }
538
+
539
+ const closingBudget = recs.length > 0 ? Math.max(0, Math.min(closingChars, budgetChars)) : 0;
540
+ const perSkillPool = Math.max(0, budgetChars - closingBudget);
541
+
542
+ // Source split — coordinator-anchored skills first, subagent-only after.
543
+ const coordinatorSkills = [];
544
+ const subagentOnlySkills = [];
545
+ for (const skill of foundSkills) {
546
+ const anchors = loc[skill].anchors;
547
+ if (anchors.some((a) => !isSubagentRecord(recs[a.recordIndex]))) coordinatorSkills.push(skill);
548
+ else subagentOnlySkills.push(skill);
549
+ }
550
+ const coordinatorAnchored = new Set(coordinatorSkills);
551
+ let subagentPool = 0;
552
+ if (subagentOnlySkills.length > 0) {
553
+ subagentPool =
554
+ coordinatorSkills.length === 0
555
+ ? perSkillPool
556
+ : Math.floor(perSkillPool * subagentPoolShare);
557
+ }
558
+ const coordinatorPool = perSkillPool - subagentPool;
559
+
560
+ let truncated = false;
561
+ /** Allot `pool` over `list`; overflow past the floor is reported, never squeezed. */
562
+ const allot = (list, pool) => {
563
+ const capacity = minPerSkillChars > 0 ? Math.floor(pool / minPerSkillChars) : list.length;
564
+ const take = Math.max(0, capacity);
565
+ for (const skill of list.slice(take)) {
566
+ skipped.push({ skill, reason: 'budget-insufficient' });
567
+ truncated = true;
568
+ }
569
+ const kept = list.slice(0, take);
570
+ const budget = kept.length > 0 ? Math.floor(pool / kept.length) : 0;
571
+ return kept.map((skill) => ({ skill, budget }));
572
+ };
573
+ const rendered = [
574
+ ...allot(coordinatorSkills, coordinatorPool),
575
+ ...allot(subagentOnlySkills, subagentPool),
576
+ ];
577
+
578
+ const sections = [];
579
+ /** @type {EvidenceRender['perSkill']} */
580
+ const perSkill = [];
581
+
582
+ for (const { skill, budget: perSkillBudget } of rendered) {
583
+ const info = loc[skill];
584
+ const anchors = info.anchors;
585
+ const picks = [anchors[0], anchors[anchors.length - 1]].filter(Boolean);
586
+ const windows = picks.map((a) => ({
587
+ start: a.recordIndex - CONTEXT_BEFORE,
588
+ end: Math.max(a.recordIndex + CONTEXT_AFTER, a.bodyRecordIndex ?? -1),
589
+ }));
590
+ // Span end (#1421). A COORDINATOR-anchored skill takes its span end from the
591
+ // MAIN transcript only: the skill was dispatched by the coordinator, so its
592
+ // window must not end inside a subagent file that merely carries the same
593
+ // `attributionSkill`. A subagent-only skill has no main-transcript span end
594
+ // by construction and keeps the unrestricted one. Both fields are equal
595
+ // whenever the caller passed no subagent records (the library default
596
+ // `includeSubagents: false`), so this is a no-op for every other caller.
597
+ const spanEnd = coordinatorAnchored.has(skill) ? info.spanEndIndexMain : info.spanEndIndex;
598
+ if (typeof spanEnd === 'number') {
599
+ windows.push({ start: spanEnd - CONTEXT_BEFORE, end: spanEnd });
600
+ }
601
+ const body = renderWindows(recs, mergeWindows(windows), {
602
+ toolTextMax,
603
+ budget: perSkillBudget,
604
+ bodyIndexes,
605
+ });
606
+ if (body.truncated) truncated = true;
607
+ const errored = anchors.some((a) => a.errored);
608
+ const header =
609
+ `### ${skill} — ${info.invocations} invocation(s), kinds: ` +
610
+ `${[...new Set(anchors.map((a) => a.kind))].join(', ')}` +
611
+ `${errored ? ', at least one call errored' : ''}`;
612
+ sections.push(`${header}\n${body.text}`);
613
+ perSkill.push({
614
+ skill,
615
+ found: true,
616
+ invocations: info.invocations,
617
+ chars: body.text.length,
618
+ anchorIndexes: anchors.map((a) => a.recordIndex),
619
+ });
620
+ }
621
+
622
+ for (const entry of skipped) {
623
+ perSkill.push({
624
+ skill: entry.skill,
625
+ found: loc[entry.skill]?.found === true,
626
+ invocations: loc[entry.skill]?.invocations ?? 0,
627
+ chars: 0,
628
+ anchorIndexes: (loc[entry.skill]?.anchors ?? []).map((a) => a.recordIndex),
629
+ });
630
+ }
631
+
632
+ // The shared closing is EVIDENCE FOR a judged skill ("did its work complete?"),
633
+ // never evidence on its own. With no skill rendered there is nothing to attach
634
+ // it to, and emitting it alone would hand the judge a non-empty fence that
635
+ // contains no invocation — the #1399 defect in a new costume.
636
+ if (closingBudget > 0 && rendered.length > 0 && mainRecordCount > 0) {
637
+ const closing = renderWindowFromEnd(
638
+ recs,
639
+ { start: Math.max(0, mainRecordCount - CLOSING_RECORDS), end: mainRecordCount - 1 },
640
+ { toolTextMax, budget: closingBudget, bodyIndexes },
641
+ );
642
+ if (closing.truncated) truncated = true;
643
+ if (closing.text) sections.push(`### session closing\n${closing.text}`);
644
+ }
645
+
646
+ const text = sections.length > 0 ? `## Skill evidence (excerpts)\n\n${sections.join('\n\n')}` : '';
647
+ return { text, chars: text.length, truncated, perSkill, skipped };
648
+ }
649
+
650
+ // ---------------------------------------------------------------------------
651
+ // IO layer
652
+ // ---------------------------------------------------------------------------
653
+
654
+ /**
655
+ * Encode a repo path the way Claude Code names its projects directory: every
656
+ * `/` and `.` becomes `-`. Same encoding as
657
+ * `scripts/lib/wave-transcript-tail.mjs::encodeProjectDir`, duplicated here
658
+ * rather than imported so this module stays free of that tailer's `events.mjs`
659
+ * / file-lock / subagent-paths closure.
660
+ *
661
+ * @param {string} repoRoot
662
+ * @returns {string}
663
+ */
664
+ export function encodeProjectDir(repoRoot) {
665
+ return String(repoRoot).replace(/[/.]/g, '-');
666
+ }
667
+
668
+ /**
669
+ * Resolve the RAW harness session id (the UUID that names the transcript file).
670
+ *
671
+ * Deliberately WITHOUT the newest-by-mtime tier that
672
+ * `wave-transcript-tail.mjs::resolveSessionId` carries: in a shared working
673
+ * copy the newest transcript directory can belong to a PEER session, and
674
+ * judging this session's skills against a peer's transcript is worse than
675
+ * judging nothing (`.claude/rules/identity-and-locks.md` — rank witnesses,
676
+ * never fall back onto a peer's artefact). No id resolves → the caller reports
677
+ * `no-transcript`.
678
+ *
679
+ * @param {{repoRoot?: string, env?: Record<string, string|undefined>}} [opts]
680
+ * @returns {{sessionId: string, source: 'env'|'session.lock'} | null}
681
+ */
682
+ export function resolveRawSessionId({ repoRoot, env = process.env } = {}) {
683
+ const fromEnv = typeof env.CLAUDE_CODE_SESSION_ID === 'string'
684
+ ? env.CLAUDE_CODE_SESSION_ID.trim()
685
+ : '';
686
+ if (fromEnv) return { sessionId: fromEnv, source: 'env' };
687
+ if (!repoRoot) return null;
688
+ let lock;
689
+ try {
690
+ lock = readLock({ repoRoot });
691
+ } catch {
692
+ return null;
693
+ }
694
+ if (!lock || !isLockLive(lock)) return null;
695
+ const fromLock = typeof lock.session_id === 'string' ? lock.session_id.trim() : '';
696
+ return fromLock ? { sessionId: fromLock, source: 'session.lock' } : null;
697
+ }
698
+
699
+ /**
700
+ * Stream-read a transcript JSONL.
701
+ *
702
+ * `malformed_lines` is part of the RETURN VALUE, not a silent skip: a
703
+ * half-written line turns a partial result into a clean-looking verdict, which
704
+ * is precisely the failure the judge exists to notice
705
+ * (`.claude/rules/measurement-discipline.md` — "Ein still ueberspringender
706
+ * JSONL-Parser macht aus einem Teilergebnis ein sauberes Verdikt").
707
+ *
708
+ * Text blocks are clipped at `maxBlockChars` AT READ TIME — see
709
+ * `DEFAULT_MAX_BLOCK_CHARS` for the ceiling and its revisit trigger.
710
+ *
711
+ * @param {string} path
712
+ * @param {{maxBlockChars?: number, subagent?: boolean}} [opts]
713
+ * @returns {Promise<{records: Array<Record<string, unknown>>, malformed_lines: number, bytes: number}>}
714
+ */
715
+ export async function readTranscriptRecords(path, opts = {}) {
716
+ const maxBlockChars = Number.isFinite(opts.maxBlockChars)
717
+ ? opts.maxBlockChars
718
+ : DEFAULT_MAX_BLOCK_CHARS;
719
+ const records = [];
720
+ let malformedLines = 0;
721
+ let bytes;
722
+ if (typeof path !== 'string' || !path || !existsSync(path)) {
723
+ return { records, malformed_lines: 0, bytes: 0 };
724
+ }
725
+ try {
726
+ bytes = statSync(path).size;
727
+ } catch {
728
+ bytes = 0;
729
+ }
730
+
731
+ const rl = createInterface({
732
+ input: createReadStream(path, { encoding: 'utf8' }),
733
+ crlfDelay: Infinity,
734
+ });
735
+ for await (const line of rl) {
736
+ const trimmed = line.trim();
737
+ if (!trimmed) continue;
738
+ let rec;
739
+ try {
740
+ rec = JSON.parse(trimmed);
741
+ } catch {
742
+ malformedLines += 1;
743
+ continue;
744
+ }
745
+ if (!rec || typeof rec !== 'object' || Array.isArray(rec)) {
746
+ malformedLines += 1;
747
+ continue;
748
+ }
749
+ const content = rec?.message?.content;
750
+ if (Array.isArray(content)) {
751
+ for (const block of content) {
752
+ if (block && typeof block.text === 'string' && block.text.length > maxBlockChars) {
753
+ block.text = clip(block.text, maxBlockChars);
754
+ }
755
+ }
756
+ }
757
+ if (opts.subagent === true) rec._subagent = true;
758
+ records.push(rec);
759
+ }
760
+ return { records, malformed_lines: malformedLines, bytes };
761
+ }
762
+
763
+ /**
764
+ * @typedef {Object} SkillEvidence
765
+ * @property {'ok'|'no-transcript'|'no-evidence'} status
766
+ * @property {{path: string|null, bytes: number, records: number, malformed_lines: number}} source
767
+ * @property {EvidenceRender['perSkill']} perSkill
768
+ * @property {Array<{skill: string, reason: string}>} skipped
769
+ * @property {string} text
770
+ * @property {number} chars
771
+ * @property {boolean} truncated
772
+ */
773
+
774
+ /**
775
+ * Build the judge's evidence window for `skills` from this session's transcript.
776
+ *
777
+ * `transcriptPath` and `projectsDir` are dependency-injection parameters —
778
+ * pass them in tests; in production omit both and the path resolves to
779
+ * `~/.claude/projects/<encodeProjectDir(repoRoot)>/<sessionId>.jsonl`.
780
+ *
781
+ * NEVER throws: every failure degrades to a status the caller can log.
782
+ *
783
+ * @param {object} opts
784
+ * @param {string} [opts.repoRoot]
785
+ * @param {string} [opts.sessionId] — the RAW harness UUID, not the semantic id
786
+ * @param {string[]} [opts.skills]
787
+ * @param {number} [opts.budgetChars]
788
+ * @param {string} [opts.transcriptPath] — DI: read exactly this file
789
+ * @param {string} [opts.projectsDir] — DI: `<projects>/<encoded-repo>` directory
790
+ * @param {boolean} [opts.includeSubagents=false]
791
+ * @param {Record<string, string|undefined>} [opts.env]
792
+ * @returns {Promise<SkillEvidence>}
793
+ */
794
+ export async function buildSkillEvidence({
795
+ repoRoot,
796
+ sessionId,
797
+ skills = [],
798
+ budgetChars = DEFAULT_BUDGET_CHARS,
799
+ transcriptPath,
800
+ projectsDir,
801
+ includeSubagents = false,
802
+ env = process.env,
803
+ } = {}) {
804
+ const empty = (status, path = null, extra = {}) => ({
805
+ status,
806
+ source: { path, bytes: 0, records: 0, malformed_lines: 0, ...extra },
807
+ perSkill: [],
808
+ skipped: (Array.isArray(skills) ? skills : [])
809
+ .filter((s) => typeof s === 'string' && s.trim())
810
+ .map((skill) => ({ skill, reason: status })),
811
+ text: '',
812
+ chars: 0,
813
+ truncated: false,
814
+ });
815
+
816
+ const wanted = Array.isArray(skills) ? skills.filter((s) => typeof s === 'string' && s.trim()) : [];
817
+ if (wanted.length === 0) return empty('no-evidence');
818
+
819
+ const rawId =
820
+ typeof sessionId === 'string' && sessionId.trim()
821
+ ? sessionId.trim()
822
+ : resolveRawSessionId({ repoRoot, env })?.sessionId ?? '';
823
+
824
+ const baseDir =
825
+ projectsDir ||
826
+ (repoRoot ? join(homedir(), '.claude', 'projects', encodeProjectDir(repoRoot)) : '');
827
+ const path = transcriptPath || (rawId && baseDir ? join(baseDir, `${rawId}.jsonl`) : '');
828
+ if (!path || !existsSync(path)) return empty('no-transcript', path || null);
829
+
830
+ let main;
831
+ try {
832
+ main = await readTranscriptRecords(path);
833
+ } catch {
834
+ return empty('no-transcript', path);
835
+ }
836
+ const records = main.records;
837
+ // Captured BEFORE the subagent records are concatenated — it is the boundary
838
+ // `renderEvidence` needs to keep the "session closing" excerpt on the MAIN
839
+ // transcript instead of on the last subagent file.
840
+ const mainRecordCount = records.length;
841
+ let malformed = main.malformed_lines;
842
+ let bytes = main.bytes;
843
+
844
+ if (includeSubagents && rawId && baseDir) {
845
+ const subDir = join(baseDir, rawId, 'subagents');
846
+ let entries;
847
+ try {
848
+ entries = existsSync(subDir)
849
+ ? readdirSync(subDir).filter((f) => f.startsWith('agent-') && f.endsWith('.jsonl'))
850
+ : [];
851
+ } catch {
852
+ entries = [];
853
+ }
854
+ for (const entry of entries.sort()) {
855
+ try {
856
+ const sub = await readTranscriptRecords(join(subDir, entry), { subagent: true });
857
+ records.push(...sub.records);
858
+ malformed += sub.malformed_lines;
859
+ bytes += sub.bytes;
860
+ } catch {
861
+ // An unreadable subagent file removes evidence, never correctness.
862
+ }
863
+ }
864
+ }
865
+
866
+ const source = { path, bytes, records: records.length, malformed_lines: malformed };
867
+ const located = locateSkillAnchors(records, wanted);
868
+ const render = renderEvidence(records, located, { budgetChars, mainRecordCount });
869
+
870
+ if (!render.text) {
871
+ return {
872
+ status: 'no-evidence',
873
+ source,
874
+ perSkill: render.perSkill,
875
+ skipped: render.skipped,
876
+ text: '',
877
+ chars: 0,
878
+ truncated: render.truncated,
879
+ };
880
+ }
881
+
882
+ return {
883
+ status: 'ok',
884
+ source,
885
+ perSkill: render.perSkill,
886
+ skipped: render.skipped,
887
+ text: render.text,
888
+ chars: render.chars,
889
+ truncated: render.truncated,
890
+ };
891
+ }