session-orchestrator 3.23.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 (393) 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 +13 -0
  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 +1401 -0
  81. package/NOTICE +11 -6
  82. package/README.md +127 -92
  83. package/agents/db-specialist.md +0 -1
  84. package/agents/eval-judge.md +1 -1
  85. package/agents/skill-applied-judge.md +1 -1
  86. package/assets/wave-lifecycle.svg +98 -0
  87. package/commands/release.md +6 -3
  88. package/commands/session.md +18 -3
  89. package/docs/README.md +4 -0
  90. package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
  91. package/docs/baseline.md +67 -0
  92. package/docs/ci-setup.md +249 -48
  93. package/docs/codex-setup.md +66 -22
  94. package/docs/components.md +37 -16
  95. package/docs/cursor-setup.md +6 -2
  96. package/docs/events-schema.md +51 -10
  97. package/docs/instruction-delivery.md +62 -0
  98. package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
  99. package/docs/migration-v4.md +341 -0
  100. package/docs/pi-setup.md +6 -1
  101. package/docs/plugin-architecture-v3.md +1 -1
  102. package/docs/rule-authoring.md +85 -19
  103. package/docs/scope-collision-guard.md +8 -8
  104. package/docs/session-config-reference.md +120 -61
  105. package/docs/session-config-template.md +40 -33
  106. package/docs/telemetry/telemetry-claims.md +11 -10
  107. package/docs/telemetry.md +187 -4
  108. package/docs/vault-docs-architecture.md +50 -11
  109. package/hooks/_lib/atomic-json.mjs +111 -0
  110. package/hooks/_lib/hook-import-set.json +1487 -0
  111. package/hooks/_lib/subagent-paths.mjs +143 -0
  112. package/hooks/_lib/subagent-transcript.mjs +562 -0
  113. package/hooks/config-protection.mjs +2 -2
  114. package/hooks/cwd-change-restore.mjs +11 -31
  115. package/hooks/enforce-commands.mjs +69 -0
  116. package/hooks/enforce-scope.mjs +35 -6
  117. package/hooks/hooks-codex.json +1 -1
  118. package/hooks/hooks-cursor.json +10 -0
  119. package/hooks/hooks-pi.json +5 -0
  120. package/hooks/hooks.json +6 -1
  121. package/hooks/loop-guard.mjs +3 -3
  122. package/hooks/on-session-end.mjs +280 -14
  123. package/hooks/on-session-start.mjs +153 -4
  124. package/hooks/on-stop.mjs +371 -17
  125. package/hooks/operator-steer.mjs +2 -2
  126. package/hooks/post-bash-write-verify.mjs +189 -4
  127. package/hooks/post-edit-import-probe.mjs +344 -0
  128. package/hooks/post-subagent-discovery-validator.mjs +278 -392
  129. package/hooks/post-tool-batch-wave-signal.mjs +272 -44
  130. package/hooks/post-tool-failure-corrective-context.mjs +11 -34
  131. package/hooks/post-tooluse-frontend-slop.mjs +3 -3
  132. package/hooks/pre-bash-destructive-guard.mjs +39 -13
  133. package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
  134. package/hooks/skill-invocation-telemetry.mjs +17 -5
  135. package/hooks/subagent-telemetry.mjs +24 -30
  136. package/monitors/monitors.json +3 -3
  137. package/package.json +9 -1
  138. package/pi/prompts/session.md +2 -2
  139. package/plugin.json +27 -0
  140. package/scripts/autopilot.mjs +26 -12
  141. package/scripts/backfill-abandoned-sessions.mjs +130 -15
  142. package/scripts/backfill-learnings-from-vault.mjs +9 -3
  143. package/scripts/dialectic-deriver.mjs +73 -8
  144. package/scripts/emit-event.mjs +10 -2
  145. package/scripts/export-hw-learnings.mjs +113 -1
  146. package/scripts/generate-agents-skills.mjs +378 -0
  147. package/scripts/generate-cursor-adapter.mjs +45 -8
  148. package/scripts/generate-hook-import-set.mjs +249 -0
  149. package/scripts/lib/agent-status.mjs +13 -2
  150. package/scripts/lib/auq/parse.mjs +5 -29
  151. package/scripts/lib/auto-dialectic.mjs +68 -0
  152. package/scripts/lib/auto-dream.mjs +38 -36
  153. package/scripts/lib/autonomy/suitability.mjs +6 -0
  154. package/scripts/lib/autopilot/loop.mjs +2 -2
  155. package/scripts/lib/autopilot/worktree-pipeline.mjs +82 -6
  156. package/scripts/lib/build-live-signals.mjs +25 -22
  157. package/scripts/lib/ci-status-banner.mjs +220 -75
  158. package/scripts/lib/codex/plugin-contract.mjs +82 -6
  159. package/scripts/lib/cold-start-detector.mjs +23 -14
  160. package/scripts/lib/config/auto-dream.mjs +2 -1
  161. package/scripts/lib/config/block-header.mjs +63 -0
  162. package/scripts/lib/config/block-preprocess.mjs +177 -0
  163. package/scripts/lib/config/broken-window.mjs +2 -1
  164. package/scripts/lib/config/cold-start.mjs +2 -1
  165. package/scripts/lib/config/config-protection.mjs +22 -2
  166. package/scripts/lib/config/context-coverage.mjs +2 -1
  167. package/scripts/lib/config/cross-repo.mjs +2 -1
  168. package/scripts/lib/config/custom-phases.mjs +2 -1
  169. package/scripts/lib/config/dialectic.mjs +2 -1
  170. package/scripts/lib/config/discovery-validator.mjs +9 -3
  171. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
  172. package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
  173. package/scripts/lib/config/docs-orchestrator.mjs +2 -1
  174. package/scripts/lib/config/docs-staleness.mjs +2 -1
  175. package/scripts/lib/config/drift-check.mjs +2 -1
  176. package/scripts/lib/config/eval.mjs +2 -1
  177. package/scripts/lib/config/events-rotation.mjs +2 -1
  178. package/scripts/lib/config/evolve.mjs +8 -2
  179. package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
  180. package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
  181. package/scripts/lib/config/handover-gate.mjs +2 -1
  182. package/scripts/lib/config/health-endpoints.mjs +388 -0
  183. package/scripts/lib/config/issue-budget.mjs +2 -1
  184. package/scripts/lib/config/loop-guard.mjs +2 -1
  185. package/scripts/lib/config/memory.mjs +2 -1
  186. package/scripts/lib/config/moc-staleness.mjs +2 -1
  187. package/scripts/lib/config/persona-gate-wave.mjs +2 -1
  188. package/scripts/lib/config/private-config-dir.mjs +67 -0
  189. package/scripts/lib/config/reconcile.mjs +2 -1
  190. package/scripts/lib/config/remote-hosts.mjs +234 -0
  191. package/scripts/lib/config/section-extractor.mjs +7 -1
  192. package/scripts/lib/config/skill-evolution.mjs +2 -1
  193. package/scripts/lib/config/slopcheck.mjs +2 -1
  194. package/scripts/lib/config/state-md-lock.mjs +2 -1
  195. package/scripts/lib/config/templates-first.mjs +2 -1
  196. package/scripts/lib/config/test.mjs +2 -1
  197. package/scripts/lib/config/vault-integration.mjs +7 -1
  198. package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
  199. package/scripts/lib/config/vault-staleness.mjs +2 -1
  200. package/scripts/lib/config/vault-sync.mjs +2 -1
  201. package/scripts/lib/config/verification-auto-fix.mjs +2 -1
  202. package/scripts/lib/config/wave-reviewers.mjs +2 -1
  203. package/scripts/lib/config/worktree-orphans.mjs +2 -1
  204. package/scripts/lib/config.mjs +31 -3
  205. package/scripts/lib/convergence-monitor.mjs +82 -16
  206. package/scripts/lib/dispatcher/enumerate.mjs +2 -17
  207. package/scripts/lib/dispatcher/rank.mjs +124 -48
  208. package/scripts/lib/ecosystem-health.mjs +16 -2
  209. package/scripts/lib/eval/engine.mjs +9 -1
  210. package/scripts/lib/eval/session-resolve.mjs +23 -4
  211. package/scripts/lib/events-schema.mjs +48 -0
  212. package/scripts/lib/events.mjs +256 -7
  213. package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
  214. package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
  215. package/scripts/lib/frontmatter-guard.mjs +131 -13
  216. package/scripts/lib/gates/gate-full.mjs +26 -0
  217. package/scripts/lib/gates/gate-helpers.mjs +76 -0
  218. package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
  219. package/scripts/lib/hardware-pattern-detector.mjs +18 -1
  220. package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
  221. package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
  222. package/scripts/lib/host-identity.mjs +50 -11
  223. package/scripts/lib/instruction-budget-guard.mjs +171 -5
  224. package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
  225. package/scripts/lib/learnings/io.mjs +60 -6
  226. package/scripts/lib/memory-banner.mjs +20 -8
  227. package/scripts/lib/memory-proposals/store.mjs +30 -22
  228. package/scripts/lib/owner-config-banner.mjs +43 -6
  229. package/scripts/lib/owner-config-loader.mjs +21 -10
  230. package/scripts/lib/owner-interview.mjs +3 -3
  231. package/scripts/lib/owner-yaml.mjs +207 -14
  232. package/scripts/lib/peer-discovery.mjs +20 -2
  233. package/scripts/lib/platform.mjs +108 -15
  234. package/scripts/lib/plugin-update-banner.mjs +406 -0
  235. package/scripts/lib/project-hygiene.mjs +38 -2
  236. package/scripts/lib/qg-command-drift-banner.mjs +50 -12
  237. package/scripts/lib/quality-gate.mjs +133 -44
  238. package/scripts/lib/reconcile/emitter.mjs +68 -6
  239. package/scripts/lib/reconcile/engine.mjs +249 -9
  240. package/scripts/lib/reconcile/idempotency.mjs +37 -4
  241. package/scripts/lib/reconcile/writer.mjs +40 -18
  242. package/scripts/lib/scope-gate.mjs +36 -0
  243. package/scripts/lib/session-close-backfill.mjs +125 -18
  244. package/scripts/lib/session-discovery.mjs +57 -3
  245. package/scripts/lib/session-end/phase-skip.mjs +2 -2
  246. package/scripts/lib/session-id.mjs +12 -23
  247. package/scripts/lib/session-identity/own-session.mjs +187 -11
  248. package/scripts/lib/session-lock-shape.mjs +43 -0
  249. package/scripts/lib/session-lock.mjs +5 -10
  250. package/scripts/lib/session-registry.mjs +25 -9
  251. package/scripts/lib/session-schema/constants.mjs +36 -2
  252. package/scripts/lib/session-schema/validator.mjs +38 -4
  253. package/scripts/lib/session-start-probes.mjs +18 -1
  254. package/scripts/lib/session-transition.mjs +1 -1
  255. package/scripts/lib/sessions-canonical.mjs +446 -0
  256. package/scripts/lib/sessions-staleness-banner.mjs +18 -11
  257. package/scripts/lib/skill-health/join.mjs +17 -4
  258. package/scripts/lib/state-md.mjs +78 -0
  259. package/scripts/lib/sunset/walker.mjs +6 -0
  260. package/scripts/lib/telemetry/schema.mjs +255 -17
  261. package/scripts/lib/telemetry/sync.mjs +417 -24
  262. package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
  263. package/scripts/lib/validate/check-agents-skills.mjs +327 -0
  264. package/scripts/lib/validate/check-agents.mjs +3 -3
  265. package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
  266. package/scripts/lib/validate/check-doc-cli-commands.mjs +9 -33
  267. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
  268. package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
  269. package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
  270. package/scripts/lib/validate/check-skill-links.mjs +163 -0
  271. package/scripts/lib/validate/check-skill-script-paths.mjs +455 -0
  272. package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
  273. package/scripts/lib/validate/check-unwired-features.mjs +0 -9
  274. package/scripts/lib/validate/check-validator-registration.mjs +254 -0
  275. package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
  276. package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
  277. package/scripts/lib/validate/markdown-fences.mjs +196 -0
  278. package/scripts/lib/vault-backfill/template.mjs +63 -6
  279. package/scripts/lib/vault-mirror/process.mjs +165 -42
  280. package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
  281. package/scripts/lib/vault-status/board-lock.mjs +185 -0
  282. package/scripts/lib/vault-status/board-writer.mjs +174 -135
  283. package/scripts/lib/vault-status/narrative-mirror.mjs +129 -37
  284. package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
  285. package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
  286. package/scripts/lib/wave-executor/remote-dispatch.mjs +502 -0
  287. package/scripts/lib/wave-resource-gate.mjs +133 -7
  288. package/scripts/lib/wave-sizing.mjs +4 -1
  289. package/scripts/lib/wave-transcript-tail.mjs +142 -8
  290. package/scripts/materialize-wave-scope.mjs +32 -9
  291. package/scripts/memory-propose.mjs +146 -8
  292. package/scripts/migrate-cold-start-seed.mjs +4 -1
  293. package/scripts/parse-config.mjs +60 -3
  294. package/scripts/promote-vault-strict.mjs +4 -15
  295. package/scripts/release.mjs +337 -29
  296. package/scripts/repair-invalid-sessions.mjs +3 -3
  297. package/scripts/run-quality-gate.mjs +128 -11
  298. package/scripts/site-numbers.mjs +36 -4
  299. package/scripts/sweep-expired-learnings.mjs +90 -0
  300. package/scripts/sync-vault-schema.mjs +3 -1
  301. package/scripts/telemetry.mjs +2 -2
  302. package/scripts/validate-plugin.mjs +187 -0
  303. package/scripts/validate-wave-scope.mjs +28 -8
  304. package/scripts/vault-consolidate.mjs +3 -11
  305. package/scripts/vault-integration-watcher.mjs +2 -4
  306. package/scripts/vault-mirror.mjs +111 -26
  307. package/scripts/wave-scope-binding.mjs +215 -0
  308. package/skills/_shared/instruction-file-resolution.md +10 -0
  309. package/skills/_shared/parallel-aware-auq.md +31 -2
  310. package/skills/_shared/parallel-aware-preamble.md +18 -4
  311. package/skills/_shared/platform-tools.md +1 -1
  312. package/skills/_shared/state-ownership.md +1 -1
  313. package/skills/architecture/SKILL.md +7 -5
  314. package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
  315. package/skills/autopilot/SKILL.md +4 -18
  316. package/skills/claude-md-drift-check/SKILL.md +5 -1
  317. package/skills/claude-md-drift-check/checker.mjs +62 -2
  318. package/skills/convergence-monitoring/SIGNALS.md +55 -0
  319. package/skills/discovery/probes/vault-staleness.mjs +37 -13
  320. package/skills/discovery/probes-arch.md +20 -18
  321. package/skills/dispatcher/SKILL.md +3 -2
  322. package/skills/ecosystem-health/SKILL.md +4 -1
  323. package/skills/ecosystem-health/wizard.md +5 -0
  324. package/skills/evolve/SKILL.md +87 -11
  325. package/skills/frontmatter-guard/SKILL.md +11 -5
  326. package/skills/npm-publish/SKILL.md +1 -1
  327. package/skills/reconcile/SKILL.md +38 -2
  328. package/skills/remote-offload/SKILL.md +89 -0
  329. package/skills/session-end/SKILL.md +18 -905
  330. package/skills/session-end/phase-3-6-tail.md +19 -9
  331. package/skills/session-end/plan-verification.md +221 -155
  332. package/skills/session-end/references/phase-2-quality-gate.md +93 -0
  333. package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
  334. package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
  335. package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
  336. package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
  337. package/skills/session-end/references/session-summary-template.md +62 -0
  338. package/skills/session-plan/SKILL.md +49 -0
  339. package/skills/session-start/SKILL.md +41 -900
  340. package/skills/session-start/phase-8-5-express-path.md +1 -1
  341. package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
  342. package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
  343. package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
  344. package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
  345. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
  346. package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
  347. package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
  348. package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
  349. package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
  350. package/skills/vault-sync/validator.mjs +21 -27
  351. package/skills/wave-executor/SKILL.md +16 -2
  352. package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
  353. package/skills/wave-executor/references/wave-loop-review.md +570 -0
  354. package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
  355. package/skills/wave-executor/wave-loop.md +14 -1271
  356. package/templates/_shared/journey-manifest.md +10 -6
  357. package/.cursor/commands/autopilot-multi.md +0 -14
  358. package/.cursor/commands/contract-version-bump.md +0 -14
  359. package/.cursor/commands/journey-audit.md +0 -14
  360. package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
  361. package/.cursor/skills/daily/SKILL.md +0 -12
  362. package/.cursor/skills/domain-model/SKILL.md +0 -13
  363. package/.cursor/skills/journey-audit/SKILL.md +0 -13
  364. package/.cursor/skills/skill-creator/SKILL.md +0 -13
  365. package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
  366. package/commands/autopilot-multi.md +0 -74
  367. package/commands/contract-version-bump.md +0 -28
  368. package/commands/journey-audit.md +0 -43
  369. package/pi/prompts/autopilot-multi.md +0 -12
  370. package/pi/prompts/contract-version-bump.md +0 -12
  371. package/pi/prompts/journey-audit.md +0 -12
  372. package/scripts/autopilot-multi.mjs +0 -885
  373. package/scripts/backfill-learnings-expires.mjs +0 -196
  374. package/scripts/backfill-learnings.mjs +0 -203
  375. package/scripts/fleet-instruction-scan.mjs +0 -141
  376. package/scripts/lib/autopilot/dep-graph.mjs +0 -417
  377. package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
  378. package/scripts/lib/webhook-url.mjs +0 -105
  379. package/scripts/lifecycle-sim-v6.mjs +0 -347
  380. package/scripts/migrate-learnings-jsonl.mjs +0 -189
  381. package/scripts/migrate-subagents-jsonl.mjs +0 -196
  382. package/scripts/upload-social-preview.mjs +0 -316
  383. package/skills/_shared/model-selection.md +0 -64
  384. package/skills/contract-version-bump/SKILL.md +0 -219
  385. package/skills/daily/SKILL.md +0 -222
  386. package/skills/daily/generate.sh +0 -92
  387. package/skills/daily/templates/daily.md.tpl +0 -36
  388. package/skills/journey-audit/SKILL.md +0 -269
  389. package/skills/skill-creator/SKILL.md +0 -168
  390. package/skills/ubiquitous-language/SKILL.md +0 -97
  391. package/skills/vault-sync/package-lock.json +0 -40
  392. /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
  393. /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
