session-orchestrator 3.17.0 → 3.19.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 (187) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/rules/030-wave-execution.mdc +17 -1
  5. package/CHANGELOG.md +105 -412
  6. package/README.md +12 -9
  7. package/SECURITY.md +190 -27
  8. package/agents/AGENTS.md +20 -3
  9. package/agents/code-implementer.md +6 -6
  10. package/agents/db-specialist.md +1 -1
  11. package/agents/qa-strategist.md +31 -6
  12. package/agents/schemas/qa-strategist.schema.json +27 -0
  13. package/agents/schemas/test-writer.schema.json +60 -2
  14. package/agents/security-reviewer.md +1 -1
  15. package/agents/session-reviewer.md +1 -1
  16. package/agents/test-writer.md +29 -10
  17. package/agents/ui-developer.md +1 -1
  18. package/commands/contract-version-bump.md +28 -0
  19. package/commands/portfolio.md +1 -1
  20. package/docs/USER-GUIDE.md +8 -3
  21. package/docs/ci-setup.md +121 -7
  22. package/docs/codex-setup.md +1 -1
  23. package/docs/components.md +6 -6
  24. package/docs/cursor-setup.md +22 -9
  25. package/docs/events-schema.md +5 -1
  26. package/docs/instruction-delivery.md +444 -0
  27. package/docs/rule-authoring.md +58 -9
  28. package/docs/session-config-reference.md +244 -9
  29. package/docs/session-config-template.md +39 -3
  30. package/hooks/_lib/guard-source-loader.mjs +467 -0
  31. package/hooks/_lib/lock-bootstrap.mjs +21 -0
  32. package/hooks/_lib/vcs-create-matcher.mjs +119 -0
  33. package/hooks/config-protection.mjs +0 -0
  34. package/hooks/enforce-commands.mjs +10 -2
  35. package/hooks/hooks-codex.json +1 -1
  36. package/hooks/hooks-cursor.json +11 -2
  37. package/hooks/hooks-pi.json +10 -0
  38. package/hooks/hooks.json +21 -1
  39. package/hooks/on-session-end.mjs +178 -18
  40. package/hooks/on-session-start.mjs +23 -0
  41. package/hooks/post-bash-write-verify.mjs +977 -0
  42. package/hooks/post-subagent-discovery-validator.mjs +256 -41
  43. package/hooks/pre-bash-destructive-guard.mjs +525 -160
  44. package/hooks/pre-bash-issue-budget.mjs +167 -0
  45. package/hooks/pre-bash-sessions-ledger-guard.mjs +627 -0
  46. package/hooks/pre-bash-templates-first.mjs +96 -63
  47. package/hooks/subagent-telemetry.mjs +527 -37
  48. package/package.json +5 -2
  49. package/pi/prompts/contract-version-bump.md +12 -0
  50. package/rules/README.md +32 -0
  51. package/scripts/archive-closed-prds.mjs +12 -22
  52. package/scripts/autopilot-multi.mjs +103 -20
  53. package/scripts/backfill-abandoned-sessions.mjs +160 -4
  54. package/scripts/check-doc-consistency.sh +17 -1
  55. package/scripts/eval-session.mjs +50 -9
  56. package/scripts/fleet-instruction-scan.mjs +141 -0
  57. package/scripts/lib/autopilot/mr-draft.mjs +31 -1
  58. package/scripts/lib/autopilot/worktree-pipeline.mjs +113 -5
  59. package/scripts/lib/backlog-scan.mjs +39 -6
  60. package/scripts/lib/blocked-commands-policy.mjs +340 -0
  61. package/scripts/lib/ci-status-banner.mjs +75 -12
  62. package/scripts/lib/claude-md-budget-lint.mjs +283 -34
  63. package/scripts/lib/command-blocker.mjs +1013 -58
  64. package/scripts/lib/config/config-protection.mjs +2 -1
  65. package/scripts/lib/config/drift-check.mjs +9 -1
  66. package/scripts/lib/config/gitlab-portfolio.mjs +1 -1
  67. package/scripts/lib/config/issue-budget.mjs +123 -0
  68. package/scripts/lib/config/reconcile.mjs +21 -0
  69. package/scripts/lib/config/section-extractor.mjs +121 -1
  70. package/scripts/lib/config-schema.mjs +23 -3
  71. package/scripts/lib/config.mjs +17 -0
  72. package/scripts/lib/convergence-monitor.mjs +49 -3
  73. package/scripts/lib/description-surface.mjs +535 -0
  74. package/scripts/lib/dispatcher/enumerate.mjs +26 -40
  75. package/scripts/lib/ecosystem-wizard/config-writer.mjs +26 -24
  76. package/scripts/lib/ecosystem-wizard/wizard-prompt.mjs +1 -1
  77. package/scripts/lib/eval/engine.mjs +47 -5
  78. package/scripts/lib/events.mjs +59 -7
  79. package/scripts/lib/gates/gate-full.mjs +15 -3
  80. package/scripts/lib/gates/gate-helpers.mjs +132 -6
  81. package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +28 -8
  82. package/scripts/lib/gitlab-portfolio/aggregator.mjs +8 -2
  83. package/scripts/lib/gitlab-portfolio/cli.mjs +1 -1
  84. package/scripts/lib/handover-gate.mjs +7 -3
  85. package/scripts/lib/harness-audit/categories/category4.mjs +9 -3
  86. package/scripts/lib/instruction-budget-guard.mjs +402 -51
  87. package/scripts/lib/io.mjs +345 -10
  88. package/scripts/lib/issue-budget.mjs +269 -0
  89. package/scripts/lib/issue-close-strip-labels.mjs +39 -9
  90. package/scripts/lib/label-scope.mjs +47 -0
  91. package/scripts/lib/learnings/schema.mjs +43 -3
  92. package/scripts/lib/lock-reaper.mjs +1 -2
  93. package/scripts/lib/memory-proposals/schema.mjs +36 -1
  94. package/scripts/lib/peer-discovery.mjs +645 -0
  95. package/scripts/lib/pi-hook-bridge.mjs +146 -17
  96. package/scripts/lib/product-repo-detect.mjs +9 -8
  97. package/scripts/lib/project-hygiene.mjs +432 -0
  98. package/scripts/lib/quality-gate.mjs +167 -0
  99. package/scripts/lib/recommendations-v0.mjs +1 -1
  100. package/scripts/lib/reconcile/eligibility.mjs +1 -1
  101. package/scripts/lib/reconcile/emitter.mjs +23 -4
  102. package/scripts/lib/reconcile/engine.mjs +147 -39
  103. package/scripts/lib/reconcile/idempotency.mjs +114 -14
  104. package/scripts/lib/reconcile-nudge-banner.mjs +65 -9
  105. package/scripts/lib/resource-probe/evaluate.mjs +70 -4
  106. package/scripts/lib/resource-probe.mjs +19 -0
  107. package/scripts/lib/rule-loader.mjs +6 -0
  108. package/scripts/lib/scope-baseline.mjs +564 -0
  109. package/scripts/lib/scope-gate.mjs +399 -98
  110. package/scripts/lib/session-close-backfill.mjs +61 -6
  111. package/scripts/lib/session-end/phase-skip.mjs +1 -0
  112. package/scripts/lib/session-id.mjs +221 -41
  113. package/scripts/lib/session-lock.mjs +304 -6
  114. package/scripts/lib/session-schema/constants.mjs +22 -3
  115. package/scripts/lib/session-schema/validator.mjs +16 -0
  116. package/scripts/lib/sessions-integrity-banner.mjs +294 -0
  117. package/scripts/lib/sessions-staleness-banner.mjs +121 -12
  118. package/scripts/lib/skill-evolution/idempotency.mjs +135 -16
  119. package/scripts/lib/skill-evolution/mr-opener.mjs +9 -1
  120. package/scripts/lib/spiral-carryover.mjs +142 -30
  121. package/scripts/lib/state-md/mission-status.mjs +53 -3
  122. package/scripts/lib/subagents-schema.mjs +43 -9
  123. package/scripts/lib/test-runner/issue-reconcile.mjs +53 -13
  124. package/scripts/lib/tests-src-ratio.mjs +484 -0
  125. package/scripts/lib/validate/check-agents.mjs +56 -0
  126. package/scripts/lib/validate/check-hooks-symmetry.mjs +244 -10
  127. package/scripts/lib/validate/check-rules.mjs +217 -35
  128. package/scripts/lib/validate/check-test-value-bans.mjs +782 -0
  129. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  130. package/scripts/lib/validate-vendored-rules.mjs +10 -2
  131. package/scripts/lib/vault-archive.mjs +17 -2
  132. package/scripts/lib/vault-backfill/glab.mjs +8 -0
  133. package/scripts/lib/vault-mirror/process.mjs +30 -0
  134. package/scripts/lib/vault-mirror/render-sessions.mjs +293 -36
  135. package/scripts/lib/vcs-repo-spec.mjs +362 -0
  136. package/scripts/lib/wave-resource-gate.mjs +115 -11
  137. package/scripts/lib/worktree/listing.mjs +44 -7
  138. package/scripts/mcp-server.sh +17 -3
  139. package/scripts/measure-context-overhead.sh +151 -0
  140. package/scripts/memory-propose.mjs +72 -9
  141. package/scripts/print-applicable-rules.mjs +51 -12
  142. package/scripts/release.mjs +534 -0
  143. package/scripts/run-quality-gate.mjs +123 -5
  144. package/scripts/validate-wave-scope.mjs +182 -17
  145. package/scripts/vault-integration-watcher.mjs +32 -10
  146. package/skills/_shared/config-reading.md +2 -2
  147. package/skills/bootstrap/fast-template.md +1 -1
  148. package/skills/claude-md-drift-check/checker.mjs +145 -28
  149. package/skills/contract-version-bump/SKILL.md +219 -0
  150. package/skills/discovery/SKILL.md +4 -4
  151. package/skills/discovery/issue-templates.md +11 -11
  152. package/skills/discovery/probes-audit.md +1 -1
  153. package/skills/discovery/probes-feature.md +1 -1
  154. package/skills/discovery/probes-session.md +26 -5
  155. package/skills/ecosystem-health/SKILL.md +1 -1
  156. package/skills/ecosystem-health/wizard.md +4 -4
  157. package/skills/evolve/SKILL.md +1 -0
  158. package/skills/gitlab-ops/SKILL.md +20 -12
  159. package/skills/gitlab-portfolio/SKILL.md +2 -2
  160. package/skills/hook-development/SKILL.md +1 -1
  161. package/skills/mode-selector/SKILL.md +1 -1
  162. package/skills/npm-publish/SKILL.md +17 -1
  163. package/skills/plan/SKILL.md +5 -5
  164. package/skills/plan/mode-feature.md +4 -4
  165. package/skills/plan/mode-new.md +10 -10
  166. package/skills/plan/mode-retro.md +1 -1
  167. package/skills/quality-gates/SKILL.md +1 -1
  168. package/skills/reconcile/SKILL.md +21 -4
  169. package/skills/session-end/SKILL.md +34 -13
  170. package/skills/session-end/discovery-scan.md +4 -2
  171. package/skills/session-end/drift-operations.md +4 -4
  172. package/skills/session-end/metrics-collection.md +13 -0
  173. package/skills/session-end/phase-3-2-docs-verification.md +1 -1
  174. package/skills/session-end/phase-3-6-tail.md +2 -1
  175. package/skills/session-end/plan-verification.md +5 -2
  176. package/skills/session-end/vault-operations.md +1 -1
  177. package/skills/session-end/verification-checklist.md +1 -1
  178. package/skills/session-plan/SKILL.md +6 -2
  179. package/skills/session-plan/wave-template.md +2 -0
  180. package/skills/session-start/SKILL.md +73 -7
  181. package/skills/session-start/phase-4-5-resource-health.md +15 -2
  182. package/skills/test-runner/SKILL.md +2 -2
  183. package/skills/vault-sync/validator.mjs +108 -7
  184. package/skills/wave-executor/SKILL.md +5 -2
  185. package/skills/wave-executor/circuit-breaker.md +2 -0
  186. package/skills/wave-executor/wave-loop.md +163 -10
  187. package/templates/_shared/loop.md +4 -4
