session-orchestrator 3.24.0 → 4.0.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 (350) hide show
  1. package/.agents/skills/architecture/SKILL.md +18 -0
  2. package/.agents/skills/autopilot/SKILL.md +17 -0
  3. package/.agents/skills/bootstrap/SKILL.md +20 -0
  4. package/.agents/skills/brainstorm/SKILL.md +22 -0
  5. package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
  6. package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
  7. package/.agents/skills/debug/SKILL.md +22 -0
  8. package/.agents/skills/discovery/SKILL.md +20 -0
  9. package/.agents/skills/dispatcher/SKILL.md +15 -0
  10. package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
  11. package/.agents/skills/ecosystem-health/SKILL.md +20 -0
  12. package/.agents/skills/eli5/SKILL.md +20 -0
  13. package/.agents/skills/eval/SKILL.md +21 -0
  14. package/.agents/skills/evolve/SKILL.md +21 -0
  15. package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
  16. package/.agents/skills/gitlab-ops/SKILL.md +20 -0
  17. package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
  18. package/.agents/skills/grill/SKILL.md +22 -0
  19. package/.agents/skills/hook-development/SKILL.md +15 -0
  20. package/.agents/skills/mcp-builder/SKILL.md +15 -0
  21. package/.agents/skills/memory-cleanup/SKILL.md +21 -0
  22. package/.agents/skills/mode-selector/SKILL.md +17 -0
  23. package/.agents/skills/npm-publish/SKILL.md +16 -0
  24. package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
  25. package/.agents/skills/persona-panel/SKILL.md +17 -0
  26. package/.agents/skills/plan/SKILL.md +20 -0
  27. package/.agents/skills/playwright-driver/SKILL.md +20 -0
  28. package/.agents/skills/quality-gates/SKILL.md +20 -0
  29. package/.agents/skills/reconcile/SKILL.md +21 -0
  30. package/.agents/skills/remote-offload/SKILL.md +20 -0
  31. package/.agents/skills/repo-audit/SKILL.md +16 -0
  32. package/.agents/skills/session-end/SKILL.md +20 -0
  33. package/.agents/skills/session-plan/SKILL.md +20 -0
  34. package/.agents/skills/session-start/SKILL.md +20 -0
  35. package/.agents/skills/spinout/SKILL.md +16 -0
  36. package/.agents/skills/sunset-review/SKILL.md +16 -0
  37. package/.agents/skills/test-runner/SKILL.md +20 -0
  38. package/.agents/skills/tmux-layout/SKILL.md +21 -0
  39. package/.agents/skills/using-orchestrator/SKILL.md +17 -0
  40. package/.agents/skills/vault-mirror/SKILL.md +15 -0
  41. package/.agents/skills/vault-sync/SKILL.md +15 -0
  42. package/.agents/skills/wave-executor/SKILL.md +20 -0
  43. package/.agents/skills/write-executable-plan/SKILL.md +22 -0
  44. package/.claude-plugin/marketplace.json +1 -1
  45. package/.claude-plugin/plugin.json +1 -1
  46. package/.codex-plugin/plugin.json +1 -1
  47. package/.cursor/commands/autopilot.md +2 -2
  48. package/.cursor/commands/bootstrap.md +1 -1
  49. package/.cursor/commands/brainstorm.md +1 -1
  50. package/.cursor/commands/debug.md +1 -1
  51. package/.cursor/commands/discovery.md +1 -1
  52. package/.cursor/commands/dispatcher.md +2 -2
  53. package/.cursor/commands/eli5.md +2 -2
  54. package/.cursor/commands/eval.md +2 -2
  55. package/.cursor/commands/evolve.md +1 -1
  56. package/.cursor/commands/go.md +1 -1
  57. package/.cursor/commands/grill.md +2 -2
  58. package/.cursor/commands/memory-cleanup.md +2 -2
  59. package/.cursor/commands/persona-panel.md +1 -1
  60. package/.cursor/commands/plan.md +1 -1
  61. package/.cursor/commands/portfolio.md +1 -1
  62. package/.cursor/commands/reconcile.md +2 -2
  63. package/.cursor/commands/release.md +2 -2
  64. package/.cursor/commands/session.md +2 -2
  65. package/.cursor/commands/spinout.md +2 -2
  66. package/.cursor/commands/sunset-review.md +2 -2
  67. package/.cursor/commands/templates-ack.md +2 -2
  68. package/.cursor/commands/test.md +2 -2
  69. package/.cursor/skills/brainstorm/SKILL.md +1 -1
  70. package/.cursor/skills/eval/SKILL.md +1 -1
  71. package/.cursor/skills/quality-gates/SKILL.md +1 -1
  72. package/.cursor/skills/remote-offload/SKILL.md +1 -1
  73. package/.orchestrator/policy/blocked-commands.json +121 -0
  74. package/.orchestrator/policy/ecosystem.schema.json +66 -0
  75. package/.orchestrator/policy/quality-gates.example.json +16 -0
  76. package/.orchestrator/policy/quality-gates.schema.json +38 -0
  77. package/.orchestrator/policy/templates-policy.json +27 -0
  78. package/.orchestrator/policy/test-profiles.json +47 -0
  79. package/AGENTS.md +225 -0
  80. package/CHANGELOG.md +1125 -2
  81. package/NOTICE +11 -6
  82. package/README.md +127 -94
  83. package/agents/eval-judge.md +1 -1
  84. package/agents/skill-applied-judge.md +1 -1
  85. package/assets/wave-lifecycle.svg +98 -0
  86. package/commands/release.md +6 -3
  87. package/commands/session.md +18 -3
  88. package/docs/README.md +4 -0
  89. package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
  90. package/docs/baseline.md +67 -0
  91. package/docs/ci-setup.md +108 -62
  92. package/docs/codex-setup.md +65 -21
  93. package/docs/components.md +36 -15
  94. package/docs/cursor-setup.md +6 -2
  95. package/docs/events-schema.md +9 -6
  96. package/docs/instruction-delivery.md +62 -0
  97. package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
  98. package/docs/migration-v4.md +341 -0
  99. package/docs/pi-setup.md +6 -1
  100. package/docs/plugin-architecture-v3.md +1 -1
  101. package/docs/rule-authoring.md +85 -19
  102. package/docs/scope-collision-guard.md +5 -5
  103. package/docs/session-config-reference.md +57 -56
  104. package/docs/session-config-template.md +6 -29
  105. package/docs/telemetry.md +157 -3
  106. package/docs/vault-docs-architecture.md +50 -11
  107. package/hooks/_lib/hook-import-set.json +1487 -0
  108. package/hooks/_lib/subagent-transcript.mjs +562 -0
  109. package/hooks/config-protection.mjs +2 -2
  110. package/hooks/cwd-change-restore.mjs +2 -2
  111. package/hooks/enforce-commands.mjs +69 -0
  112. package/hooks/hooks-codex.json +1 -1
  113. package/hooks/hooks-cursor.json +10 -0
  114. package/hooks/hooks-pi.json +5 -0
  115. package/hooks/hooks.json +6 -1
  116. package/hooks/loop-guard.mjs +3 -3
  117. package/hooks/on-session-end.mjs +2 -2
  118. package/hooks/on-session-start.mjs +103 -2
  119. package/hooks/on-stop.mjs +36 -11
  120. package/hooks/operator-steer.mjs +2 -2
  121. package/hooks/post-bash-write-verify.mjs +85 -0
  122. package/hooks/post-edit-import-probe.mjs +344 -0
  123. package/hooks/post-subagent-discovery-validator.mjs +187 -431
  124. package/hooks/post-tool-batch-wave-signal.mjs +118 -4
  125. package/hooks/post-tool-failure-corrective-context.mjs +2 -2
  126. package/hooks/post-tooluse-frontend-slop.mjs +3 -3
  127. package/hooks/pre-bash-destructive-guard.mjs +39 -13
  128. package/hooks/skill-invocation-telemetry.mjs +17 -5
  129. package/hooks/subagent-telemetry.mjs +13 -4
  130. package/monitors/monitors.json +3 -3
  131. package/package.json +9 -1
  132. package/pi/prompts/session.md +2 -2
  133. package/plugin.json +27 -0
  134. package/scripts/backfill-abandoned-sessions.mjs +50 -4
  135. package/scripts/backfill-learnings-from-vault.mjs +9 -3
  136. package/scripts/dialectic-deriver.mjs +73 -8
  137. package/scripts/export-hw-learnings.mjs +113 -1
  138. package/scripts/generate-agents-skills.mjs +378 -0
  139. package/scripts/generate-cursor-adapter.mjs +45 -8
  140. package/scripts/generate-hook-import-set.mjs +249 -0
  141. package/scripts/lib/agent-status.mjs +13 -2
  142. package/scripts/lib/auto-dream.mjs +38 -36
  143. package/scripts/lib/autonomy/suitability.mjs +6 -0
  144. package/scripts/lib/autopilot/loop.mjs +2 -2
  145. package/scripts/lib/ci-status-banner.mjs +220 -75
  146. package/scripts/lib/codex/plugin-contract.mjs +82 -6
  147. package/scripts/lib/config/auto-dream.mjs +2 -1
  148. package/scripts/lib/config/block-header.mjs +8 -0
  149. package/scripts/lib/config/block-preprocess.mjs +177 -0
  150. package/scripts/lib/config/broken-window.mjs +2 -1
  151. package/scripts/lib/config/cold-start.mjs +2 -1
  152. package/scripts/lib/config/config-protection.mjs +22 -2
  153. package/scripts/lib/config/context-coverage.mjs +2 -1
  154. package/scripts/lib/config/cross-repo.mjs +2 -1
  155. package/scripts/lib/config/custom-phases.mjs +2 -1
  156. package/scripts/lib/config/dialectic.mjs +2 -1
  157. package/scripts/lib/config/discovery-validator.mjs +2 -1
  158. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
  159. package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
  160. package/scripts/lib/config/docs-orchestrator.mjs +2 -1
  161. package/scripts/lib/config/docs-staleness.mjs +2 -1
  162. package/scripts/lib/config/drift-check.mjs +2 -1
  163. package/scripts/lib/config/eval.mjs +2 -1
  164. package/scripts/lib/config/events-rotation.mjs +2 -1
  165. package/scripts/lib/config/evolve.mjs +8 -2
  166. package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
  167. package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
  168. package/scripts/lib/config/handover-gate.mjs +2 -1
  169. package/scripts/lib/config/health-endpoints.mjs +7 -2
  170. package/scripts/lib/config/issue-budget.mjs +2 -1
  171. package/scripts/lib/config/loop-guard.mjs +2 -1
  172. package/scripts/lib/config/memory.mjs +2 -1
  173. package/scripts/lib/config/moc-staleness.mjs +2 -1
  174. package/scripts/lib/config/persona-gate-wave.mjs +2 -1
  175. package/scripts/lib/config/private-config-dir.mjs +67 -0
  176. package/scripts/lib/config/reconcile.mjs +2 -1
  177. package/scripts/lib/config/remote-hosts.mjs +2 -1
  178. package/scripts/lib/config/section-extractor.mjs +7 -1
  179. package/scripts/lib/config/skill-evolution.mjs +2 -1
  180. package/scripts/lib/config/slopcheck.mjs +2 -1
  181. package/scripts/lib/config/state-md-lock.mjs +2 -1
  182. package/scripts/lib/config/templates-first.mjs +2 -1
  183. package/scripts/lib/config/test.mjs +2 -1
  184. package/scripts/lib/config/vault-integration.mjs +7 -1
  185. package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
  186. package/scripts/lib/config/vault-staleness.mjs +2 -1
  187. package/scripts/lib/config/vault-sync.mjs +2 -1
  188. package/scripts/lib/config/verification-auto-fix.mjs +2 -1
  189. package/scripts/lib/config/wave-reviewers.mjs +2 -1
  190. package/scripts/lib/config/worktree-orphans.mjs +2 -1
  191. package/scripts/lib/convergence-monitor.mjs +82 -16
  192. package/scripts/lib/dispatcher/rank.mjs +124 -48
  193. package/scripts/lib/ecosystem-health.mjs +16 -2
  194. package/scripts/lib/eval/engine.mjs +9 -1
  195. package/scripts/lib/eval/session-resolve.mjs +23 -4
  196. package/scripts/lib/events.mjs +22 -6
  197. package/scripts/lib/frontmatter-guard.mjs +131 -13
  198. package/scripts/lib/gates/gate-full.mjs +26 -0
  199. package/scripts/lib/gates/gate-helpers.mjs +76 -0
  200. package/scripts/lib/hardware-pattern-detector.mjs +18 -1
  201. package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
  202. package/scripts/lib/host-identity.mjs +50 -11
  203. package/scripts/lib/instruction-budget-guard.mjs +171 -5
  204. package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
  205. package/scripts/lib/learnings/io.mjs +60 -6
  206. package/scripts/lib/memory-proposals/store.mjs +30 -22
  207. package/scripts/lib/owner-config-banner.mjs +43 -6
  208. package/scripts/lib/owner-config-loader.mjs +21 -10
  209. package/scripts/lib/owner-interview.mjs +3 -3
  210. package/scripts/lib/owner-yaml.mjs +207 -14
  211. package/scripts/lib/platform.mjs +108 -15
  212. package/scripts/lib/plugin-update-banner.mjs +406 -0
  213. package/scripts/lib/project-hygiene.mjs +38 -2
  214. package/scripts/lib/qg-command-drift-banner.mjs +50 -12
  215. package/scripts/lib/quality-gate.mjs +133 -44
  216. package/scripts/lib/reconcile/emitter.mjs +68 -6
  217. package/scripts/lib/reconcile/engine.mjs +13 -4
  218. package/scripts/lib/reconcile/idempotency.mjs +37 -4
  219. package/scripts/lib/reconcile/writer.mjs +40 -18
  220. package/scripts/lib/session-close-backfill.mjs +67 -9
  221. package/scripts/lib/session-id.mjs +12 -23
  222. package/scripts/lib/session-identity/own-session.mjs +125 -10
  223. package/scripts/lib/session-lock-shape.mjs +43 -0
  224. package/scripts/lib/session-lock.mjs +5 -10
  225. package/scripts/lib/session-registry.mjs +25 -9
  226. package/scripts/lib/session-schema/constants.mjs +36 -2
  227. package/scripts/lib/session-schema/validator.mjs +38 -4
  228. package/scripts/lib/session-start-probes.mjs +18 -1
  229. package/scripts/lib/sessions-staleness-banner.mjs +18 -11
  230. package/scripts/lib/skill-health/join.mjs +17 -4
  231. package/scripts/lib/state-md.mjs +78 -0
  232. package/scripts/lib/sunset/walker.mjs +6 -0
  233. package/scripts/lib/telemetry/schema.mjs +181 -9
  234. package/scripts/lib/telemetry/sync.mjs +368 -12
  235. package/scripts/lib/validate/check-agents-skills.mjs +327 -0
  236. package/scripts/lib/validate/check-agents.mjs +3 -3
  237. package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
  238. package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
  239. package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
  240. package/scripts/lib/validate/check-skill-links.mjs +163 -0
  241. package/scripts/lib/validate/check-skill-script-paths.mjs +47 -28
  242. package/scripts/lib/validate/check-unwired-features.mjs +0 -2
  243. package/scripts/lib/validate/check-validator-registration.mjs +10 -4
  244. package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
  245. package/scripts/lib/vault-backfill/template.mjs +63 -6
  246. package/scripts/lib/vault-mirror/process.mjs +165 -42
  247. package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
  248. package/scripts/lib/vault-status/narrative-mirror.mjs +127 -18
  249. package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
  250. package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
  251. package/scripts/lib/wave-executor/remote-dispatch.mjs +5 -7
  252. package/scripts/lib/wave-resource-gate.mjs +8 -2
  253. package/scripts/lib/wave-sizing.mjs +4 -1
  254. package/scripts/lib/wave-transcript-tail.mjs +118 -4
  255. package/scripts/materialize-wave-scope.mjs +12 -5
  256. package/scripts/memory-propose.mjs +19 -5
  257. package/scripts/migrate-cold-start-seed.mjs +4 -1
  258. package/scripts/parse-config.mjs +60 -3
  259. package/scripts/release.mjs +337 -29
  260. package/scripts/repair-invalid-sessions.mjs +3 -3
  261. package/scripts/run-quality-gate.mjs +128 -11
  262. package/scripts/sweep-expired-learnings.mjs +90 -0
  263. package/scripts/sync-vault-schema.mjs +3 -1
  264. package/scripts/telemetry.mjs +2 -2
  265. package/scripts/validate-plugin.mjs +161 -0
  266. package/scripts/validate-wave-scope.mjs +28 -8
  267. package/scripts/wave-scope-binding.mjs +215 -0
  268. package/skills/_shared/instruction-file-resolution.md +10 -0
  269. package/skills/_shared/parallel-aware-preamble.md +1 -0
  270. package/skills/_shared/platform-tools.md +1 -1
  271. package/skills/_shared/state-ownership.md +1 -1
  272. package/skills/architecture/SKILL.md +7 -5
  273. package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
  274. package/skills/autopilot/SKILL.md +4 -18
  275. package/skills/claude-md-drift-check/SKILL.md +5 -1
  276. package/skills/claude-md-drift-check/checker.mjs +62 -2
  277. package/skills/convergence-monitoring/SIGNALS.md +55 -0
  278. package/skills/discovery/probes/vault-staleness.mjs +37 -13
  279. package/skills/discovery/probes-arch.md +20 -18
  280. package/skills/dispatcher/SKILL.md +3 -2
  281. package/skills/evolve/SKILL.md +65 -26
  282. package/skills/frontmatter-guard/SKILL.md +11 -5
  283. package/skills/npm-publish/SKILL.md +1 -1
  284. package/skills/reconcile/SKILL.md +33 -0
  285. package/skills/remote-offload/SKILL.md +1 -1
  286. package/skills/session-end/SKILL.md +18 -905
  287. package/skills/session-end/phase-3-6-tail.md +10 -3
  288. package/skills/session-end/plan-verification.md +221 -155
  289. package/skills/session-end/references/phase-2-quality-gate.md +93 -0
  290. package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
  291. package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
  292. package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
  293. package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
  294. package/skills/session-end/references/session-summary-template.md +62 -0
  295. package/skills/session-plan/SKILL.md +49 -0
  296. package/skills/session-start/SKILL.md +22 -904
  297. package/skills/session-start/phase-8-5-express-path.md +1 -1
  298. package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
  299. package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
  300. package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
  301. package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
  302. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
  303. package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
  304. package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
  305. package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
  306. package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
  307. package/skills/vault-sync/validator.mjs +21 -27
  308. package/skills/wave-executor/SKILL.md +15 -1
  309. package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
  310. package/skills/wave-executor/references/wave-loop-review.md +570 -0
  311. package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
  312. package/skills/wave-executor/wave-loop.md +14 -1309
  313. package/templates/_shared/journey-manifest.md +10 -6
  314. package/.cursor/commands/autopilot-multi.md +0 -14
  315. package/.cursor/commands/contract-version-bump.md +0 -14
  316. package/.cursor/commands/journey-audit.md +0 -14
  317. package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
  318. package/.cursor/skills/daily/SKILL.md +0 -12
  319. package/.cursor/skills/domain-model/SKILL.md +0 -13
  320. package/.cursor/skills/journey-audit/SKILL.md +0 -13
  321. package/.cursor/skills/skill-creator/SKILL.md +0 -13
  322. package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
  323. package/commands/autopilot-multi.md +0 -74
  324. package/commands/contract-version-bump.md +0 -28
  325. package/commands/journey-audit.md +0 -43
  326. package/pi/prompts/autopilot-multi.md +0 -12
  327. package/pi/prompts/contract-version-bump.md +0 -12
  328. package/pi/prompts/journey-audit.md +0 -12
  329. package/scripts/autopilot-multi.mjs +0 -885
  330. package/scripts/backfill-learnings-expires.mjs +0 -196
  331. package/scripts/backfill-learnings.mjs +0 -203
  332. package/scripts/fleet-instruction-scan.mjs +0 -141
  333. package/scripts/lib/autopilot/dep-graph.mjs +0 -417
  334. package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
  335. package/scripts/lib/webhook-url.mjs +0 -105
  336. package/scripts/lifecycle-sim-v6.mjs +0 -347
  337. package/scripts/migrate-learnings-jsonl.mjs +0 -189
  338. package/scripts/migrate-subagents-jsonl.mjs +0 -196
  339. package/scripts/upload-social-preview.mjs +0 -316
  340. package/skills/_shared/model-selection.md +0 -64
  341. package/skills/contract-version-bump/SKILL.md +0 -219
  342. package/skills/daily/SKILL.md +0 -222
  343. package/skills/daily/generate.sh +0 -92
  344. package/skills/daily/templates/daily.md.tpl +0 -36
  345. package/skills/journey-audit/SKILL.md +0 -270
  346. package/skills/skill-creator/SKILL.md +0 -168
  347. package/skills/ubiquitous-language/SKILL.md +0 -97
  348. package/skills/vault-sync/package-lock.json +0 -40
  349. /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
  350. /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