@@ -11,24 +11,39 @@
11
11
  * 1. shouldRunHook('post-subagent-discovery-validator') gate — exit 0 when disabled.
12
12
  * 2. Read JSON payload from stdin; require hook_event_name === 'SubagentStop'.
13
13
  * 3. Read `discovery-validator.enabled` from CLAUDE.md/AGENTS.md Session Config.
14
- * Default OFF — exit 0 immediately unless explicitly enabled.
15
- * 4. Read `input.transcript_path` (whole-session JSONL of assistant/user records),
16
- * scan the TAIL (last ~8 `type:"assistant"` records), concat text blocks.
17
- * 5. Regex-scan the concatenated text for 8 claim patterns — 6 quantifier-
18
- * triggered distributional claims, the #908 bare-cardinal repo-state
19
- * fact ("14 commits", "92 learnings", "5 dirty files", "412 lines"), and
20
- * the #918 numerator/denominator slash form ("12/14 files", "4/4 callers").
21
- * 6. For each match, check whether a fenced ```bash block containing a
22
- * MEASUREMENT command (grep/rg/find/git/wc/jq/ls/node/npm) appears within
23
- * ±5 lines. If a claim has NO adjacent measurement block → record a
24
- * `discovery_validator_violation` event in events.jsonl + a stderr WARN.
25
- * 7. ADVISORY (#908 Baustein 2 input): for claims that ARE verified, check
26
- * whether the adjacent block also carries a measurement TIMESTAMP (ISO
27
- * date, `HEAD`, "as of", "measured at"). Undated-but-verified claims are
28
- * counted and reported in the warn text — they are NOT violations in v1.
14
+ * Default OFF (opt-in) — exit 0 immediately unless explicitly enabled.
15
+ * 4. Resolve the STOPPING SUBAGENT's OWN transcript (never the parent):
16
+ * `input.agent_transcript_path` when the harness sends it, else
17
+ * `<dir(transcript_path)>/<base>/subagents/agent-<agent_id>.jsonl`. Scan its
18
+ * TAIL (last ~8 `type:"assistant"` records), concat text blocks. When no
19
+ * `agent_id` is derivable (or the file is absent) the hook exits 0 and
20
+ * records NOTHING — see the scope note below.
21
+ * 5. Hand the concatenated text to `hooks/_lib/subagent-transcript.mjs`
22
+ * `findViolations()` — the whole matcher (claim patterns, negative-context
23
+ * guards, evidence proximity, normalisation, dedup) lives there so it can
24
+ * be measured against a claim corpus without spawning this hook. It
25
+ * returns DEDUPLICATED `{claim, normalized, occurrences}` records.
26
+ * 6. Attribute the claim: `agent` + `agent_source` (`payload`|`meta`|`none`)
27
+ * + `agent_description`, and — on `none` — the sorted stdin `payload_keys`
28
+ * the harness DID send, so a gap is diagnosable from the ledger.
29
+ * 7. Write ONE record per distinct claim per session (session-scoped tmp
30
+ * sentinel), plus a stderr WARN. ADVISORY (#908 Baustein 2 input): claims
31
+ * that ARE verified but carry no measurement TIMESTAMP are counted and
32
+ * reported in the warn text — never recorded as violations in v1.
29
33
  *
30
- * Why read the transcript: the SubagentStop stdin payload has NO output_text
31
- * field. The agent's text lives in `input.transcript_path`.
34
+ * Why read a transcript at all: the SubagentStop stdin payload has NO
35
+ * output_text field — the agent's text only exists on disk.
36
+ *
37
+ * WHICH transcript (#1191, the root cause behind the fleet false-positive
38
+ * flood): `input.transcript_path` is the PARENT/MAIN session transcript, not
39
+ * the subagent's. Scanning it flagged the COORDINATOR's own prose — wave plans,
40
+ * TL;DRs, complexity scores. Measured 2026-09-02 on a seeded random sample of
41
+ * 60 violations: 100% coordinator text, scope-adjusted precision 0%, and
42
+ * `agent` was `"unknown"` in 90.8% of 1,541 vault events. The hook therefore
43
+ * reads `<transcriptDir>/<session>/subagents/agent-<agent_id>.jsonl` (the same
44
+ * layout `hooks/subagent-telemetry.mjs` and `scripts/lib/wave-transcript-tail.mjs`
45
+ * read) and NEVER falls back to the parent path: a scan of the wrong transcript
46
+ * is worse than no scan.
32
47
  *
33
48
  * Output channels — THREE writes, TWO different recipients:
34
49
  * - `discovery_validator_violation` in .orchestrator/metrics/events.jsonl,
@@ -55,194 +70,13 @@ import { promises as fs } from 'node:fs';
55
70
  import { tmpdir } from 'node:os';
56
71
  import path from 'node:path';
57
72
 
73
+ import { resolveSubagentSidecar } from './_lib/subagent-paths.mjs';
74
+ import { findViolations, readTranscriptTail } from './_lib/subagent-transcript.mjs';
58
75
  import { appendJsonl } from '../scripts/lib/common.mjs';
59
76
  import { eventsFilePath } from '../scripts/lib/events.mjs';
60
- import { SO_PROJECT_DIR } from '../scripts/lib/platform.mjs';
77
+ import { getProjectDir } from '../scripts/lib/platform.mjs';
61
78
  import { _parseDiscoveryValidator } from '../scripts/lib/config/discovery-validator.mjs';
62
79
 
63
- // ---------------------------------------------------------------------------
64
- // Constants
65
- // ---------------------------------------------------------------------------
66
-
67
- /** Number of trailing assistant records to scan. */
68
- const TAIL_RECORDS = 8;
69
- /** Proximity window (in lines) for an adjacent grep transcript. */
70
- const GREP_PROXIMITY_LINES = 5;
71
- /** Max characters of claim text persisted to the event record. */
72
- const CLAIM_TEXT_MAX = 200;
73
-
74
- /**
75
- * Code-distribution context nouns (singular or plural). A distributional claim
76
- * is only a PSA-006 *code* claim when one of these appears near the trigger —
77
- * this is what separates "4 of 4 callers" (a real claim) from "Turn 3 of 25"
78
- * (a turn counter) and "every caller imports X" from "every developer should
79
- * test" (generic advice). Tightening per W2-review LOW finding (#567).
80
- *
81
- * Single optional `\s?` only (call site / callsite) — no nested quantifiers,
82
- * so the alternation stays linear-time / ReDoS-safe.
83
- */
84
- 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?)';
85
-
86
- /**
87
- * Repo-STATE nouns (#908). The four documented #908 drift cases were counts of
88
- * repository state, not of code locations: "14 commits", "92 learnings",
89
- * "5 dirty files", "412 lines". None of them contains a CTX noun, so the
90
- * original six patterns could not see them.
91
- *
92
- * Vocabulary taken from this repo's own artefacts (`.orchestrator/metrics/*.jsonl`
93
- * record kinds, `.claude/rules/`, `skills/`, `agents/`, `hooks/`) rather than a
94
- * generic English list — a noun that never names a countable repo artefact here
95
- * only buys false positives.
96
- */
97
- const STATE = '(?:commits?|learnings?|issues?|branches?|lines?|entries|records?|sessions?|rules?|skills?|probes?|waves?|proposals?|worktrees?)';
98
-
99
- /**
100
- * The noun class the BARE-CARDINAL pattern may use — deliberately a strict
101
- * subset of STATE ∪ CTX, restricted to the artefact kinds the #908 drift was
102
- * actually measured in (commit counts, learnings counts, open-issue counts,
103
- * branch counts, line counts, dirty-file counts).
104
- *
105
- * Measured, not guessed: over 32 real agent-stop windows from this repo's own
106
- * transcripts, admitting the full CTX ∪ STATE set fired 93 times (2.9 per
107
- * stop — the "validator gets switched off" zone). Every noun below earns its
108
- * place by naming one of the documented #908 facts; the ones that only cost
109
- * false positives (`tests`, `references`, `matches`, `agents`, `files` without
110
- * a state adjective) are excluded here and remain reachable through the six
111
- * quantifier-triggered patterns above, which have a lexical anchor.
112
- *
113
- * `callers` (#918): PSA-006's own canonical noun — the rule text's worked
114
- * examples ("4 of 4 callers", "100% of callers opt-in") all count callers, yet
115
- * the cardinal pattern could not see a bare "14 callers". Re-measured with it
116
- * admitted: 490 real SubagentStop transcripts (2026-07-31), +0 additional
117
- * firings from the bare form — the noun is free on this corpus (all +3 delta
118
- * firings came from the #918 slash pattern below).
119
- */
120
- const CARDINAL_NOUN = '(?:commits?|learnings?|issues?|branches?|lines?|files?|callers?)';
121
-
122
- /**
123
- * Wide noun class = code-distribution nouns ∪ repo-state nouns. Used by the six
124
- * QUANTIFIER-triggered patterns ("N of M", "100% of", "all N", "no remaining",
125
- * "none of") — each of those carries a strong lexical trigger, so widening the
126
- * noun set there is low-risk.
127
- *
128
- * Deliberately NOT used by the `every <noun>` pattern: `every` has no numeric
129
- * anchor, so `every commit must be signed` / `every rule is always-on` are
130
- * ordinary prose, not measured claims. That pattern keeps the narrow CTX.
131
- */
132
- const WIDE = `(?:${CTX}|${STATE})`;
133
-
134
- /** Bounded same-line gap between a trigger and its context noun. */
135
- const CTX_GAP = '[^\\n]{0,40}?';
136
-
137
- /**
138
- * Distributional-claim patterns (case-insensitive). A match is a PSA-006 claim
139
- * that requires an adjacent measurement transcript.
140
- *
141
- * Each pattern requires a context noun within a small, bounded same-line window
142
- * of the trigger. The `[^\n]{0,40}?` gap is a bounded lazy character class
143
- * (linear-time — the ReDoS-safety the W2 reviewer verified is preserved). True
144
- * claims ("4 of 4 callers opt-in", "every caller imports X", "no remaining
145
- * references to Y") still flag; benign strings ("Turn 3 of 25 complete",
146
- * "every developer should test", "100% of users love it") do not.
147
- */
148
- const CLAIM_PATTERNS = [
149
- new RegExp(`\\b\\d+ of \\d+\\b${CTX_GAP}\\b${WIDE}\\b`, 'i'),
150
- new RegExp(`100% of\\b${CTX_GAP}\\b${WIDE}\\b`, 'i'),
151
- new RegExp(`\\ball \\d+\\b${CTX_GAP}\\b${WIDE}\\b`, 'i'),
152
- new RegExp(`no remaining\\b${CTX_GAP}\\b${WIDE}\\b`, 'i'),
153
- new RegExp(`every ${CTX}\\b`, 'i'),
154
- new RegExp(`none of\\b${CTX_GAP}\\b${WIDE}\\b`, 'i'),
155
- ];
156
-
157
- /**
158
- * Pattern 7 (#908) — the BARE CARDINAL repo-state fact. `14 commits` has no
159
- * quantifier trigger at all, which is exactly why the #908 drift went unseen.
160
- *
161
- * A naive `\d+` would fire on every issue reference, version literal, date,
162
- * line number and percentage in a normal report — and a validator that fires on
163
- * every report gets switched off, which is strictly worse than no validator.
164
- * Precision is therefore bought three ways:
165
- *
166
- * 1. TRIGGER: a digit run that is not glued to identifier punctuation.
167
- * Lookbehind rejects `#906`, `v3`, `PSA-006`, `W2`, `foo.mjs:123`;
168
- * lookahead rejects `3.17`, `70%`, `2026-07-29`, `12615/0`.
169
- * 2. GAP: at most two intervening ADJECTIVE-like words, and never a
170
- * preposition/article/copula. "5 dirty files" matches; "3 of 5 stars" and
171
- * "2 sections below the rules" do not.
172
- * 3. SCOPE: evaluated only on prose lines with inline-code spans masked out —
173
- * fenced blocks are skipped entirely (see `findViolations`), because a
174
- * number inside a fence is tool OUTPUT (the evidence itself), not an
175
- * unverified assertion about it.
176
- *
177
- * All quantifiers are bounded ({1,9}, {0,2}) — linear-time, ReDoS-safe.
178
- */
179
- const CARDINAL_TRIGGER = '(?<![\\w#$:/.-])\\d{1,9}(?![\\d.%:/-])';
180
- const CARDINAL_STOPWORDS =
181
- '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';
182
- const CARDINAL_GAP = `(?:\\s+(?!(?:${CARDINAL_STOPWORDS})\\b)[A-Za-z][\\w-]*){0,2}`;
183
- const CARDINAL_PATTERN = new RegExp(`${CARDINAL_TRIGGER}${CARDINAL_GAP}\\s+${CARDINAL_NOUN}\\b`, 'i');
184
-
185
- /**
186
- * Pattern 8 (#918) — the PSA-006 CANONICAL numerator/denominator slash form:
187
- * "12/14 files", "4/4 callers". PSA-006 literally demands "Quote the numerator
188
- * AND denominator", and `N/M` is the notation that demand produces — yet the
189
- * cardinal trigger's lookahead `(?![\d.%:/-])` excludes the slash, making the
190
- * rule's own canon form structurally invisible (#918).
191
- *
192
- * Admitting the slash re-opens the `12615/0/11` gate-summary class the
193
- * lookahead exists to suppress, so the slash form gets its OWN pattern with a
194
- * STRICTER contract than the bare cardinal:
195
- *
196
- * 1. RATIO: exactly two slash-joined numbers. Both boundary guards stay:
197
- * the lookbehind rejects a numerator glued to a path/id (`hooks/12/14`),
198
- * the lookahead after the DENOMINATOR rejects a third slash-segment —
199
- * `12615/0/11` fails twice over (denominator `0` is followed by `/`, and
200
- * the trailing pair `0/11` has a `/`-glued numerator).
201
- * 2. NOUN IMMEDIATELY AFTER — no CARDINAL_GAP. The noun-after-denominator is
202
- * the discriminator that separates a measured ratio ("12/14 files") from
203
- * a bare slash pair that is a date or score ("on 12/14 we shipped",
204
- * "rated 3/5 overall"): those are followed by anything BUT an artefact
205
- * noun. Widening to the gapped form would admit US-date + adjective +
206
- * noun collisions with no PSA-006 payoff.
207
- *
208
- * FP re-measured on the CORRECT text sort (#918 requirement — the prior 1.07
209
- * rate was measured on coordinator narration, not on what this hook reads):
210
- * 490 real SubagentStop subagent transcripts (`~/.claude/projects/<slug>/<session>/
211
- * subagents/*.jsonl`), spawning THIS hook per transcript, 2026-07-31.
212
- * Baseline (pre-#918): 345 firings / 0.7041 per stop. With slash form +
213
- * `callers` admitted: 348 / 0.7102 — +3 firings, of which 1 is a true positive
214
- * ("0/49 Learnings mit Allow-List-Typ", an unverified canon-form claim), 1
215
- * quotes the #918 example sentence itself (mention-not-use), and 1 is a
216
- * before/after line-count pair ("858/857 lines", class-consistent with the
217
- * bare "412 lines" behaviour of pattern 7).
218
- *
219
- * Bounded quantifiers only ({1,9}) — linear-time, ReDoS-safe.
220
- */
221
- const CARDINAL_RATIO_PATTERN = new RegExp(
222
- `(?<![\\w#$:/.-])\\d{1,9}/\\d{1,9}(?![\\d.%:/-])\\s+${CARDINAL_NOUN}\\b`,
223
- 'i'
224
- );
225
-
226
- /** Inline-code spans are masked before the cardinal pattern runs. */
227
- const INLINE_CODE_RE = /`[^`\n]*`/g;
228
-
229
- /**
230
- * Commands that count as a MEASUREMENT inside a fenced block. `grep|rg|find`
231
- * (the #567 set) only covers text search; the #908 facts are measured with
232
- * `git log --oneline | wc -l`, `jq` over a JSONL metrics file, `ls | wc -l`,
233
- * or a `node`/`npm` script. Refusing to recognise those made the honest,
234
- * evidence-quoting path fail verification.
235
- */
236
- const MEASUREMENT_CMD_RE = /\b(grep|rg|find|git|wc|jq|ls|node|npm)\b/;
237
-
238
- /**
239
- * Markers that date a measurement (#908 Baustein 2 input). ADVISORY in v1:
240
- * an undated-but-verified claim is counted and reported, never a violation —
241
- * a hard contract without an established authoring habit buys friction, not
242
- * accuracy. Baustein 2 can escalate this to a violation once the habit exists.
243
- */
244
- const TIMESTAMP_MARKER_RE = /\b\d{4}-\d{2}-\d{2}\b|\bHEAD\b|\bas of\b|\bmeasured (?:at|on)\b|\brev-parse\b/i;
245
-
246
80
  // ---------------------------------------------------------------------------
247
81
  // stdin reading (inline — Stop-family hooks exit 0 always, never deny)
248
82
  // ---------------------------------------------------------------------------
@@ -287,8 +121,8 @@ function readStdinJson() {
287
121
  */
288
122
  async function isEnabled() {
289
123
  const candidates = [
290
- path.join(SO_PROJECT_DIR, 'CLAUDE.md'),
291
- path.join(SO_PROJECT_DIR, 'AGENTS.md'),
124
+ path.join(getProjectDir(), 'CLAUDE.md'),
125
+ path.join(getProjectDir(), 'AGENTS.md'),
292
126
  ];
293
127
  for (const file of candidates) {
294
128
  try {
@@ -302,205 +136,143 @@ async function isEnabled() {
302
136
  }
303
137
 
304
138
  // ---------------------------------------------------------------------------
305
- // transcript reading
139
+ // payload helpers
306
140
  // ---------------------------------------------------------------------------
307
141
 
308
142
  /**
309
- * Read the transcript JSONL at `transcriptPath`, take the last TAIL_RECORDS
310
- * `type:"assistant"` records, and concatenate their text content blocks.
311
- * Returns '' on any failure (missing file, /dev/null, malformed lines).
143
+ * Pick the first non-empty trimmed string value from `input` across the given
144
+ * candidate keys, in order. Returns `fallback` when none match. Mirrors the
145
+ * helper in hooks/subagent-telemetry.mjs so the two hooks resolve session ids
146
+ * identically (parent_session_id first).
312
147
  *
313
- * @param {string} transcriptPath
314
- * @returns {Promise<string>}
148
+ * @param {object} input
149
+ * @param {string[]} keys
150
+ * @param {*} fallback
151
+ * @returns {string|*}
315
152
  */
316
- async function readTranscriptTail(transcriptPath) {
317
- if (typeof transcriptPath !== 'string' || !transcriptPath) return '';
318
- let raw;
319
- try {
320
- raw = await fs.readFile(transcriptPath, 'utf8');
321
- } catch {
322
- return '';
323
- }
324
- if (!raw.trim()) return '';
325
-
326
- const assistantRecords = [];
327
- for (const line of raw.split(/\r?\n/)) {
328
- const trimmed = line.trim();
329
- if (!trimmed) continue;
330
- let rec;
331
- try { rec = JSON.parse(trimmed); } catch { continue; }
332
- if (rec && rec.type === 'assistant') assistantRecords.push(rec);
333
- }
334
-
335
- const tail = assistantRecords.slice(-TAIL_RECORDS);
336
- const textBlocks = [];
337
- for (const rec of tail) {
338
- const content = rec?.message?.content;
339
- if (!Array.isArray(content)) continue;
340
- for (const block of content) {
341
- if (block && block.type === 'text' && typeof block.text === 'string') {
342
- textBlocks.push(block.text);
343
- }
344
- }
153
+ function firstNonEmptyString(input, keys, fallback) {
154
+ for (const k of keys) {
155
+ const v = input[k];
156
+ if (typeof v === 'string' && v.trim()) return v;
345
157
  }
346
- return textBlocks.join('\n');
158
+ return fallback;
347
159
  }
348
160
 
349
- // ---------------------------------------------------------------------------
350
- // claim scanning
351
- // ---------------------------------------------------------------------------
352
-
353
161
  /**
354
- * Single fence walk. Returns two disjoint-purpose index sets:
355
- * - `measurementLines`: lines belonging to a fenced block whose body contains
356
- * a MEASUREMENT_CMD_RE invocation (the evidence a claim can lean on).
357
- * - `fencedLines`: lines belonging to ANY fenced block (evidence or not).
358
- * Used only to keep the greedy #908 cardinal pattern out of tool output;
359
- * the six quantifier patterns are unchanged and still scan fenced lines.
162
+ * Resolve the STOPPING SUBAGENT's own sidecar pair (#1191, #1196).
360
163
  *
361
- * An unterminated trailing fence is treated as fenced-to-EOF (conservative for
362
- * false-positive suppression) but never as a measurement block (its body was
363
- * never closed, so we cannot claim it verified anything).
164
+ * Thin wrapper over the consolidated derivation —
165
+ * `hooks/_lib/subagent-paths.mjs` `resolveSubagentSidecar()` — which now ALSO
166
+ * containment-checks the `agent_transcript_path` override this file used to
167
+ * return unvalidated (see that module's header divergence table). Returns
168
+ * null when the derivation is impossible — the caller must then scan
169
+ * NOTHING. Falling back to `input.transcript_path` is the defect this
170
+ * function exists to remove: that path is the coordinator's transcript.
364
171
  *
365
- * @param {string[]} lines
366
- * @returns {{ measurementLines: Set<number>, fencedLines: Set<number> }}
172
+ * @param {object} input — SubagentStop stdin payload
173
+ * @param {string|null} agentId
174
+ * @returns {{base: string, transcript: string, meta: string}|null}
367
175
  */
368
- function scanFences(lines) {
369
- const measurementLines = new Set();
370
- const fencedLines = new Set();
371
- let fenceStart = -1;
372
- let buffer = [];
373
- let inFence = false;
374
-
375
- for (let i = 0; i < lines.length; i++) {
376
- const isFence = /^\s*```/.test(lines[i]);
377
- if (!inFence) {
378
- if (isFence) { inFence = true; fenceStart = i; buffer = []; }
379
- continue;
380
- }
381
- // inside a fence
382
- if (isFence) {
383
- // closing fence — evaluate the buffered body
384
- const body = buffer.join('\n');
385
- const isMeasurement = MEASUREMENT_CMD_RE.test(body);
386
- for (let j = fenceStart; j <= i; j++) {
387
- fencedLines.add(j);
388
- if (isMeasurement) measurementLines.add(j);
389
- }
390
- inFence = false;
391
- fenceStart = -1;
392
- buffer = [];
393
- } else {
394
- buffer.push(lines[i]);
395
- }
396
- }
397
-
398
- // Unterminated trailing fence — suppress cardinal matches, grant no evidence.
399
- if (inFence) {
400
- for (let j = fenceStart; j < lines.length; j++) fencedLines.add(j);
401
- }
402
-
403
- // INLINE-code evidence: PSA-006 asks for "the exact pattern executed" to be
404
- // quoted — an inline `git log --oneline | wc -l` satisfies that exactly as
405
- // well as a fenced block, and one-line findings are commonly written that
406
- // way. Refusing to count it would penalise the honest path.
407
- for (let i = 0; i < lines.length; i++) {
408
- if (fencedLines.has(i)) continue;
409
- for (const span of lines[i].match(INLINE_CODE_RE) ?? []) {
410
- if (MEASUREMENT_CMD_RE.test(span)) { measurementLines.add(i); break; }
411
- }
412
- }
413
-
414
- return { measurementLines, fencedLines };
176
+ function resolveAgentTranscriptPath(input, agentId) {
177
+ const agentTranscriptPath = firstNonEmptyString(input, ['agent_transcript_path'], null);
178
+ return resolveSubagentSidecar({ transcriptPath: input.transcript_path, agentId, agentTranscriptPath });
415
179
  }