@@ -3,8 +3,9 @@
3
3
  * post-subagent-discovery-validator.mjs — SubagentStop hook that mechanically
4
4
  * enforces PSA-006 (distributional claims need adjacent grep transcripts).
5
5
  *
6
- * Issue #567. Non-blocking v1 (log + warn only). EXIT 0 ALWAYS — exit 2
7
- * (blocking) is RESERVED for a future hard-gate and MUST NOT be used here.
6
+ * Issue #567 (v1), Issue #908 (repo-state facts). Non-blocking (log + warn
7
+ * only). EXIT 0 ALWAYS — exit 2 (blocking) is RESERVED for a future hard-gate
8
+ * and MUST NOT be used here.
8
9
  *
9
10
  * Decision flow:
10
11
  * 1. shouldRunHook('post-subagent-discovery-validator') gate — exit 0 when disabled.
@@ -13,10 +14,18 @@
13
14
  * Default OFF — exit 0 immediately unless explicitly enabled.
14
15
  * 4. Read `input.transcript_path` (whole-session JSONL of assistant/user records),
15
16
  * scan the TAIL (last ~8 `type:"assistant"` records), concat text blocks.
16
- * 5. Regex-scan the concatenated text for 6 distributional-claim patterns.
17
- * 6. For each match, check whether a fenced ```bash block containing grep/rg/find
18
- * appears within ±5 lines. If a claim has NO adjacent grep block → record a
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
19
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.
20
29
  *
21
30
  * Why read the transcript: the SubagentStop stdin payload has NO output_text
22
31
  * field. The agent's text lives in `input.transcript_path`.
@@ -61,29 +70,166 @@ const CLAIM_TEXT_MAX = 200;
61
70
  */
62
71
  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?)';
63
72
 
73
+ /**
74
+ * Repo-STATE nouns (#908). The four documented #908 drift cases were counts of
75
+ * repository state, not of code locations: "14 commits", "92 learnings",
76
+ * "5 dirty files", "412 lines". None of them contains a CTX noun, so the
77
+ * original six patterns could not see them.
78
+ *
79
+ * Vocabulary taken from this repo's own artefacts (`.orchestrator/metrics/*.jsonl`
80
+ * record kinds, `.claude/rules/`, `skills/`, `agents/`, `hooks/`) rather than a
81
+ * generic English list — a noun that never names a countable repo artefact here
82
+ * only buys false positives.
83
+ */
84
+ const STATE = '(?:commits?|learnings?|issues?|branches?|lines?|entries|records?|sessions?|rules?|skills?|probes?|waves?|proposals?|worktrees?)';
85
+
86
+ /**
87
+ * The noun class the BARE-CARDINAL pattern may use — deliberately a strict
88
+ * subset of STATE ∪ CTX, restricted to the artefact kinds the #908 drift was
89
+ * actually measured in (commit counts, learnings counts, open-issue counts,
90
+ * branch counts, line counts, dirty-file counts).
91
+ *
92
+ * Measured, not guessed: over 32 real agent-stop windows from this repo's own
93
+ * transcripts, admitting the full CTX ∪ STATE set fired 93 times (2.9 per
94
+ * stop — the "validator gets switched off" zone). Every noun below earns its
95
+ * place by naming one of the documented #908 facts; the ones that only cost
96
+ * false positives (`tests`, `references`, `matches`, `agents`, `files` without
97
+ * a state adjective) are excluded here and remain reachable through the six
98
+ * quantifier-triggered patterns above, which have a lexical anchor.
99
+ *
100
+ * `callers` (#918): PSA-006's own canonical noun — the rule text's worked
101
+ * examples ("4 of 4 callers", "100% of callers opt-in") all count callers, yet
102
+ * the cardinal pattern could not see a bare "14 callers". Re-measured with it
103
+ * admitted: 490 real SubagentStop transcripts (2026-07-31), +0 additional
104
+ * firings from the bare form — the noun is free on this corpus (all +3 delta
105
+ * firings came from the #918 slash pattern below).
106
+ */
107
+ const CARDINAL_NOUN = '(?:commits?|learnings?|issues?|branches?|lines?|files?|callers?)';
108
+
109
+ /**
110
+ * Wide noun class = code-distribution nouns ∪ repo-state nouns. Used by the six
111
+ * QUANTIFIER-triggered patterns ("N of M", "100% of", "all N", "no remaining",
112
+ * "none of") — each of those carries a strong lexical trigger, so widening the
113
+ * noun set there is low-risk.
114
+ *
115
+ * Deliberately NOT used by the `every <noun>` pattern: `every` has no numeric
116
+ * anchor, so `every commit must be signed` / `every rule is always-on` are
117
+ * ordinary prose, not measured claims. That pattern keeps the narrow CTX.
118
+ */
119
+ const WIDE = `(?:${CTX}|${STATE})`;
120
+
64
121
  /** Bounded same-line gap between a trigger and its context noun. */