@@ -0,0 +1,562 @@
1
+ /**
2
+ * subagent-transcript.mjs — the PSA-006 claim-extraction engine (#1218/#1198).
3
+ *
4
+ * Extracted out of `hooks/post-subagent-discovery-validator.mjs`, which kept
5
+ * the whole scanner inline behind a `process.stdin` read: the only way to
6
+ * exercise a claim string was to spawn the hook with a sandbox repo, a
7
+ * CLAUDE.md (or AGENTS.md — transparent aliases, see skills/_shared/instruction-file-resolution.md)
8
+ * and a transcript on disk. That cost made the precision of the
9
+ * matcher — the property the fleet actually complained about — effectively
10
+ * unmeasurable, so it was never measured. This module is the pure half
11
+ * (string in, findings out); the hook keeps stdin, config gate, sidecar
12
+ * resolution, sentinels and the three output channels.
13
+ *
14
+ * Three responsibilities, in the order the hook uses them:
15
+ * 1. `readTranscriptTail()` — the last N assistant records of ONE transcript
16
+ * JSONL, text blocks concatenated.
17
+ * 2. `findViolations()` — distributional/repo-state claims that carry no
18
+ * adjacent measurement evidence.
19
+ * 3. `normalizeClaim()` / `dedupeViolations()` — one record per distinct
20
+ * claim, with an `occurrences` count (#1198: the fleet's worst repo held
21
+ * 3,360 records over 205 distinct `claim_text` values = duplication
22
+ * factor 16.4, measured 2026-09-06 over
23
+ * `extern/aiat-barrierefrei-engine/.orchestrator/metrics/events.jsonl`).
24
+ *
25
+ * @module hooks/_lib/subagent-transcript
26
+ */
27
+
28
+ import { promises as fs } from 'node:fs';
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Constants
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /** Number of trailing assistant records to scan. */
35
+ export const TAIL_RECORDS = 8;
36
+ /** Proximity window (in lines) for an adjacent grep transcript. */
37
+ export const GREP_PROXIMITY_LINES = 5;
38
+ /** Max characters of claim text persisted to the event record. */
39
+ export const CLAIM_TEXT_MAX = 200;
40
+
41
+ /**
42
+ * Code-distribution context nouns (singular or plural). A distributional claim
43
+ * is only a PSA-006 *code* claim when one of these appears near the trigger —
44
+ * this is what separates "4 of 4 callers" (a real claim) from "Turn 3 of 25"
45
+ * (a turn counter) and "every caller imports X" from "every developer should
46
+ * test" (generic advice). Tightening per W2-review LOW finding (#567).
47
+ *
48
+ * Single optional `\s?` only (call site / callsite) — no nested quantifiers,
49
+ * so the alternation stays linear-time / ReDoS-safe.
50
+ */
51
+ const CTX = '(?:call\\s?sites?|callers?|sites?|references?|instances?|files?|consumers?|imports?|matches|match|occurrences?|usages?|modules?|tests?|places?|functions?|dependenc(?:y|ies)|endpoints?|hooks?)';
52
+
53
+ /**
54
+ * Repo-STATE nouns (#908). The four documented #908 drift cases were counts of
55
+ * repository state, not of code locations: "14 commits", "92 learnings",
56
+ * "5 dirty files", "412 lines". None of them contains a CTX noun, so the
57
+ * original six patterns could not see them.
58
+ *
59
+ * Vocabulary taken from this repo's own artefacts (`.orchestrator/metrics/*.jsonl`
60
+ * record kinds, `.claude/rules/`, `skills/`, `agents/`, `hooks/`) rather than a
61
+ * generic English list — a noun that never names a countable repo artefact here
62
+ * only buys false positives.
63
+ */
64
+ const STATE = '(?:commits?|learnings?|issues?|branches?|lines?|entries|records?|sessions?|rules?|skills?|probes?|waves?|proposals?|worktrees?)';
65
+
66
+ /**
67
+ * The noun class the BARE-CARDINAL pattern may use — deliberately a strict
68
+ * subset of STATE ∪ CTX, restricted to the artefact kinds the #908 drift was
69
+ * actually measured in (commit counts, learnings counts, open-issue counts,
70
+ * branch counts, line counts, dirty-file counts).
71
+ *
72
+ * Measured, not guessed: over 32 real agent-stop windows from this repo's own
73
+ * transcripts, admitting the full CTX ∪ STATE set fired 93 times (2.9 per
74
+ * stop — the "validator gets switched off" zone). Every noun below earns its
75
+ * place by naming one of the documented #908 facts; the ones that only cost
76
+ * false positives (`tests`, `references`, `matches`, `agents`, `files` without
77
+ * a state adjective) are excluded here and remain reachable through the six
78
+ * quantifier-triggered patterns above, which have a lexical anchor.
79
+ *
80
+ * `callers` (#918): PSA-006's own canonical noun — the rule text's worked
81
+ * examples ("4 of 4 callers", "100% of callers opt-in") all count callers, yet
82
+ * the cardinal pattern could not see a bare "14 callers". Re-measured with it
83
+ * admitted: 490 real SubagentStop transcripts (2026-07-31), +0 additional
84
+ * firings from the bare form — the noun is free on this corpus (all +3 delta
85
+ * firings came from the #918 slash pattern below).
86
+ *
87
+ * German extension (#1211): `zeilen?|dateien?|datei|aufrufer|eintr(?:ag|(?:ä|ae)ge)`
88
+ * only — the same D3 measurement that found the quantifier patterns
89
+ * disciplined (7/41, see WIDE_DE below) found the WIDE German noun set
90
+ * (Sessions/Repos/Treffer/Stellen/Module/Tests included) firing 39/41
91
+ * (~57% false-positive rate in the labelled sample) when admitted here.
92
+ */
93
+ const CARDINAL_NOUN =
94
+ '(?:commits?|learnings?|issues?|branches?|lines?|files?|callers?|zeilen?|dateien?|datei|aufrufer|eintr(?:ag|(?:ä|ae)ge))';
95
+
96
+ /**
97
+ * Wide noun class = code-distribution nouns ∪ repo-state nouns. Used by the six
98
+ * QUANTIFIER-triggered patterns ("N of M", "100% of", "all N", "no remaining",
99
+ * "none of") — each of those carries a strong lexical trigger, so widening the
100
+ * noun set there is low-risk.
101
+ *
102
+ * Deliberately NOT used by the `every <noun>` pattern: `every` has no numeric
103
+ * anchor, so `every commit must be signed` / `every rule is always-on` are
104
+ * ordinary prose, not measured claims. That pattern keeps the narrow CTX.
105
+ */
106
+ const WIDE = `(?:${CTX}|${STATE})`;
107
+
108
+ /**
109
+ * German counterparts of CTX/STATE/WIDE (#1211). D3 (Discovery, 2026-09-03)
110
+ * measured these against 41 real German claim lines pulled from this repo's
111
+ * own `.orchestrator/metrics/events.jsonl`: the quantifier-triggered patterns
112
+ * built from WIDE_DE below flagged 7/41 — the same "disciplined" order of
113
+ * magnitude as the English CLAIM_PATTERNS.
114
+ */
115
+ const CTX_DE =
116
+ '(?:Aufrufer|Stellen?|Module?|Tests?|Treffer|Zeilen?|Aufrufstellen?|Referenzen?|Instanzen?|Konsumenten?|Verweise?)';
117
+ const STATE_DE =
118
+ '(?:Commits?|Learnings?|Issues?|Branches?|Zeilen?|Eintr(?:ag|(?:ä|ae)ge)|Sessions?|Repos?|Dateien?|Datei|Waves?|Wellen?)';
119
+ const WIDE_DE = `(?:${CTX_DE}|${STATE_DE})`;
120
+
121
+ /** Bounded same-line gap between a trigger and its context noun. */
122
+ const CTX_GAP = '[^\\n]{0,40}?';
123
+
124
+ /**
125
+ * Distributional-claim patterns (case-insensitive). A match is a PSA-006 claim
126
+ * that requires an adjacent measurement transcript.
127
+ *
128
+ * Each pattern requires a context noun within a small, bounded same-line window
129
+ * of the trigger. The `[^\n]{0,40}?` gap is a bounded lazy character class
130
+ * (linear-time — the ReDoS-safety the W2 reviewer verified is preserved).
131
+ */
132
+ export const CLAIM_PATTERNS = [
133
+ new RegExp(`\\b\\d+ of \\d+\\b${CTX_GAP}\\b${WIDE}\\b`, 'i'),
134
+ new RegExp(`100% of\\b${CTX_GAP}\\b${WIDE}\\b`, 'i'),
135
+ new RegExp(`\\ball \\d+\\b${CTX_GAP}\\b${WIDE}\\b`, 'i'),
136
+ new RegExp(`no remaining\\b${CTX_GAP}\\b${WIDE}\\b`, 'i'),
137
+ new RegExp(`every ${CTX}\\b`, 'i'),
138
+ new RegExp(`none of\\b${CTX_GAP}\\b${WIDE}\\b`, 'i'),
139
+
140
+ // German equivalents (#1211). Each mirrors one of the six English patterns
141
+ // above, plus three shapes with no direct English counterpart in this list
142
+ // ("davon" subset-claims, "sämtliche", "kein einziger").
143
+ new RegExp(`\\b\\d+\\s+von\\s+\\d+\\b${CTX_GAP}\\b${WIDE_DE}\\b`, 'i'), // "N von M <noun>"
144
+ new RegExp(`\\b100\\s?%\\s*(?:von|der|aller)\\b${CTX_GAP}\\b${WIDE_DE}\\b`, 'i'), // "100 % von/der/aller <noun>"
145
+ new RegExp(`\\balle\\s+\\d+\\b${CTX_GAP}\\b${WIDE_DE}\\b`, 'i'), // "alle N <noun>"
146
+ new RegExp(`\\bkeine\\s+verbleibenden\\b${CTX_GAP}\\b${WIDE_DE}\\b`, 'i'), // "keine verbleibenden <noun>"
147
+ new RegExp(`\\bjed(?:er|e|es)\\s+${CTX_DE}\\b`, 'i'), // "jeder/jede/jedes <CTX_DE>"
148
+ new RegExp(`\\bkein(?:er|e|es)\\s+(?:von|der)\\b${CTX_GAP}\\b${WIDE_DE}\\b`, 'i'), // "keiner/keine/keins von/der <noun>"
149
+ // "N <noun> … davon N" (#1198 anchor).
150
+ new RegExp(`\\b\\d+\\s+${WIDE_DE}\\b${CTX_GAP}\\bdavon\\s+\\d+\\b`, 'i'),
151
+ new RegExp(`\\bs(?:ä|ae)mtliche\\s+${CTX_DE}\\b`, 'i'), // "sämtliche/saemtliche <CTX_DE>"
152
+ new RegExp(`\\bkein\\w{0,2}\\s+einzige[rs]?\\b${CTX_GAP}\\b${WIDE_DE}\\b`, 'i'), // "kein(e)? einzige[rs]? <noun>"
153
+ ];
154
+
155
+ /**
156
+ * Pattern 7 (#908) — the BARE CARDINAL repo-state fact. `14 commits` has no
157
+ * quantifier trigger at all, which is exactly why the #908 drift went unseen.
158
+ *
159
+ * Precision is bought three ways: a TRIGGER whose lookbehind/lookahead reject
160
+ * identifier punctuation (`#906`, `v3`, `3.17`, `70%`, `2026-07-29`), a GAP of
161
+ * at most two adjective-like words with no preposition/article/copula, and a
162
+ * SCOPE restricted to prose lines (see `NON_PROSE_LINE_RE` and `scanFences`).
163
+ *
164
+ * All quantifiers are bounded ({1,9}, {0,2}) — linear-time, ReDoS-safe.
165
+ */
166
+ const CARDINAL_TRIGGER = '(?<![\\w#$:/.-])\\d{1,9}(?![\\d.%:/-])';
167
+ const CARDINAL_STOPWORDS =
168
+ 'of|in|on|at|for|to|the|a|an|and|or|is|are|was|were|from|with|by|that|than|per|out|over|into|onto|via|but|as';
169
+ const CARDINAL_GAP = `(?:\\s+(?!(?:${CARDINAL_STOPWORDS})\\b)[A-Za-z][\\w-]*){0,2}`;
170
+ export const CARDINAL_PATTERN = new RegExp(`${CARDINAL_TRIGGER}${CARDINAL_GAP}\\s+${CARDINAL_NOUN}\\b`, 'i');
171
+
172
+ /**
173
+ * Pattern 8 (#918) — the PSA-006 CANONICAL numerator/denominator slash form:
174
+ * "12/14 files", "4/4 callers". Stricter contract than the bare cardinal:
175
+ * exactly two slash-joined numbers, and the noun must follow IMMEDIATELY (no
176
+ * gap), which is what separates a measured ratio from a date or a score
177
+ * ("on 12/14 we shipped", "rated 3/5 overall").
178
+ */
179
+ export const CARDINAL_RATIO_PATTERN = new RegExp(
180
+ `(?<![\\w#$:/.-])\\d{1,9}/\\d{1,9}(?![\\d.%:/-])\\s+${CARDINAL_NOUN}\\b`,
181
+ 'i'
182
+ );
183
+
184
+ /** Inline-code spans are masked before any pattern runs. */
185
+ const INLINE_CODE_RE = /`[^`\n]*`/g;
186
+
187
+ /**
188
+ * Gate-summary / STATUS-report lines are the HARNESS's own completion output —
189
+ * tool evidence, not an unverified assertion about the codebase. GitLab #1198
190
+ * (Discovery D8, 2026-09-02): a 400-event sample showed 186/400 (46.5%) firing
191
+ * on this exact class — the single largest false-positive source measured to
192
+ * date. Skipped at the LINE level, before ANY pattern runs.
193
+ *
194
+ * Trade named, not fixed: matching anywhere on the line silences the WHOLE
195
+ * line, so a distributional claim sharing a line with a `Gate N/M` mention goes
196
+ * unflagged too — accepted because a gate-summary line is harness evidence.
197
+ */
198
+ const GATE_SUMMARY_LINE_RE =
199
+ /\b\d+\s+passed\s*\/\s*\d+\s+failed\b|^\s*STATUS:\s*(?:done|partial|failed)\b|\bFull Gate\b|\bGate:\s*(?:typecheck|grün|gruen|rot)\b|\bGate \d[\d.]*\/\d+\b|\b\d+\s+Wellen?,\s*\d+\s+Agents?\b|\bArbeitsbaum leer\b|\bmit Nachweis geschlossen\b/i;
200
+
201
+ /**
202
+ * NON-PROSE structural lines (#1218 negative-context guard). A PSA-006 claim is
203
+ * an ASSERTION in prose; these four shapes are not prose at all, and every one
204
+ * of them was measured as a live false-positive class.
205
+ *
206
+ * Measured 2026-09-06 over the 205 DISTINCT `claim_text` values in
207
+ * `~/Projects/extern/aiat-barrierefrei-engine/.orchestrator/metrics/events.jsonl`
208
+ * (the fleet's worst-affected repo; command in the module test header):
209
+ * 201/205 re-fire on the pre-#1218 matcher, of which 35 are markdown table
210
+ * rows and 9 are ATX headings — 44 firings, 21.9% of the corpus, with a true
211
+ * positive rate of zero in the independently sampled 60 (2026-09-02).
212
+ *
213
+ * 1. TABLE ROW — `| Lane B | 3 Commits, Baum sauber |`. A status matrix
214
+ * cell is structured data; the number in it is a field value.
215
+ * 2. TABLE SEPARATOR — `|---|---:|`.
216
+ * 3. ATX HEADING — `## Welle 1 abgeschlossen — 10/10 Lanes, 15 Commits`. A
217
+ * heading LABELS a section; the claim, if there is one, is restated in
218
+ * the body below it where it is still caught.
219
+ * NAMED CEILING (BV-004): a claim that appears ONLY in a heading and
220
+ * never in the body escapes. Accepted because the class measured zero
221
+ * true positives; REVISIT TRIGGER: a labelled sample in which a
222
+ * heading-only claim is a genuine PSA-006 violation.
223
+ * 4. PLAN / INTENT item — a task-list checkbox, or a line whose FIRST token
224
+ * declares intent rather than a finding (`TODO`, `Next`, `Geplant`,
225
+ * `Empfehlung`, `Follow-up`, …). An intent is a statement about the
226
+ * future; PSA-006 governs assertions about the present.
227
+ *
228
+ * All four are anchored at line START (after at most one bullet marker), so a
229
+ * mid-sentence "TODO" or a pipe character inside prose cannot silence a line.
230
+ */
231
+ const TABLE_ROW_RE = /^\s{0,3}\|.*\|\s*$/;
232
+ const TABLE_SEPARATOR_RE = /^\s{0,3}\|?[\s:|-]*-{2,}[\s:|-]*\|?\s*$/;
233
+ const HEADING_RE = /^\s{0,3}#{1,6}\s+\S/;
234
+ const PLAN_INTENT_RE =
235
+ /^\s*(?:[-*+]\s+)?(?:\[[ xX]\]|TODO\b|FIXME\b|Plan\b|Planned\b|Next\b|Geplant\b|N(?:ä|ae)chste[rs]?\b|Vorschlag\b|Empfehlung\b|Recommendation\b|Follow-?ups?\b|Ziel\b|Aufgabe\b)/i;
236
+
237
+ const NON_PROSE_LINE_RE = new RegExp(
238
+ `(${TABLE_ROW_RE.source})|(${TABLE_SEPARATOR_RE.source})|(${HEADING_RE.source})|(${PLAN_INTENT_RE.source})`,
239
+ 'i'
240
+ );
241
+
242
+ /**
243
+ * CONFIG / SCORE / VERSION literal line (#1218). A `key: <scalar>` line is a
244
+ * configuration or JSON fragment, not an assertion — the number is a SETTING.
245
+ *
246
+ * The key must be all-lowercase kebab/snake (`agents-per-wave`, `"session_id"`,
247
+ * `enabled`) — which is exactly what distinguishes a YAML/JSON key from a
248
+ * German or English prose label. `Gemessen: 4 von 4 Aufrufern` keeps its
249
+ * capital G and stays a claim; `quota-per-wave: 5` does not.
250
+ *
251
+ * Version literals (`v0.15.0`, `0.18.0`) need no rule of their own: the
252
+ * cardinal trigger's `(?![\d.%:/-])` lookahead already rejects a digit followed
253
+ * by a dot. Score ratios (`PAC bei 23/29`, `rated 3/5 overall`) are likewise
254
+ * already excluded — CARDINAL_RATIO_PATTERN demands an artefact noun
255
+ * immediately after the denominator. This rule closes the remaining third of
256
+ * the class: the config-block line.
257
+ *
258
+ * Suppresses ONLY the cardinal/ratio branch, never the quantifier patterns: a
259
+ * genuine "all 4 callers" inside a config-shaped line is still a claim.
260
+ */
261
+ const CONFIG_KEY_LINE_RE =
262
+ /^\s*(?:[-*+]\s+)?["']?[a-z_][a-z0-9_.-]*["']?\s*:\s*(?:["'[{]|-?\d|true\b|false\b|null\b|on\b|off\b|~\s*$)/;
263
+
264
+ /**
265
+ * A YAML/JSON BLOCK line that is not itself a `key: value` pair — a bare list
266
+ * item or a closing brace/bracket inside an object literal. Only recognised
267
+ * when the surrounding lines are config lines (see `scanConfigBlocks`), so a
268
+ * prose bullet is never mistaken for one.
269
+ */
270
+ const CONFIG_CONTINUATION_RE = /^\s*(?:[-*]\s+\S|[}\]],?\s*$)/;
271
+
272
+ /**
273
+ * Commands that count as a MEASUREMENT inside a fenced or inline-code block.
274
+ * `grep|rg|find` (the #567 set) only covers text search; the #908 facts are
275
+ * measured with `git log --oneline | wc -l`, `jq` over a JSONL metrics file,
276
+ * `ls | wc -l`, or a `node`/`npm` script.
277
+ */
278
+ const MEASUREMENT_CMD_RE = /\b(grep|rg|find|git|wc|jq|ls|node|npm)\b/;
279
+
280
+ /**
281
+ * GERMAN PROSE evidence (#1218). Until now a measurement only counted when it
282
+ * appeared as a COMMAND — fenced or in backticks — so the German evidence
283
+ * forms this repo's own rules prescribe (`Gemessen 2026-09-06 …`,
284
+ * `Verifizierte Zahlen … HEAD <sha>`) were read as unverified prose and the
285
+ * honest author was flagged. PSA-006 item 4 asks for the measurement TIME; a
286
+ * German marker that carries one is evidence by the rule's own definition.
287
+ *
288
+ * The marker alone grants nothing: it must be followed within 80 characters by
289
+ * an ISO date, `HEAD`, or a commit SHA. `Gemessen wurde nichts` therefore does
290
+ * not buy an exemption, while `Gemessen 2026-09-06 @ e4674109: 4 von 4` does.
291
+ * Bounded quantifier ({0,80}) — linear-time, ReDoS-safe.
292
+ */
293
+ const GERMAN_EVIDENCE_RE =
294
+ /\b(?:gemessen|nachgemessen|nachgez(?:ä|ae)hlt|ausgez(?:ä|ae)hlt|verifizierte\s+zahlen|verifiziert\s+(?:gegen|an|auf))\b[^\n]{0,80}?(?:\b\d{4}-\d{2}-\d{2}\b|\bHEAD\b|\b[0-9a-f]{7,40}\b)/i;
295
+
296
+ /**
297
+ * Markers that date a measurement (#908 Baustein 2 input). ADVISORY: an
298
+ * undated-but-verified claim is counted and reported, never a violation.
299
+ * German markers (#1218) sit alongside the English ones for the same reason
300
+ * the evidence rule above admits them.
301
+ */
302
+ const TIMESTAMP_MARKER_RE =
303
+ /\b\d{4}-\d{2}-\d{2}\b|\bHEAD\b|\bas of\b|\bmeasured (?:at|on)\b|\brev-parse\b|\bgemessen (?:am|an|auf|gegen)\b|\bStand vom\b/i;
304
+
305
+ /** Leading list-bullet / heading / blockquote markers stripped by `normalizeClaim`. */
306
+ const LEADING_MARKER_RE = /^(?:\s*(?:[-*+•]|\d{1,3}[.)]|#{1,6}|>)\s+)+/;
307
+
308
+ // ---------------------------------------------------------------------------
309
+ // transcript reading
310
+ // ---------------------------------------------------------------------------
311
+
312
+ /**
313
+ * Read the transcript JSONL at `transcriptPath`, take the last `TAIL_RECORDS`
314
+ * `type:"assistant"` records, and concatenate their text content blocks.
315
+ * Returns '' on any failure (missing file, /dev/null, malformed lines).
316
+ *
317
+ * @param {string} transcriptPath
318
+ * @returns {Promise<string>}
319
+ */
320
+ export async function readTranscriptTail(transcriptPath) {
321
+ if (typeof transcriptPath !== 'string' || !transcriptPath) return '';
322
+ let raw;
323
+ try {
324
+ raw = await fs.readFile(transcriptPath, 'utf8');
325
+ } catch {
326
+ return '';
327
+ }
328
+ if (!raw.trim()) return '';
329
+
330
+ const assistantRecords = [];
331
+ for (const line of raw.split(/\r?\n/)) {
332
+ const trimmed = line.trim();
333
+ if (!trimmed) continue;
334
+ let rec;
335
+ try { rec = JSON.parse(trimmed); } catch { continue; }
336
+ if (rec && rec.type === 'assistant') assistantRecords.push(rec);
337
+ }
338
+
339
+ const tail = assistantRecords.slice(-TAIL_RECORDS);
340
+ const textBlocks = [];
341
+ for (const rec of tail) {
342
+ const content = rec?.message?.content;
343
+ if (!Array.isArray(content)) continue;
344
+ for (const block of content) {
345
+ if (block && block.type === 'text' && typeof block.text === 'string') {
346
+ textBlocks.push(block.text);
347
+ }
348
+ }
349
+ }
350
+ return textBlocks.join('\n');
351
+ }
352
+
353
+ // ---------------------------------------------------------------------------
354
+ // claim scanning
355
+ // ---------------------------------------------------------------------------
356
+
357
+ /**
358
+ * Single fence walk. Returns two disjoint-purpose index sets:
359
+ * - `measurementLines`: lines belonging to a fenced block whose body contains
360
+ * a MEASUREMENT_CMD_RE invocation (the evidence a claim can lean on), plus
361
+ * lines carrying an inline-code measurement or a German prose measurement.
362
+ * - `fencedLines`: lines belonging to ANY fenced block (evidence or not).
363
+ * Used only to keep the greedy #908 cardinal pattern out of tool output.
364
+ *
365
+ * An unterminated trailing fence is treated as fenced-to-EOF (conservative for
366
+ * false-positive suppression) but never as a measurement block.
367
+ *
368
+ * @param {string[]} lines
369
+ * @returns {{ measurementLines: Set<number>, fencedLines: Set<number> }}
370
+ */
371
+ export function scanFences(lines) {
372
+ const measurementLines = new Set();
373
+ const fencedLines = new Set();
374
+ let fenceStart = -1;
375
+ let buffer = [];
376
+ let inFence = false;
377
+
378
+ for (let i = 0; i < lines.length; i++) {
379
+ const isFence = /^\s*```/.test(lines[i]);
380
+ if (!inFence) {
381
+ if (isFence) { inFence = true; fenceStart = i; buffer = []; }
382
+ continue;
383
+ }
384
+ // inside a fence
385
+ if (isFence) {
386
+ // closing fence — evaluate the buffered body
387
+ const body = buffer.join('\n');
388
+ const isMeasurement = MEASUREMENT_CMD_RE.test(body);
389
+ for (let j = fenceStart; j <= i; j++) {
390
+ fencedLines.add(j);
391
+ if (isMeasurement) measurementLines.add(j);
392
+ }
393
+ inFence = false;
394
+ fenceStart = -1;
395
+ buffer = [];
396
+ } else {
397
+ buffer.push(lines[i]);
398
+ }
399
+ }
400
+
401
+ // Unterminated trailing fence — suppress cardinal matches, grant no evidence.
402
+ if (inFence) {
403
+ for (let j = fenceStart; j < lines.length; j++) fencedLines.add(j);
404
+ }
405
+
406
+ // INLINE-code evidence: PSA-006 asks for "the exact pattern executed" to be
407
+ // quoted — an inline `git log --oneline | wc -l` satisfies that exactly as
408
+ // well as a fenced block. GERMAN PROSE evidence (#1218) is admitted on the
409
+ // same line-level footing: `Gemessen 2026-09-06 @ e4674109` is a dated
410
+ // measurement declaration, which is what the rule asks for.
411
+ for (let i = 0; i < lines.length; i++) {
412
+ if (fencedLines.has(i)) continue;
413
+ if (GERMAN_EVIDENCE_RE.test(lines[i])) { measurementLines.add(i); continue; }
414
+ for (const span of lines[i].match(INLINE_CODE_RE) ?? []) {
415
+ if (MEASUREMENT_CMD_RE.test(span)) { measurementLines.add(i); break; }
416
+ }
417
+ }
418
+
419
+ return { measurementLines, fencedLines };
420
+ }
421
+
422
+ /**
423
+ * Index set of lines that belong to a YAML/JSON block (#1218). A `key: scalar`
424
+ * line always qualifies; a bare list item or a closing brace qualifies only
425
+ * when an adjacent line is already a config line, so an ordinary prose bullet
426
+ * is never swept in.
427
+ *
428
+ * @param {string[]} lines
429
+ * @returns {Set<number>}
430
+ */
431
+ export function scanConfigBlocks(lines) {
432
+ const configLines = new Set();
433
+ for (let i = 0; i < lines.length; i++) {
434
+ if (CONFIG_KEY_LINE_RE.test(lines[i])) configLines.add(i);
435
+ }
436
+ // One widening pass: continuation lines glued to a config line above them.
437
+ for (let i = 1; i < lines.length; i++) {
438
+ if (configLines.has(i)) continue;
439
+ if (configLines.has(i - 1) && CONFIG_CONTINUATION_RE.test(lines[i])) configLines.add(i);
440
+ }
441
+ return configLines;
442
+ }
443
+
444
+ /**
445
+ * True when any line in `indices` sits within ±GREP_PROXIMITY_LINES of `i`.
446
+ *
447
+ * @param {Set<number>} indices
448
+ * @param {number} i
449
+ * @returns {boolean}
450
+ */
451
+ function nearIndex(indices, i) {
452
+ for (let j = i - GREP_PROXIMITY_LINES; j <= i + GREP_PROXIMITY_LINES; j++) {
453
+ if (indices.has(j)) return true;
454
+ }
455
+ return false;
456
+ }
457
+
458
+ /**
459
+ * True when a measurement TIMESTAMP marker appears within the same proximity
460
+ * window used for the measurement block itself.
461
+ *
462
+ * @param {string[]} lines
463
+ * @param {number} i
464
+ * @returns {boolean}
465
+ */
466
+ function hasMeasurementTimestamp(lines, i) {
467
+ const from = Math.max(0, i - GREP_PROXIMITY_LINES);
468
+ const to = Math.min(lines.length - 1, i + GREP_PROXIMITY_LINES);
469
+ for (let j = from; j <= to; j++) {
470
+ if (TIMESTAMP_MARKER_RE.test(lines[j])) return true;
471
+ }
472
+ return false;
473
+ }
474
+
475
+ /**
476
+ * Normalise a claim line into its dedup key (#1198): trim, strip leading
477
+ * bullet / ordered-list / heading / blockquote markers, collapse all internal
478
+ * whitespace runs to one space, lowercase.
479
+ *
480
+ * The marker strip is what makes the key stable across the re-formatting an
481
+ * agent does between a bullet in its progress note and the same sentence in
482
+ * its final report — the two shapes that produced the measured duplication.
483
+ *
484
+ * @param {string} text
485
+ * @returns {string}
486
+ */
487
+ export function normalizeClaim(text) {
488
+ if (typeof text !== 'string') return '';
489
+ return text
490
+ .replace(LEADING_MARKER_RE, '')
491
+ .replace(/\s+/g, ' ')
492
+ .trim()
493
+ .toLowerCase();
494
+ }
495
+
496
+ /**
497
+ * Collapse repeated claims into one entry per distinct normalized key,
498
+ * preserving first-seen order and counting `occurrences` (#1198).
499
+ *
500
+ * @param {string[]} claims — raw claim strings in encounter order
501
+ * @returns {{claim: string, normalized: string, occurrences: number}[]}
502
+ */
503
+ export function dedupeViolations(claims) {
504
+ /** @type {Map<string, {claim: string, normalized: string, occurrences: number}>} */
505
+ const byKey = new Map();
506
+ for (const claim of claims) {
507
+ const normalized = normalizeClaim(claim);
508
+ if (!normalized) continue;
509
+ const hit = byKey.get(normalized);
510
+ if (hit) { hit.occurrences += 1; continue; }
511
+ byKey.set(normalized, { claim, normalized, occurrences: 1 });
512
+ }
513
+ return [...byKey.values()];
514
+ }
515
+
516
+ /**
517
+ * Scan concatenated transcript text for claims lacking an adjacent measurement
518
+ * block (within ±GREP_PROXIMITY_LINES).
519
+ *
520
+ * @param {string} text
521
+ * @returns {{ violations: {claim: string, normalized: string, occurrences: number}[], undatedVerified: number }}
522
+ * `violations` — deduplicated, truncated claim snippets with an occurrence
523
+ * count; `undatedVerified` — count of claims that ARE verified but carry no
524
+ * measurement timestamp (advisory).
525
+ */
526
+ export function findViolations(text) {
527
+ if (!text) return { violations: [], undatedVerified: 0 };
528
+ const lines = text.split(/\r?\n/);
529
+ const { measurementLines, fencedLines } = scanFences(lines);
530
+ const configLines = scanConfigBlocks(lines);
531
+ const raw = [];
532
+ let undatedVerified = 0;
533
+
534
+ for (let i = 0; i < lines.length; i++) {
535
+ const line = lines[i];
536
+ // #1198 FIX 2: gate-summary/STATUS lines are tool OUTPUT, not a claim —
537
+ // skipped before any pattern runs (see GATE_SUMMARY_LINE_RE header).
538
+ if (GATE_SUMMARY_LINE_RE.test(line)) continue;
539
+ // #1218: table rows, headings and plan/intent items are not prose
540
+ // assertions at all — skipped before any pattern runs.
541
+ if (NON_PROSE_LINE_RE.test(line)) continue;
542
+
543
+ // #1198 FIX 3 (masking-order bug): mask inline-code spans ONCE, then test
544
+ // BOTH the CLAIM_PATTERNS and the cardinal/ratio patterns against the
545
+ // masked text, so a claim quoted entirely inside backticks (evidence or
546
+ // example text, not an assertion) cannot trip a pattern.
547
+ const masked = line.replace(INLINE_CODE_RE, ' ');
548
+ let matched = CLAIM_PATTERNS.some((re) => re.test(masked));
549
+ if (!matched && !fencedLines.has(i) && !configLines.has(i)) {
550
+ matched = CARDINAL_PATTERN.test(masked) || CARDINAL_RATIO_PATTERN.test(masked);
551
+ }
552
+ if (!matched) continue;
553
+
554
+ if (nearIndex(measurementLines, i)) {
555
+ if (!hasMeasurementTimestamp(lines, i)) undatedVerified++;
556
+ continue;
557
+ }
558
+
559
+ raw.push(line.trim().slice(0, CLAIM_TEXT_MAX));
560
+ }
561
+ return { violations: dedupeViolations(raw), undatedVerified };
562
+ }
@@ -50,7 +50,7 @@ import path from 'node:path';
50
50
 
51
51
  import { emitAllow, emitDeny } from '../scripts/lib/io.mjs';
52
52
  import { emitEvent } from '../scripts/lib/events.mjs';
53
- import { SO_PROJECT_DIR } from '../scripts/lib/platform.mjs';
53
+ import { getProjectDir } from '../scripts/lib/platform.mjs';
54
54
  import {
55
55
  _parseConfigProtection,
56
56
  _isConfigWeakeningAllowed,
@@ -461,7 +461,7 @@ async function main() {
461
461
  let cfgContent = '';
462
462
  for (const file of ['CLAUDE.md', 'AGENTS.md']) {
463
463
  try {
464
- cfgContent = await fs.readFile(path.join(SO_PROJECT_DIR, file), 'utf8');
464
+ cfgContent = await fs.readFile(path.join(getProjectDir(), file), 'utf8');
465
465
  if (cfgContent) break;
466
466
  } catch { /* try next candidate */ }
467
467
  }
@@ -30,7 +30,7 @@ import { shouldRunHook } from './_lib/profile-gate.mjs';
30
30
  // Exit 0 immediately when disabled via SO_HOOK_PROFILE / SO_DISABLED_HOOKS.
31
31
  if (!shouldRunHook('cwd-change-restore')) process.exit(0);
32
32
 
33
- import { SO_PROJECT_DIR } from '../scripts/lib/platform.mjs';
33
+ import { getProjectDir } from '../scripts/lib/platform.mjs';
34
34
  import { atomicMutateJson } from './_lib/atomic-json.mjs';
35
35
 
36
36
  // ---------------------------------------------------------------------------
@@ -89,7 +89,7 @@ async function main() {
89
89
  new_cwd: newCwd,
90
90
  };
91
91
 
92
- const sessionFile = path.join(SO_PROJECT_DIR, '.orchestrator', 'current-session.json');
92
+ const sessionFile = path.join(getProjectDir(), '.orchestrator', 'current-session.json');
93
93
 
94
94
  const result = await atomicMutateJson(sessionFile, {}, (current) => {
95
95
  const existing = Array.isArray(current.cwd_changes)