416
180
 
417
181
  /**
418
- * True when any line in `indices` sits within ±GREP_PROXIMITY_LINES of `i`.
419
- *
420
- * @param {Set<number>} indices
421
- * @param {number} i
422
- * @returns {boolean}
182
+ * Agent types carry a plugin qualifier, so the COLON is part of the real shape:
183
+ * `session-orchestrator:code-implementer` (37 chars, measured on-disk in a real
184
+ * sidecar meta.json 2026-09-02). Same constant as `AGENT_TYPE_META_RE` in
185
+ * hooks/on-stop.mjs — kept local because the two hooks share no module.
423
186
  */
424
- function nearIndex(indices, i) {
425
- for (let j = i - GREP_PROXIMITY_LINES; j <= i + GREP_PROXIMITY_LINES; j++) {
426
- if (indices.has(j)) return true;
427
- }
428
- return false;
429
- }
187
+ const AGENT_TYPE_META_RE = /^[A-Za-z0-9_.:-]{1,64}$/;
430
188
 
431
189
  /**
432
- * True when a measurement TIMESTAMP marker appears within the same proximity
433
- * window used for the measurement block itself.
190
+ * The sidecar `description` is free operator prose ("W1-d5 Scripts-Audit"), so
191
+ * it is clamped by LENGTH and stripped of control characters rather than
192
+ * charset-validated. 120 chars is the widest description measured on-disk
193
+ * (2026-09-06, 20 real `agent-*.meta.json` files under
194
+ * `~/.claude/projects/<slug>/<session>/subagents/`: max 34 chars).
434
195
  *
435
- * @param {string[]} lines
436
- * @param {number} i
437
- * @returns {boolean}
196
+ * Exposure note: unlike `orchestrator.agent.stopped` — which deliberately
197
+ * omits `description` because that record travels the optional Clank webhook
198
+ * unredacted (see hooks/on-stop.mjs) — this record is appended straight to
199
+ * `.orchestrator/metrics/events.jsonl` via `appendJsonl`, never through
200
+ * `emitEvent`, and it ALREADY carries up to 200 characters of the agent's own
201
+ * prose in `claim_text`. Carrying the description adds no new exposure class.
438
202
  */