65
122
  const CTX_GAP = '[^\\n]{0,40}?';
66
123
 
67
124
  /**
68
125
  * Distributional-claim patterns (case-insensitive). A match is a PSA-006 claim
69
- * that requires an adjacent grep/rg/find transcript.
126
+ * that requires an adjacent measurement transcript.
70
127
  *
71
- * Each pattern requires a code-distribution context noun (CTX) within a small,
72
- * bounded same-line window of the trigger. The `[^\n]{0,40}?` gap is a bounded
73
- * lazy character class (linear-time — the ReDoS-safety the W2 reviewer verified
74
- * is preserved). True claims ("4 of 4 callers opt-in", "every caller imports X",
75
- * "no remaining references to Y") still flag; benign strings ("Turn 3 of 25
76
- * complete", "every developer should test", "100% of users love it") do not.
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). True
131
+ * claims ("4 of 4 callers opt-in", "every caller imports X", "no remaining
132
+ * references to Y") still flag; benign strings ("Turn 3 of 25 complete",
133
+ * "every developer should test", "100% of users love it") do not.
77
134
  */
78
135
  const CLAIM_PATTERNS = [
79
- new RegExp(`\\b\\d+ of \\d+\\b${CTX_GAP}\\b${CTX}\\b`, 'i'),
80
- new RegExp(`100% of\\b${CTX_GAP}\\b${CTX}\\b`, 'i'),
81
- new RegExp(`\\ball \\d+\\b${CTX_GAP}\\b${CTX}\\b`, 'i'),
82
- new RegExp(`no remaining\\b${CTX_GAP}\\b${CTX}\\b`, 'i'),
136
+ new RegExp(`\\b\\d+ of \\d+\\b${CTX_GAP}\\b${WIDE}\\b`, 'i'),
137
+ new RegExp(`100% of\\b${CTX_GAP}\\b${WIDE}\\b`, 'i'),
138
+ new RegExp(`\\ball \\d+\\b${CTX_GAP}\\b${WIDE}\\b`, 'i'),
139
+ new RegExp(`no remaining\\b${CTX_GAP}\\b${WIDE}\\b`, 'i'),
83
140
  new RegExp(`every ${CTX}\\b`, 'i'),
84
- new RegExp(`none of\\b${CTX_GAP}\\b${CTX}\\b`, 'i'),
141
+ new RegExp(`none of\\b${CTX_GAP}\\b${WIDE}\\b`, 'i'),
85
142
  ];
