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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/030-wave-execution.mdc +17 -1
- package/CHANGELOG.md +105 -412
- package/README.md +12 -9
- package/SECURITY.md +190 -27
- package/agents/AGENTS.md +20 -3
- package/agents/code-implementer.md +6 -6
- package/agents/db-specialist.md +1 -1
- package/agents/qa-strategist.md +31 -6
- package/agents/schemas/qa-strategist.schema.json +27 -0
- package/agents/schemas/test-writer.schema.json +60 -2
- package/agents/security-reviewer.md +1 -1
- package/agents/session-reviewer.md +1 -1
- package/agents/test-writer.md +29 -10
- package/agents/ui-developer.md +1 -1
- package/commands/contract-version-bump.md +28 -0
- package/commands/portfolio.md +1 -1
- package/docs/USER-GUIDE.md +8 -3
- package/docs/ci-setup.md +121 -7
- package/docs/codex-setup.md +1 -1
- package/docs/components.md +6 -6
- package/docs/cursor-setup.md +22 -9
- package/docs/events-schema.md +5 -1
- package/docs/instruction-delivery.md +444 -0
- package/docs/rule-authoring.md +58 -9
- package/docs/session-config-reference.md +244 -9
- package/docs/session-config-template.md +39 -3
- package/hooks/_lib/guard-source-loader.mjs +467 -0
- package/hooks/_lib/lock-bootstrap.mjs +21 -0
- package/hooks/_lib/vcs-create-matcher.mjs +119 -0
- package/hooks/config-protection.mjs +0 -0
- package/hooks/enforce-commands.mjs +10 -2
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +11 -2
- package/hooks/hooks-pi.json +10 -0
- package/hooks/hooks.json +21 -1
- package/hooks/on-session-end.mjs +178 -18
- package/hooks/on-session-start.mjs +23 -0
- package/hooks/post-bash-write-verify.mjs +977 -0
- package/hooks/post-subagent-discovery-validator.mjs +256 -41
- package/hooks/pre-bash-destructive-guard.mjs +525 -160
- package/hooks/pre-bash-issue-budget.mjs +167 -0
- package/hooks/pre-bash-sessions-ledger-guard.mjs +627 -0
- package/hooks/pre-bash-templates-first.mjs +96 -63
- package/hooks/subagent-telemetry.mjs +527 -37
- package/package.json +5 -2
- package/pi/prompts/contract-version-bump.md +12 -0
- package/rules/README.md +32 -0
- package/scripts/archive-closed-prds.mjs +12 -22
- package/scripts/autopilot-multi.mjs +103 -20
- package/scripts/backfill-abandoned-sessions.mjs +160 -4
- package/scripts/check-doc-consistency.sh +17 -1
- package/scripts/eval-session.mjs +50 -9
- package/scripts/fleet-instruction-scan.mjs +141 -0
- package/scripts/lib/autopilot/mr-draft.mjs +31 -1
- package/scripts/lib/autopilot/worktree-pipeline.mjs +113 -5
- package/scripts/lib/backlog-scan.mjs +39 -6
- package/scripts/lib/blocked-commands-policy.mjs +340 -0
- package/scripts/lib/ci-status-banner.mjs +75 -12
- package/scripts/lib/claude-md-budget-lint.mjs +283 -34
- package/scripts/lib/command-blocker.mjs +1013 -58
- package/scripts/lib/config/config-protection.mjs +2 -1
- package/scripts/lib/config/drift-check.mjs +9 -1
- package/scripts/lib/config/gitlab-portfolio.mjs +1 -1
- package/scripts/lib/config/issue-budget.mjs +123 -0
- package/scripts/lib/config/reconcile.mjs +21 -0
- package/scripts/lib/config/section-extractor.mjs +121 -1
- package/scripts/lib/config-schema.mjs +23 -3
- package/scripts/lib/config.mjs +17 -0
- package/scripts/lib/convergence-monitor.mjs +49 -3
- package/scripts/lib/description-surface.mjs +535 -0
- package/scripts/lib/dispatcher/enumerate.mjs +26 -40
- package/scripts/lib/ecosystem-wizard/config-writer.mjs +26 -24
- package/scripts/lib/ecosystem-wizard/wizard-prompt.mjs +1 -1
- package/scripts/lib/eval/engine.mjs +47 -5
- package/scripts/lib/events.mjs +59 -7
- package/scripts/lib/gates/gate-full.mjs +15 -3
- package/scripts/lib/gates/gate-helpers.mjs +132 -6
- package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +28 -8
- package/scripts/lib/gitlab-portfolio/aggregator.mjs +8 -2
- package/scripts/lib/gitlab-portfolio/cli.mjs +1 -1
- package/scripts/lib/handover-gate.mjs +7 -3
- package/scripts/lib/harness-audit/categories/category4.mjs +9 -3
- package/scripts/lib/instruction-budget-guard.mjs +402 -51
- package/scripts/lib/io.mjs +345 -10
- package/scripts/lib/issue-budget.mjs +269 -0
- package/scripts/lib/issue-close-strip-labels.mjs +39 -9
- package/scripts/lib/label-scope.mjs +47 -0
- package/scripts/lib/learnings/schema.mjs +43 -3
- package/scripts/lib/lock-reaper.mjs +1 -2
- package/scripts/lib/memory-proposals/schema.mjs +36 -1
- package/scripts/lib/peer-discovery.mjs +645 -0
- package/scripts/lib/pi-hook-bridge.mjs +146 -17
- package/scripts/lib/product-repo-detect.mjs +9 -8
- package/scripts/lib/project-hygiene.mjs +432 -0
- package/scripts/lib/quality-gate.mjs +167 -0
- package/scripts/lib/recommendations-v0.mjs +1 -1
- package/scripts/lib/reconcile/eligibility.mjs +1 -1
- package/scripts/lib/reconcile/emitter.mjs +23 -4
- package/scripts/lib/reconcile/engine.mjs +147 -39
- package/scripts/lib/reconcile/idempotency.mjs +114 -14
- package/scripts/lib/reconcile-nudge-banner.mjs +65 -9
- package/scripts/lib/resource-probe/evaluate.mjs +70 -4
- package/scripts/lib/resource-probe.mjs +19 -0
- package/scripts/lib/rule-loader.mjs +6 -0
- package/scripts/lib/scope-baseline.mjs +564 -0
- package/scripts/lib/scope-gate.mjs +399 -98
- package/scripts/lib/session-close-backfill.mjs +61 -6
- package/scripts/lib/session-end/phase-skip.mjs +1 -0
- package/scripts/lib/session-id.mjs +221 -41
- package/scripts/lib/session-lock.mjs +304 -6
- package/scripts/lib/session-schema/constants.mjs +22 -3
- package/scripts/lib/session-schema/validator.mjs +16 -0
- package/scripts/lib/sessions-integrity-banner.mjs +294 -0
- package/scripts/lib/sessions-staleness-banner.mjs +121 -12
- package/scripts/lib/skill-evolution/idempotency.mjs +135 -16
- package/scripts/lib/skill-evolution/mr-opener.mjs +9 -1
- package/scripts/lib/spiral-carryover.mjs +142 -30
- package/scripts/lib/state-md/mission-status.mjs +53 -3
- package/scripts/lib/subagents-schema.mjs +43 -9
- package/scripts/lib/test-runner/issue-reconcile.mjs +53 -13
- package/scripts/lib/tests-src-ratio.mjs +484 -0
- package/scripts/lib/validate/check-agents.mjs +56 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +244 -10
- package/scripts/lib/validate/check-rules.mjs +217 -35
- package/scripts/lib/validate/check-test-value-bans.mjs +782 -0
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate-vendored-rules.mjs +10 -2
- package/scripts/lib/vault-archive.mjs +17 -2
- package/scripts/lib/vault-backfill/glab.mjs +8 -0
- package/scripts/lib/vault-mirror/process.mjs +30 -0
- package/scripts/lib/vault-mirror/render-sessions.mjs +293 -36
- package/scripts/lib/vcs-repo-spec.mjs +362 -0
- package/scripts/lib/wave-resource-gate.mjs +115 -11
- package/scripts/lib/worktree/listing.mjs +44 -7
- package/scripts/mcp-server.sh +17 -3
- package/scripts/measure-context-overhead.sh +151 -0
- package/scripts/memory-propose.mjs +72 -9
- package/scripts/print-applicable-rules.mjs +51 -12
- package/scripts/release.mjs +534 -0
- package/scripts/run-quality-gate.mjs +123 -5
- package/scripts/validate-wave-scope.mjs +182 -17
- package/scripts/vault-integration-watcher.mjs +32 -10
- package/skills/_shared/config-reading.md +2 -2
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/claude-md-drift-check/checker.mjs +145 -28
- package/skills/contract-version-bump/SKILL.md +219 -0
- package/skills/discovery/SKILL.md +4 -4
- package/skills/discovery/issue-templates.md +11 -11
- package/skills/discovery/probes-audit.md +1 -1
- package/skills/discovery/probes-feature.md +1 -1
- package/skills/discovery/probes-session.md +26 -5
- package/skills/ecosystem-health/SKILL.md +1 -1
- package/skills/ecosystem-health/wizard.md +4 -4
- package/skills/evolve/SKILL.md +1 -0
- package/skills/gitlab-ops/SKILL.md +20 -12
- package/skills/gitlab-portfolio/SKILL.md +2 -2
- package/skills/hook-development/SKILL.md +1 -1
- package/skills/mode-selector/SKILL.md +1 -1
- package/skills/npm-publish/SKILL.md +17 -1
- package/skills/plan/SKILL.md +5 -5
- package/skills/plan/mode-feature.md +4 -4
- package/skills/plan/mode-new.md +10 -10
- package/skills/plan/mode-retro.md +1 -1
- package/skills/quality-gates/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +21 -4
- package/skills/session-end/SKILL.md +34 -13
- package/skills/session-end/discovery-scan.md +4 -2
- package/skills/session-end/drift-operations.md +4 -4
- package/skills/session-end/metrics-collection.md +13 -0
- package/skills/session-end/phase-3-2-docs-verification.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +2 -1
- package/skills/session-end/plan-verification.md +5 -2
- package/skills/session-end/vault-operations.md +1 -1
- package/skills/session-end/verification-checklist.md +1 -1
- package/skills/session-plan/SKILL.md +6 -2
- package/skills/session-plan/wave-template.md +2 -0
- package/skills/session-start/SKILL.md +73 -7
- package/skills/session-start/phase-4-5-resource-health.md +15 -2
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/vault-sync/validator.mjs +108 -7
- package/skills/wave-executor/SKILL.md +5 -2
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/wave-loop.md +163 -10
- 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
|
|
7
|
-
* (blocking) is RESERVED for a future hard-gate
|
|
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
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
126
|
+
* that requires an adjacent measurement transcript.
|
|
70
127
|
*
|
|
71
|
-
* Each pattern requires a
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
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${
|
|
80
|
-
new RegExp(`100% of\\b${CTX_GAP}\\b${
|
|
81
|
-
new RegExp(`\\ball \\d+\\b${CTX_GAP}\\b${
|
|
82
|
-
new RegExp(`no remaining\\b${CTX_GAP}\\b${
|
|
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${
|
|
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
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
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
|
|
353
|
+
* @returns {{ measurementLines: Set<number>, fencedLines: Set<number> }}
|
|
201
354
|
*/
|
|
202
|
-
function
|
|
203
|
-
const
|
|
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
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
233
|
-
*
|
|
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[]
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
|
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
|
|