439
- function hasMeasurementTimestamp(lines, i) {
440
- const from = Math.max(0, i - GREP_PROXIMITY_LINES);
441
- const to = Math.min(lines.length - 1, i + GREP_PROXIMITY_LINES);
442
- for (let j = from; j <= to; j++) {
443
- if (TIMESTAMP_MARKER_RE.test(lines[j])) return true;
444
- }
445
- return false;
446
- }
203
+ const AGENT_DESCRIPTION_MAX = 120;
447
204
 
448
205
  /**
449
- * Scan concatenated transcript text for claims lacking an adjacent measurement
450
- * block (within ±GREP_PROXIMITY_LINES).
206
+ * Resolve WHO made the claim, and say where the answer came from (#1218).
207
+ *
208
+ * Measured 2026-09-06 over the fleet's worst-affected repo
209
+ * (`extern/aiat-barrierefrei-engine`, 3,360 `discovery_validator_violation`
210
+ * records): `agent` was the literal string `"unknown"` on 3,026 of them
211
+ * (90.1%), and NOT ONE record carried an `agent_id`. Both the sidecar-meta
212
+ * fallback and the `agent_id` field landed in the same commit (936dae8a,
213
+ * 2026-09-02); the newest of those 3,360 records is 2026-08-25. The 90.1% is
214
+ * therefore a property of a PRE-FIX corpus, not of the code at HEAD — d3's
215
+ * open question 3 ("pre- or post-#1191?") resolves to *pre*.
216
+ *
217
+ * What the corpus does NOT excuse is the SHAPE of the answer. Two gaps remain
218
+ * at HEAD and this function closes them:
451
219
  *
452
- * @param {string} text
453
- * @returns {{ violations: string[], undatedVerified: number }}
454
- * `violations` — truncated claim-text snippets; `undatedVerified` — count of
455
- * claims that ARE verified but carry no measurement timestamp (advisory).
220
+ * 1. `"unknown"` was indistinguishable from a real agent type named
221
+ * "unknown", and carried no hint of WHY resolution failed. The record now
222
+ * always carries `agent_source` — `payload` | `meta` | `none` — and, on
223
+ * `none`, the sorted list of stdin keys that WERE present, so the next
224
+ * reader diagnoses the harness gap from the ledger instead of guessing.
225
+ * 2. `agentType` is frequently the useless class `general-purpose`: measured
226
+ * on the same day over 20 real sidecars in THIS repo's own session
227
+ * directory, 14 read `general-purpose` and only 6 a plugin-qualified type.
228
+ * The sidecar's `description` ("W1-d5 Scripts-Audit") is what actually
229
+ * identifies the agent, so it is carried alongside as
230
+ * `agent_description`.
231
+ *
232
+ * @param {object} input — SubagentStop stdin payload
233
+ * @param {string} metaPath — `resolveSubagentSidecar(...).meta`
234
+ * @returns {Promise<{agent: string, source: 'payload'|'meta'|'none', description: string|null, payloadKeys: string[]}>}
456
235
  */