86
143
 
144
+ /**
145
+ * Pattern 7 (#908) — the BARE CARDINAL repo-state fact. `14 commits` has no
146
+ * quantifier trigger at all, which is exactly why the #908 drift went unseen.
147
+ *
148
+ * A naive `\d+` would fire on every issue reference, version literal, date,
149
+ * line number and percentage in a normal report — and a validator that fires on
150
+ * every report gets switched off, which is strictly worse than no validator.
151
+ * Precision is therefore bought three ways:
152
+ *
153
+ * 1. TRIGGER: a digit run that is not glued to identifier punctuation.
154
+ * Lookbehind rejects `#906`, `v3`, `PSA-006`, `W2`, `foo.mjs:123`;
155
+ * lookahead rejects `3.17`, `70%`, `2026-07-29`, `12615/0`.
156
+ * 2. GAP: at most two intervening ADJECTIVE-like words, and never a
157
+ * preposition/article/copula. "5 dirty files" matches; "3 of 5 stars" and
158
+ * "2 sections below the rules" do not.
159
+ * 3. SCOPE: evaluated only on prose lines with inline-code spans masked out —
160
+ * fenced blocks are skipped entirely (see `findViolations`), because a
161
+ * number inside a fence is tool OUTPUT (the evidence itself), not an
162
+ * unverified assertion about it.
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
+ 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". PSA-006 literally demands "Quote the numerator
175
+ * AND denominator", and `N/M` is the notation that demand produces — yet the
176
+ * cardinal trigger's lookahead `(?![\d.%:/-])` excludes the slash, making the
177
+ * rule's own canon form structurally invisible (#918).
178
+ *
179
+ * Admitting the slash re-opens the `12615/0/11` gate-summary class the
180
+ * lookahead exists to suppress, so the slash form gets its OWN pattern with a
181
+ * STRICTER contract than the bare cardinal:
182
+ *
183
+ * 1. RATIO: exactly two slash-joined numbers. Both boundary guards stay:
184
+ * the lookbehind rejects a numerator glued to a path/id (`hooks/12/14`),
185
+ * the lookahead after the DENOMINATOR rejects a third slash-segment —
186
+ * `12615/0/11` fails twice over (denominator `0` is followed by `/`, and
187
+ * the trailing pair `0/11` has a `/`-glued numerator).
188
+ * 2. NOUN IMMEDIATELY AFTER — no CARDINAL_GAP. The noun-after-denominator is
189
+ * the discriminator that separates a measured ratio ("12/14 files") from
190
+ * a bare slash pair that is a date or score ("on 12/14 we shipped",
191
+ * "rated 3/5 overall"): those are followed by anything BUT an artefact
192
+ * noun. Widening to the gapped form would admit US-date + adjective +
193
+ * noun collisions with no PSA-006 payoff.
194
+ *
195
+ * FP re-measured on the CORRECT text sort (#918 requirement — the prior 1.07
196
+ * rate was measured on coordinator narration, not on what this hook reads):
197
+ * 490 real SubagentStop subagent transcripts (`~/.claude/projects/<slug>/<session>/
198
+ * subagents/*.jsonl`), spawning THIS hook per transcript, 2026-07-31.
199
+ * Baseline (pre-#918): 345 firings / 0.7041 per stop. With slash form +
200
+ * `callers` admitted: 348 / 0.7102 — +3 firings, of which 1 is a true positive
201
+ * ("0/49 Learnings mit Allow-List-Typ", an unverified canon-form claim), 1
202
+ * quotes the #918 example sentence itself (mention-not-use), and 1 is a
203
+ * before/after line-count pair ("858/857 lines", class-consistent with the
204
+ * bare "412 lines" behaviour of pattern 7).
205
+ *
206
+ * Bounded quantifiers only ({1,9}) — linear-time, ReDoS-safe.
207
+ */
208
+ const CARDINAL_RATIO_PATTERN = new RegExp(
209
+ `(?<![\\w#$:/.-])\\d{1,9}/\\d{1,9}(?![\\d.%:/-])\\s+${CARDINAL_NOUN}\\b`,
210
+ 'i'
211
+ );
212
+
213
+ /** Inline-code spans are masked before the cardinal pattern runs. */
214
+ const INLINE_CODE_RE = /`[^`\n]*`/g;
215
+
216
+ /**
217
+ * Commands that count as a MEASUREMENT inside a fenced block. `grep|rg|find`
218
+ * (the #567 set) only covers text search; the #908 facts are measured with
219
+ * `git log --oneline | wc -l`, `jq` over a JSONL metrics file, `ls | wc -l`,
220
+ * or a `node`/`npm` script. Refusing to recognise those made the honest,
221
+ * evidence-quoting path fail verification.
222
+ */
223
+ const MEASUREMENT_CMD_RE = /\b(grep|rg|find|git|wc|jq|ls|node|npm)\b/;
224
+
225
+ /**
226
+ * Markers that date a measurement (#908 Baustein 2 input). ADVISORY in v1:
227
+ * an undated-but-verified claim is counted and reported, never a violation —
228
+ * a hard contract without an established authoring habit buys friction, not
229
+ * accuracy. Baustein 2 can escalate this to a violation once the habit exists.
230
+ */
231
+ 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;
232
+
87
233
  // ---------------------------------------------------------------------------
88
234
  // stdin reading (inline — Stop-family hooks exit 0 always, never deny)
89
235
  // ---------------------------------------------------------------------------
@@ -192,15 +338,23 @@ async function readTranscriptTail(transcriptPath) {
192
338
  // ---------------------------------------------------------------------------
193
339
 
194
340
  /**
195
- * Identify the 0-based line indices that open or close a fenced ```bash (or ```sh
196
- * / bare ```) block whose body contains a grep/rg/find invocation. Returns the
197
- * set of line indices that belong to such a verification block.
341
+ * Single fence walk. Returns two disjoint-purpose index sets:
342
+ * - `measurementLines`: lines belonging to a fenced block whose body contains
343
+ * a MEASUREMENT_CMD_RE invocation (the evidence a claim can lean on).
344
+ * - `fencedLines`: lines belonging to ANY fenced block (evidence or not).
345
+ * Used only to keep the greedy #908 cardinal pattern out of tool output;
346
+ * the six quantifier patterns are unchanged and still scan fenced lines.
347
+ *
348
+ * An unterminated trailing fence is treated as fenced-to-EOF (conservative for
349
+ * false-positive suppression) but never as a measurement block (its body was
350
+ * never closed, so we cannot claim it verified anything).
198
351
  *
199
352
  * @param {string[]} lines
200
- * @returns {Set<number>} indices of lines inside a grep/rg/find fenced block
353
+ * @returns {{ measurementLines: Set<number>, fencedLines: Set<number> }}
201
354
  */
202
- function grepBlockLineIndices(lines) {
203
- const indices = new Set();
355
+ function scanFences(lines) {
356
+ const measurementLines = new Set();
357
+ const fencedLines = new Set();
204
358
  let fenceStart = -1;
205
359
  let buffer = [];
206
360
  let inFence = false;
@@ -215,8 +369,10 @@ function grepBlockLineIndices(lines) {
215
369
  if (isFence) {
216
370
  // closing fence — evaluate the buffered body
217
371
  const body = buffer.join('\n');
218
- if (/\b(grep|rg|find)\b/.test(body)) {
219
- for (let j = fenceStart; j <= i; j++) indices.add(j);
372
+ const isMeasurement = MEASUREMENT_CMD_RE.test(body);
373
+ for (let j = fenceStart; j <= i; j++) {
374
+ fencedLines.add(j);
375
+ if (isMeasurement) measurementLines.add(j);
220
376
  }
221
377
  inFence = false;
222
378
  fenceStart = -1;
@@ -225,37 +381,90 @@ function grepBlockLineIndices(lines) {
225
381
  buffer.push(lines[i]);
226
382
  }
227
383
  }
228
- return indices;
384
+
385
+ // Unterminated trailing fence — suppress cardinal matches, grant no evidence.
386
+ if (inFence) {
387
+ for (let j = fenceStart; j < lines.length; j++) fencedLines.add(j);
388
+ }
389
+
390
+ // INLINE-code evidence: PSA-006 asks for "the exact pattern executed" to be
391
+ // quoted — an inline `git log --oneline | wc -l` satisfies that exactly as
392
+ // well as a fenced block, and one-line findings are commonly written that
393
+ // way. Refusing to count it would penalise the honest path.
394
+ for (let i = 0; i < lines.length; i++) {
395
+ if (fencedLines.has(i)) continue;
396
+ for (const span of lines[i].match(INLINE_CODE_RE) ?? []) {
397
+ if (MEASUREMENT_CMD_RE.test(span)) { measurementLines.add(i); break; }
398
+ }
399
+ }
400
+
401
+ return { measurementLines, fencedLines };
229
402
  }
230
403
 
231
404
  /**
232
- * Scan concatenated transcript text for distributional claims lacking an
233
- * adjacent grep/rg/find fenced block (within ±GREP_PROXIMITY_LINES).
405
+ * True when any line in `indices` sits within ±GREP_PROXIMITY_LINES of `i`.
406
+ *
407
+ * @param {Set<number>} indices
408
+ * @param {number} i
409
+ * @returns {boolean}
410
+ */
411
+ function nearIndex(indices, i) {
412
+ for (let j = i - GREP_PROXIMITY_LINES; j <= i + GREP_PROXIMITY_LINES; j++) {
413
+ if (indices.has(j)) return true;
414
+ }
415
+ return false;
416
+ }
417
+
418
+ /**
419
+ * True when a measurement TIMESTAMP marker appears within the same proximity
420
+ * window used for the measurement block itself.
421
+ *
422
+ * @param {string[]} lines
423
+ * @param {number} i
424
+ * @returns {boolean}
425
+ */
426
+ function hasMeasurementTimestamp(lines, i) {
427
+ const from = Math.max(0, i - GREP_PROXIMITY_LINES);
428
+ const to = Math.min(lines.length - 1, i + GREP_PROXIMITY_LINES);
429
+ for (let j = from; j <= to; j++) {
430
+ if (TIMESTAMP_MARKER_RE.test(lines[j])) return true;
431
+ }
432
+ return false;
433
+ }
434
+
435
+ /**
436
+ * Scan concatenated transcript text for claims lacking an adjacent measurement
437
+ * block (within ±GREP_PROXIMITY_LINES).
234
438
  *
235
439
  * @param {string} text
236
- * @returns {string[]} truncated claim-text snippets for each violation
440
+ * @returns {{ violations: string[], undatedVerified: number }}
441
+ * `violations` — truncated claim-text snippets; `undatedVerified` — count of
442
+ * claims that ARE verified but carry no measurement timestamp (advisory).
237
443
  */
238
444
  function findViolations(text) {
239
- if (!text) return [];
445
+ if (!text) return { violations: [], undatedVerified: 0 };
240
446
  const lines = text.split(/\r?\n/);
241
- const grepLines = grepBlockLineIndices(lines);
447
+ const { measurementLines, fencedLines } = scanFences(lines);
242
448
  const violations = [];
449
+ let undatedVerified = 0;
243
450
 
244
451
  for (let i = 0; i < lines.length; i++) {
245
452
  const line = lines[i];
246
- const matched = CLAIM_PATTERNS.some((re) => re.test(line));
453
+ let matched = CLAIM_PATTERNS.some((re) => re.test(line));
454
+ if (!matched && !fencedLines.has(i)) {
455
+ const masked = line.replace(INLINE_CODE_RE, ' ');
456
+ matched = CARDINAL_PATTERN.test(masked) || CARDINAL_RATIO_PATTERN.test(masked);
457
+ }
247
458
  if (!matched) continue;
248
459
 
249
- // A claim is verified if any grep-block line sits within the proximity window.
250
- let verified = false;
251
- for (let j = i - GREP_PROXIMITY_LINES; j <= i + GREP_PROXIMITY_LINES; j++) {
252
- if (grepLines.has(j)) { verified = true; break; }
460
+ if (nearIndex(measurementLines, i)) {
461
+ if (!hasMeasurementTimestamp(lines, i)) undatedVerified++;
462
+ continue;
253
463
  }
254
- if (verified) continue;
255
464
 
256
465
  violations.push(line.trim().slice(0, CLAIM_TEXT_MAX));
257
466
  }
258
- return violations;
467
+ return { violations, undatedVerified };
259
468
  }
260
469
 
261
470
  // ---------------------------------------------------------------------------
@@ -334,7 +543,7 @@ async function main() {
334
543
  if (!(await isEnabled())) return;
335
544
 
336
545
  const text = await readTranscriptTail(input.transcript_path);
337
- const violations = findViolations(text);
546
+ const { violations, undatedVerified } = findViolations(text);
338
547
  if (violations.length === 0) return;
339
548
 
340
549
  const agentForDedup = firstNonEmptyString(input, ['agent_type'], null);
@@ -361,9 +570,15 @@ async function main() {
361
570
  });
362
571
  }
363
572
 
573
+ // Advisory only (#908 item 4) — never promoted to a violation in v1.
574
+ const undatedNote = undatedVerified > 0
575
+ ? ` ${undatedVerified} verified claim(s) carry no measurement timestamp (advisory).`
576
+ : '';
577
+
364
578
  const warnText =
365
- `⚠ PSA-006: ${violations.length} distributional claim(s) from agent "${agent}" ` +
366
- `lack an adjacent grep/rg/find transcript (non-blocking). ` +
579
+ `⚠ PSA-006: ${violations.length} repo-state/distributional claim(s) from agent "${agent}" ` +
580
+ `lack an adjacent measurement transcript (grep/rg/find/git/wc/jq/ls/node/npm) (non-blocking).` +
581
+ `${undatedNote} ` +
367
582
  `See .claude/rules/parallel-sessions.md § PSA-006.`;
368
583
  process.stderr.write(warnText + '\n');
369
584