457
- function findViolations(text) {
458
- if (!text) return { violations: [], undatedVerified: 0 };
459
- const lines = text.split(/\r?\n/);
460
- const { measurementLines, fencedLines } = scanFences(lines);
461
- const violations = [];
462
- let undatedVerified = 0;
463
-
464
- for (let i = 0; i < lines.length; i++) {
465
- const line = lines[i];
466
- let matched = CLAIM_PATTERNS.some((re) => re.test(line));
467
- if (!matched && !fencedLines.has(i)) {
468
- const masked = line.replace(INLINE_CODE_RE, ' ');
469
- matched = CARDINAL_PATTERN.test(masked) || CARDINAL_RATIO_PATTERN.test(masked);
470
- }
471
- if (!matched) continue;
472
-
473
- if (nearIndex(measurementLines, i)) {
474
- if (!hasMeasurementTimestamp(lines, i)) undatedVerified++;
475
- continue;
236
+ async function resolveAgentAttribution(input, metaPath) {
237
+ const fromPayload = firstNonEmptyString(input, ['agent_type', 'subagent_type'], null);
238
+ // Clamped with the same shape hooks/on-stop.mjs applies to `agentType`
239
+ // (colon included — `session-orchestrator:code-implementer` is the real
240
+ // shape). This value reaches BOTH the ledger event and the model-visible
241
+ // `additionalContext` string, so a mismatch is OMITTED rather than
242
+ // truncated: an unmeasured type stays visibly unmeasured.
243
+ const payloadAgent =
244
+ fromPayload !== null && AGENT_TYPE_META_RE.test(fromPayload.trim()) ? fromPayload.trim() : null;
245
+
246
+ let metaAgent = null;
247
+ let description = null;
248
+ try {
249
+ const meta = JSON.parse(await fs.readFile(metaPath, 'utf8'));
250
+ const t = meta?.agentType;
251
+ if (typeof t === 'string' && AGENT_TYPE_META_RE.test(t.trim())) metaAgent = t.trim();
252
+ const d = meta?.description;
253
+ if (typeof d === 'string' && d.trim()) {
254
+ // Control characters (a stray NUL above all: one NUL makes a text
255
+ // file invisible to every grep-based audit) are replaced, never
256
+ // carried. Filtered by code point rather than by a control-char
257
+ // regex so this file itself stays greppable.
258
+ const clean = [...d]
259
+ .map((ch) => { const c = ch.codePointAt(0); return c < 0x20 || c === 0x7f ? ' ' : ch; })
260
+ .join('')
261
+ .replace(/\s+/g, ' ')
262
+ .trim();
263
+ if (clean) description = clean.slice(0, AGENT_DESCRIPTION_MAX);
476
264
  }
477
-
478
- violations.push(line.trim().slice(0, CLAIM_TEXT_MAX));
265
+ } catch {
266
+ // absent or corrupt sidecar meta — the payload branch still stands
479
267
  }
480
- return { violations, undatedVerified };
481
- }
482
-
483
- // ---------------------------------------------------------------------------
484
- // payload helpers
485
- // ---------------------------------------------------------------------------
486
268
 
487
- /**
488
- * Pick the first non-empty trimmed string value from `input` across the given
489
- * candidate keys, in order. Returns `fallback` when none match. Mirrors the
490
- * helper in hooks/subagent-telemetry.mjs so the two hooks resolve session ids
491
- * identically (parent_session_id first).
492
- *
493
- * @param {object} input
494
- * @param {string[]} keys
495
- * @param {*} fallback
496
- * @returns {string|*}
497
- */
498
- function firstNonEmptyString(input, keys, fallback) {
499
- for (const k of keys) {
500
- const v = input[k];
501
- if (typeof v === 'string' && v.trim()) return v;
502
- }
503
- return fallback;
269
+ const agent = payloadAgent ?? metaAgent ?? 'unknown';
270
+ const source = payloadAgent !== null ? 'payload' : metaAgent !== null ? 'meta' : 'none';
271
+ // Keys only, never values: the diagnostic question is "what DID the harness
272
+ // send?", and a value could carry a path or prose that has no business in a
273
+ // record whose whole point is attribution.
274
+ const payloadKeys = input && typeof input === 'object' ? Object.keys(input).sort() : [];
275
+ return { agent, source, description, payloadKeys };
504
276
  }
505
277
 
506
278
  /**
@@ -524,23 +296,82 @@ function projectRootHash(projectRoot) {
524
296
  }
525
297
 
526
298
  /**
527
- * Build the dedup sentinel path for real project/session/agent contexts.
299
+ * Build the dedup sentinel path for real project/session/agent/claim contexts.
528
300
  * Missing fallback IDs intentionally return null so unrelated hooks/tests do
529
301
  * not collide on a global "unknown" key.
530
302
  *
303
+ * #1198 FIX 1: the key used to be `(projectRoot, sessionId, agent_type)` —
304
+ * `agent_type` is a CLASS ("discovery"), not an individual agent, so two
305
+ * DIFFERENT real subagents of the same type running in the same session
306
+ * collided on the same sentinel: the second agent's own `additionalContext`
307
+ * feedback was silently suppressed even though it never received a copy of
308
+ * the first agent's warning. Keying on `agentId` (the harness's per-process
309
+ * `agent_id`/`subagent_id`) instead removes that cross-agent collision. The
310
+ * claim-text hash is ADDITIVE: it lets a genuinely SECOND, DISTINCT claim
311
+ * from the same real agent still surface its own suppression check, rather
312
+ * than being silenced merely because that agent already triggered once for a
313
+ * different claim.
314
+ *
531
315
  * @param {object} opts
532
316
  * @param {string} opts.projectRoot
533
317
  * @param {string|null} opts.sessionId
534
- * @param {string|null} opts.agent
318
+ * @param {string|null} opts.agentId
319
+ * @param {string|null} opts.claimText
535
320
  * @returns {string|null}
536
321
  */
537
- function dedupSentinelPath({ projectRoot, sessionId, agent }) {
322
+ function dedupSentinelPath({ projectRoot, sessionId, agentId, claimText }) {
538
323
  if (typeof sessionId !== 'string' || !sessionId.trim()) return null;
539
- if (typeof agent !== 'string' || !agent.trim()) return null;
324
+ if (typeof agentId !== 'string' || !agentId.trim()) return null;
325
+ if (typeof claimText !== 'string' || !claimText.trim()) return null;
540
326
 
327
+ const claimHash = createHash('sha256').update(claimText).digest('hex').slice(0, 16);
541
328
  return path.join(
542
329
  tmpdir(),
543
- `psa006-${projectRootHash(projectRoot)}-${safeSentinelComponent(sessionId)}-${safeSentinelComponent(agent)}.lock`
330
+ `psa006-${projectRootHash(projectRoot)}-${safeSentinelComponent(sessionId)}-${safeSentinelComponent(agentId)}-${claimHash}.lock`
331
+ );
332
+ }
333
+
334
+ /**
335
+ * Build the SESSION-scoped LEDGER sentinel for one normalized claim (#1198).
336
+ *
337
+ * Distinct from `dedupSentinelPath` above in BOTH key and effect, and the
338
+ * difference is the point:
339
+ * - `dedupSentinelPath` is keyed on the individual AGENT and suppresses only
340
+ * the `additionalContext` echo, so each real agent still receives its own
341
+ * copy of the feedback.
342
+ * - this one is keyed on the SESSION and the NORMALIZED claim, and suppresses
343
+ * the events.jsonl WRITE. The ledger is a record of distinct findings; the
344
+ * same sentence re-asserted by a second agent of the same session is the
345
+ * same finding, and writing it twice is what produced a duplication factor
346
+ * of 16.4 (3,360 records over 205 distinct `claim_text` values, measured
347
+ * 2026-09-06 in `extern/aiat-barrierefrei-engine`).
348
+ *
349
+ * Returns null without a session id — a claim that cannot be bound to a session
350
+ * must not collide with an unrelated one under a global "unknown" key, so it is
351
+ * always written (the pre-#1198 behaviour, preserved for that case only).
352
+ *
353
+ * NAMED CEILING (BV-004): one zero-byte tmp file per distinct claim per
354
+ * session, never cleaned up by this hook — the same shape (and the same
355
+ * unbounded growth) `dedupSentinelPath` above has carried since #567, relying
356
+ * on the OS's tmpdir reaping. Fine at the measured rate: 158 distinct claims
357
+ * across a repo's worst 90 days. REVISIT TRIGGER: a repo whose distinct-claim
358
+ * count per session reaches the hundreds — then this belongs in a single
359
+ * per-session state file rather than one inode per claim.
360
+ *
361
+ * @param {object} opts
362
+ * @param {string} opts.projectRoot
363
+ * @param {string|null} opts.sessionId
364
+ * @param {string} opts.normalizedClaim — `normalizeClaim()` output
365
+ * @returns {string|null}
366
+ */
367
+ function claimLedgerSentinelPath({ projectRoot, sessionId, normalizedClaim }) {
368
+ if (typeof sessionId !== 'string' || !sessionId.trim()) return null;
369
+ if (typeof normalizedClaim !== 'string' || !normalizedClaim) return null;
370
+
371
+ const claimHash = createHash('sha256').update(normalizedClaim).digest('hex').slice(0, 16);
372
+ return path.join(
373
+ tmpdir(),
374
+ `psa006-claim-${projectRootHash(projectRoot)}-${safeSentinelComponent(sessionId)}-${claimHash}.lock`
544
375
  );
545
376
  }
546
377
 
@@ -555,32 +386,79 @@ async function main() {
555
386
 
556
387
  if (!(await isEnabled())) return;
557
388
 
558
- const text = await readTranscriptTail(input.transcript_path);
389
+ // #1191: scan the SUBAGENT's own transcript, never the parent. No agent_id
390
+ // (or no derivable path) → scan nothing and record nothing.
391
+ const agentId = firstNonEmptyString(input, ['agent_id', 'subagent_id'], null);
392
+ const sidecar = resolveAgentTranscriptPath(input, agentId);
393
+ if (sidecar === null) return;
394
+
395
+ const text = await readTranscriptTail(sidecar.transcript);
559
396
  const { violations, undatedVerified } = findViolations(text);
560
397
  if (violations.length === 0) return;
561
398
 
562
- const agentForDedup = firstNonEmptyString(input, ['agent_type'], null);
563
- const agent = agentForDedup ?? 'unknown';
399
+ const attribution = await resolveAgentAttribution(input, sidecar.meta);
400
+ const agent = attribution.agent;
564
401
  // session_id precedence: parent_session_id first, mirroring the sibling hook
565
402
  // hooks/subagent-telemetry.mjs (firstNonEmptyString(['parent_session_id',
566
403
  // 'session_id'])). W2-review LOW finding (#567) — the prior `session_id ||
567
404
  // parent_session_id` order disagreed with telemetry and could log the wrong id.
568
405
  const sessionId = firstNonEmptyString(input, ['parent_session_id', 'session_id'], null);
569
406
 
570
- // Project/session/agent deduplication: only emit additionalContext once for
571
- // repeated real contexts. Missing session IDs never create/read a sentinel,
572
- // so fallback traffic still surfaces warnings and cannot collide globally.
573
- const sentinel = dedupSentinelPath({ projectRoot: SO_PROJECT_DIR, sessionId, agent: agentForDedup });
407
+ // Project/session/agent/claim deduplication: only emit additionalContext
408
+ // once for repeated real contexts. Missing session IDs never create/read a
409
+ // sentinel, so fallback traffic still surfaces warnings and cannot collide
410
+ // globally. Keyed on agentId (not agent TYPE, #1198 FIX 1) plus the first
411
+ // violation's claim text so distinct real agents and distinct claims never
412
+ // share a sentinel.
413
+ const projectRoot = getProjectDir();
414
+ const sentinel = dedupSentinelPath({
415
+ projectRoot,
416
+ sessionId,
417
+ agentId,
418
+ claimText: violations[0].claim,
419
+ });
574
420
 
421
+ // Ledger write, ONE record per distinct normalized claim (#1198). Two
422
+ // levels, because the duplication had two sources: `findViolations()` already
423
+ // collapsed the repeats INSIDE this transcript tail into an `occurrences`
424
+ // count, and the session sentinel below drops a claim this session has
425
+ // already recorded from an earlier SubagentStop.
575
426
  const filePath = eventsFilePath();
576
- for (const claim of violations) {
427
+ let written = 0;
428
+ for (const violation of violations) {
429
+ const claimSentinel = claimLedgerSentinelPath({
430
+ projectRoot,
431
+ sessionId,
432
+ normalizedClaim: violation.normalized,
433
+ });
434
+ if (claimSentinel !== null) {
435
+ let seen = false;
436
+ try {
437
+ await fs.writeFile(claimSentinel, '', { flag: 'wx' });
438
+ } catch (err) {
439
+ // EEXIST = this session already recorded this claim. Any OTHER
440
+ // filesystem error must NOT suppress the record: losing a finding to a
441
+ // full disk or a read-only tmpdir is the worse failure.
442
+ seen = err && err.code === 'EEXIST';
443
+ }
444
+ if (seen) continue;
445
+ }
577
446
  await appendJsonl(filePath, {
578
447
  event: 'discovery_validator_violation',
579
448
  timestamp: new Date().toISOString(),
580
449
  agent,
450
+ // Always present, even (especially) on 'none': a bare "unknown" is
451
+ // indistinguishable from a real agent type of that name, and says
452
+ // nothing about WHY resolution failed.
453
+ agent_source: attribution.source,
454
+ ...(attribution.description !== null ? { agent_description: attribution.description } : {}),
455
+ ...(attribution.source === 'none' ? { payload_keys: attribution.payloadKeys } : {}),
456
+ ...(agentId !== null ? { agent_id: agentId } : {}),
581
457
  ...(sessionId !== null ? { session_id: sessionId } : {}),
582
- claim_text: claim,
458
+ claim_text: violation.claim,
459
+ occurrences: violation.occurrences,
583
460
  });
461
+ written++;
584
462
  }
585
463
 
586
464
  // Advisory only (#908 item 4) — never promoted to a violation in v1.
@@ -588,10 +466,18 @@ async function main() {
588
466
  ? ` ${undatedVerified} verified claim(s) carry no measurement timestamp (advisory).`
589
467
  : '';
590
468
 
469
+ // `written < violations.length` means this session had already recorded the
470
+ // difference from an earlier SubagentStop — say so rather than let the WARN
471
+ // count and the ledger count disagree with no explanation (#1198).
472
+ const suppressedNote = written < violations.length
473
+ ? ` ${violations.length - written} already recorded earlier in this session.`
474
+ : '';
475
+
591
476
  const warnText =
592
- `⚠ PSA-006: ${violations.length} repo-state/distributional claim(s) from agent "${agent}" ` +
593
- `lack an adjacent measurement transcript (grep/rg/find/git/wc/jq/ls/node/npm) (non-blocking).` +
594
- `${undatedNote} ` +
477
+ `⚠ PSA-006: ${violations.length} distinct repo-state/distributional claim(s) from agent ` +
478
+ `"${agent}" (source: ${attribution.source}) lack an adjacent measurement transcript ` +
479
+ `(grep/rg/find/git/wc/jq/ls/node/npm) (non-blocking).` +
480
+ `${suppressedNote}${undatedNote} ` +
595
481
  `See .claude/rules/parallel-sessions.md § PSA-006.`;
596
482
  process.stderr.write(warnText + '\n');
597
483