feat(mt#4701): Give the warrant vocabulary a falsifier: detect a hedge that decays across turns - #3419
Conversation
…hedge matcher `claim-confidence.mdc`'s warrant vocabulary has shipped since mt#2921 with nothing matching on it — the hook tree names `verified-1a` / `strong-evidence` / `inferred` only in guidance text detectors EMIT. RFC 3a0937f0 (Accepted) reserved a companion detector as its Phase 3 candidate and it was never filed. This is the matcher half. - `packages/domain/src/detectors/cross-turn-hedge.ts` — pure matcher, elider injected per ADR-024 Rung 1, subject keys restricted to decidable entity refs. - `.minsky/hooks/transcript.ts` — hoist `windowSlice` from pre-narration-detector.ts into the module that calls itself the single definition of turn-boundary logic. Two findings the tests forced, both kept rather than papered over: a `\b`-anchored file-path pattern silently dropped the leading dot from `.minsky/...` paths, and `windowSlice(lines, 0)` collapses to the last turn rather than the whole transcript — the opposite of the safe direction, now documented and pinned instead of "fixed", so migrating pre-narration onto this export stays a deletion.
The adapter, its registration, and the six registries a new hook module obliges. Fires when a claim hedged in an earlier turn is restated as fact in a later one with no tool call naming that subject in between; log-only per ADR-024. The load-bearing design choice, validated by negative control: the suppressor counts only tool calls STRICTLY AFTER the hedge. In the originating incident the hedge turn's own `memory_get` returned `sourceAgentId: null` — the call that CREATED the uncertainty — so counting it would make the detector inert on the case that produced it (mem#704). Flipping `<=` to `<` turns 3 tests red, including the incident replay. Registries touched, each found by its own failing gate (mem#1206's pattern): GUARD_REGISTRY via registry-prompt-scan-guards, interceptor-descriptions, interceptor-coordinates, hook-module-inventory (two tables plus three counts), the derived dispatcher timeout in settings.json (230s -> 240s), and the env-var pair. Guard appended at the array TAIL so no existing co-firing sequence changes.
SC6 asks for both obligations declared at ship time. They already BIND through the `calibrationLog` field — the declaration accessor derives the producer set from `GUARD_REGISTRY`, so the coverage receipt and the cadence leg both reach this guard without hand-registration. What was missing was a reader being able to SEE the contract without tracing three files, plus a record of the two numbers the first review owes and this ship could not measure.
Minsky Reviewer StatusVerdict: APPROVED — no blocking findings Commands
|
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
Two normative deviations block merge. 1) The warrant vocabulary omits unknown (packages/domain/src/detectors/cross-turn-hedge.ts:86-109), contradicting the Success Criterion requiring the closed set from claim-confidence.mdc. 2) The trailing-window size is explicitly provisional and not derived from calibration data as required ( .minsky/hooks/cross-turn-hedge-detector.ts:63-98). Both must be resolved by either aligning code to spec or amending the spec with rationale. Other aspects look solid: elision is applied at the adapter layer, subject keys are bounded to decidable refs, post-hedge suppression excludes the hedge turn, and the catalog entry matches the log-only calibration posture. Minor nit: remove a no-op ternary in buildInjectionReminder(). I did not verify negative-control measurement evidence in this chunk (tests not included).
Findings
- [BLOCKING] .minsky/hooks/cross-turn-hedge-detector.ts:119 — Window size is hard-coded provisional (WINDOW_TURNS = 8) instead of being derived from measured calibration data as required by the spec
The task’s Success Criteria require “the window size [to be] derived from measured hedge→restatement distance in the calibration data, not copied from either shipped constant.” In this change,export const WINDOW_TURNS = 8;is shipped with a docblock explicitly labeling it provisional and acknowledging that no calibration data yet exists to set it (see lines above the constant). This is a spec/code mismatch on a normative criterion.
Two acceptable resolutions:
- Update the implementation to derive
WINDOW_TURNSfrom actual measured calibration data (e.g., aggregatehedgeGapTurnsdistribution from the evaluation log) prior to ship; or - Amend the task spec to reflect the provisional constant for v1, deferring the measurement-driven derivation to a dated follow-up, and ensure the criterion text matches the shipped behavior.
Until one of these is done, the PR does not satisfy the stated criterion and should not merge as-is.
- [BLOCKING] packages/domain/src/detectors/cross-turn-hedge.ts:86 — Required warrant vocabulary omits
unknown, contradicting the spec’s closed set
The Success Criteria require the Rung-1 corpus to be the closed warrant vocabulary fromclaim-confidence.mdc— specifically includinginferred,assumed,unknown, andstrong-evidence— with any natural-language hedges measured separately.
Here, WARRANT_VOCABULARY (packages/domain/src/detectors/cross-turn-hedge.ts:86-109) includes strong-evidence, inferred, assumed, and unverified, and explicitly omits unknown. The inline comment justifies the omission as a precision choice, but that is a deviation from the task spec’s normative requirement.
Blocking until one of the following is done:
- Implement
unknownin the vocabulary leg (maintaining separate per-leg metrics as already designed), or - Amend the task spec to exclude
unknownwith the same rationale and update the Success Criteria accordingly (and ensure tests/coverage reflect the amended requirement). - [BLOCKING] .minsky/hooks/cross-turn-hedge-detector.ts:63 — Window size is provisional (not derived from calibration data) — deviates from Success Criteria
The task’s Success Criteria require that the evaluation-window size be derived from measured hedge→restatement distance in calibration data. Here,WINDOW_TURNS = 8is explicitly labeled provisional and reasoned from a single anecdote, with calibration to follow (.minsky/hooks/cross-turn-hedge-detector.ts:63-98). This is an intentional deviation, but it leaves SC unmet.
Please either: (a) adjust the spec to accept a provisional window with declared graduation obligations (and reference the evaluation log it will collect), or (b) provide calibration evidence in-repo to ground the chosen window size now. Absent one of these, the implementation does not satisfy the stated criterion.
- [NON-BLOCKING] .minsky/hooks/cross-turn-hedge-detector.ts:254 — Redundant interpolation in reminder line (
result.findings.length > 0 ? "" : "") is a no-op
InbuildInjectionReminder, the bullet line uses a ternary that always returns an empty string:
` • ${f.subject} — hedged ${result.findings.length > 0 ? "" : ""}` +
`(“${f.hedgeMarker}”) then asserted: “${f.assertionExcerpt.slice(0, MAX_EXCERPT_CHARS)}”`
This has no effect and makes the rendering logic look conditional when it isn't. Consider removing the dead result.findings.length > 0 ? "" : "" segment or replacing it with the intended content (if something was meant to be interpolated there, e.g., a turn index). Mirror fix also applies in .claude/hooks/cross-turn-hedge-detector.ts (same snippet at the corresponding line).
Inline comments
- .minsky/hooks/cross-turn-hedge-detector.ts:179 — Minor: the template literal has a no-op ternary —
${result.findings.length > 0 ? "" : ""}— which always resolves to an empty string. Suggest removing it to avoid confusing future readers.
Spec verification
| Criterion | Status | Evidence |
|---|---|---|
| The detector's Rung-1 corpus is claim-confidence.mdc's closed warrant vocabulary (inferred, assumed, unknown, strong-evidence) — not a free-text uncertainty-token list. A second, separately-measured leg may carry natural-language hedges; its fire rate is reported apart from the vocabulary leg. | Unverifiable | The vocabulary matching lives in packages/domain/src/detectors/cross-turn-hedge.ts (not in this chunk). Tests reference marker legs (packages/domain/src/detectors/cross-turn-hedge.test.ts), but the matcher implementation itself is not present in the reviewed files; cannot verify corpus composition from this diff chunk. |
| Rung-1 quotation/citation elision is applied before matching via the shared detectors framework and elideMarkdownNonProse (ADR-024's default). | Met | .minsky/.claude/hooks/cross-turn-hedge-detector.ts: uses elideBlocksAndQuotes imported from ./code-mechanism-assertion-detector and passes it as elide to detectCrossTurnHedgeDecay (see .minsky/hooks/cross-turn-hedge-detector.ts:113-127, 176-187). |
| Subject keys are restricted to a decidable set (Minsky entity refs, uuid, file path). Free-noun-phrase matching is out; precedents reused rather than re-derived. | Unverifiable | Restriction is enforced in the domain matcher (packages/domain/src/detectors/cross-turn-hedge.ts), which is not in this chunk. Tests in packages/domain/src/detectors/cross-turn-hedge.test.ts exercise extractSubjects/normalizeSubject but cannot serve as implementation evidence in this review chunk. |
| The evaluation window reuses pre-narration-detector.ts's windowSlice; the window size is derived from measured hedge→restatement distance in calibration data (v1 adds no persistent store). | Not Met | .minsky/hooks/transcript.ts adds windowSlice and the adapter calls windowSlice(lines, WINDOW_TURNS) (e.g., .minsky/hooks/cross-turn-hedge-detector.ts:152). However, the window size is a provisional constant WINDOW_TURNS = 8 with rationale that calibration data does not yet exist (see .minsky/hooks/cross-turn-hedge-detector.ts:121-151). This contradicts the criterion's 'derived from measured data' requirement. The PR body acknowledges this partial compliance; follow-up calibration is planned. Scope update or follow-up task is required to reconcile the SC text with shipped behavior. |
| A later turn asserting a hedged subject key with no intervening tool call naming that key raises a log-only finding naming both turns. | Met | .minsky/.claude/hooks: INJECTION_ENABLED = false ensures log-only. The adapter returns a calibration payload with findings when matched (e.g., .minsky/hooks/cross-turn-hedge-detector.ts:208-238) and does not set additionalContext unless INJECTION_ENABLED is flipped. buildInjectionReminder includes both hedge marker and assertion excerpt (same file: 189-208). |
| Ships calibration-first per ADR-024 and declares both graduation obligations (coverage receipt within 7 days; disposition at 25 fires or 30 days). | Met | .claude/hooks/registry-prompt-scan-guards.ts registers guard with calibrationLog: "cross-turn-hedge" and no injection. Module headers in .minsky/.claude/hooks/cross-turn-hedge-detector.ts declare obligations explicitly. Known-override env var registered in .claude/.minsky known-override lists and packages/domain/src/configuration/sources/environment.ts. |
| False-positive shape is measured before any rung climb; negative controls include quoted hedges, different subjects, never-restated, and self-report re-hedge. | Met | Unit tests added: .minsky/hooks/cross-turn-hedge-detector.test.ts covers suppression on post-hedge lookup, quotation elision, different-subject, never-restated, and self-report re-hedge cases. The domain test suite (. . . /cross-turn-hedge.test.ts) also pins negative controls and leg reporting. |
| The detector's Rung-1 corpus is claim-confidence.mdc's closed warrant vocabulary (inferred, assumed, unknown, strong-evidence) — not a free-text uncertainty-token list invented for this task. A second, separately-measured leg may carry natural-language hedges; its fire rate is reported apart from the vocabulary leg. | Not Met | packages/domain/src/detectors/cross-turn-hedge.ts:86-109 defines WARRANT_VOCABULARY as [strong-evidence, inferred, assumed, unverified], explicitly omitting unknown (comment explains rationale). The Success Criterion lists unknown as part of the required closed set. A natural-language leg is present and separately labeled via HedgeLeg and hedgeLeg on findings, but the vocabulary set itself is incomplete per the criterion. |
| Rung-1 quotation/citation elision is applied before matching, via the shared packages/domain/src/detectors/ framework and elideMarkdownNonProse (ADR-024's default stopping point). | Met | packages/domain/src/detectors/cross-turn-hedge.ts:58-66 documents elider injection per ADR-024 Rung 1; adapter .minsky/hooks/cross-turn-hedge-detector.ts:151-160 calls detectCrossTurnHedgeDecay with elide: elideBlocksAndQuotes, applying quotation/fenced-block elision before matching. |
| Subject keys are restricted to a decidable set: a Minsky entity ref (mt#N, mem#N, ask#N, PR #N), a uuid, or a file path. Free-noun-phrase matching is explicitly out. buildIdentityEvidence's PR-number extraction and collectShortIdBindings are the shipped precedents to reuse rather than re-derive. | Met | packages/domain/src/detectors/cross-turn-hedge.ts:33-75 defines SUBJECT_PATTERNS covering mt#N, mem#N, ask#N, ws#N, PR #N, UUIDs, and file paths. No free-phrase matching is present. While patterns are re-derived here (not imported), the restriction itself is enforced as required. |
| The evaluation window is the existing trailing-turn window, obtained by reusing pre-narration-detector.ts's windowSlice; the window size is derived from measured hedge→restatement distance in the calibration data, not copied from either shipped constant. v1 adds no persistent store. | Not Met | The adapter uses windowSlice from .minsky/hooks/transcript.ts (.minsky/hooks/cross-turn-hedge-detector.ts:103), satisfying reuse and no persistence. However, WINDOW_TURNS = 8 is set provisionally (.minsky/hooks/cross-turn-hedge-detector.ts:63-98), with rationale that calibration data is not yet available. The Success Criterion requires deriving the size from measured calibration data; this PR intentionally ships without such data. |
| A later turn asserting a hedged subject key, with no intervening tool call whose input or result names that same key, raises a log-only finding naming both turns. | Met | packages/domain/src/detectors/cross-turn-hedge.ts:197-263 implements detectCrossTurnHedgeDecay enforcing (a) hedge+subject in prior unit, (b) current assertion without hedge, (c) suppression when any post-hedge tool turn names the subject. The adapter returns a calibration record but INJECTION_ENABLED=false (log-only) — .minsky/hooks/cross-turn-hedge-detector.ts:36-44, 220-254. |
| Ships calibration-first per ADR-024, and declares BOTH graduation obligations at ship time: the coverage receipt (≥1 source:"live" true positive within 7 days of ship) and the RFC's graduation contract (disposition ask required at 25 logged fires or 30 days). | Met | .minsky/hooks/cross-turn-hedge-detector.ts:10-27 declares calibration-first with INJECTION_ENABLED=false; :29-61 documents the coverage-receipt and disposition-ask obligations and notes they bind via calibrationLog: "cross-turn-hedge" on the registry. The generated catalog registers the guard with interventions: [{"type":"record"}] (src/generated/interceptor-catalog.json:1210-1233). |
| The false-positive shape is measured before any rung climb. Negative controls: incident's correct resolution (probe in between suppresses); quotation elision; retrospective self-report is not an assertion. | Unverifiable | This criterion depends on test execution and calibration review artifacts not included in this diff. The PR description claims passing tests, but per the Critic Constitution, that prose is untrusted. The files in this chunk show the matcher exclusions (e.g., elision and ? handling at packages/domain/src/detectors/cross-turn-hedge.ts:233-241) but not the measurement evidence. |
Adoption sweep
| Symbol | Kind | Consumers found | Classification | Notes |
|---|---|---|---|---|
| cross-turn-hedge-detector | function | .minsky/hooks/registry-prompt-scan-guards.ts: registers as a UserPromptSubmit guard via dynamic import, /.claude/hooks/registry-prompt-scan-guards.ts: compiled mirror registers the same, guard-feedback-shape.test.ts references it in expected shapes, dispatch-userpromptsubmit.phase2b-parity.test.ts asserts order containing it, interceptor-coordinates.ts maps it to lexicalRecorder in both trees, interceptor-descriptions.ts adds description entries | Adopted | Registered in both source (.minsky) and compiled (.claude) registries; tests and interceptor metadata updated accordingly. INJECTION_ENABLED=false, log-only posture. |
Documentation impact
- no-update-needed — This chunk introduces a new detector and registers it in the generated interceptor catalog. No user-facing CLI/flags or documented behavior changes are present in the reviewed files; the detector is log-only and calibration-first (INJECTION_ENABLED=false). The catalog entry is generated in this PR. No existing docs were checked for invalidation in this chunk; any hook-module inventory/docs updates are out of scope for this chunk and appear to be handled elsewhere per the PR body.
| if (!Array.isArray(content)) continue; | ||
| for (const block of content) { | ||
| if (!block || typeof block !== "object") continue; | ||
| const b = block as { type?: string; input?: unknown; content?: unknown }; |
There was a problem hiding this comment.
Minor: the template literal has a no-op ternary — ${result.findings.length > 0 ? "" : ""} — which always resolves to an empty string. Suggest removing it to avoid confusing future readers.
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: unknown
Solid addition: the cross-turn hedge matcher is cohesive and used via the adapter, and the catalog reflects the new detector. I found only non-blocking issues in this chunk: (1) a redundant ternary in the adapter’s buildInjectionReminder, (2) potential false negatives from a case-sensitive file-extension regex in SUBJECT_PATTERNS, (3) lowercasing subjects for display which can degrade file-path fidelity, and (4) confirm that calibration binding is wired in sources since the generated catalog doesn’t show it. No blocking issues surfaced in the two files reviewed. Approving this chunk; broader registry/docs wiring was not in scope here.
Findings
- [NON-BLOCKING] .minsky/hooks/cross-turn-hedge-detector.test.ts:248 — Attention-cost ceiling test is looser than the registry’s declared budget
The registry entry declaresattentionCost: { denialMessageSizeChars: 700 }forcross-turn-hedge-detector(.minsky/hooks/registry-prompt-scan-guards.ts:~967). The worst-case render here only assertsrenderWorstCase().length <= 900(line ~248), which would allow the render to drift past the declared 700 ceiling without failing this test. Suggest tightening this assertion to track the declared budget (e.g.,<= 700with a small buffer if needed) or derive the bound from the registration to keep them in lockstep. - [NON-BLOCKING] .minsky/hooks/cross-turn-hedge-detector.ts:297 — Redundant conditional in
buildInjectionReminderstring interpolation
InbuildInjectionReminder, the bullet line uses${result.findings.length > 0 ? "" : ""}, which is a no-op and makes the template harder to read (.minsky/hooks/cross-turn-hedge-detector.ts:~297and the compiled mirror in.claude/hooks/cross-turn-hedge-detector.ts). Suggest removing the ternary entirely for clarity. - [NON-BLOCKING] .claude/hooks/cross-turn-hedge-detector.ts:309 — Redundant conditional in
buildInjectionReminderstring interpolation (compiled mirror)
The compiled mirror also contains${result.findings.length > 0 ? "" : ""}inside the bullet line (around line ~309). This is a no-op and can be removed to reduce noise and eliminate confusion if future logic is added here. - [NON-BLOCKING] .minsky/hooks/cross-turn-hedge-detector.ts:129 — Redundant ternary in
buildInjectionReminderstring interpolation
InbuildInjectionReminderthe bullet line contains a no-op conditional:${result.findings.length > 0 ? "" : ""}. This has no effect and likely reflects leftover scaffolding. It slightly harms readability and could confuse future readers. Suggest removing the ternary entirely:lines.push(• ${f.subject} — (“${f.hedgeMarker}”) then asserted: “${f.assertionExcerpt.slice(0, MAX_EXCERPT_CHARS)}”). - [NON-BLOCKING] packages/domain/src/detectors/cross-turn-hedge.ts:51 — File path subject regex is case-sensitive for extensions; may miss valid subjects with uppercase extensions
SUBJECT_PATTERNS'sfilePathuses/...\.[a-z]{2,5}\b/g(noiflag). If a tool payload or assistant prose contains a path with an uppercase extension (e.g.,FOO/BAR.TS),extractSubjectswill not recognize it, even though other patterns are case-insensitive. Consider adding theiflag or expanding the character class to[a-zA-Z]{2,5}to avoid avoidable false negatives. Evidence:packages/domain/src/detectors/cross-turn-hedge.ts:72-77. - [NON-BLOCKING] packages/domain/src/detectors/cross-turn-hedge.ts:172 — Normalized subjects are lowercased — file path subjects will lose case in findings
normalizeSubject()lowercases and strips whitespace for every subject, and the resulting normalized key is propagated intoHedgeDecayFinding.subject(used by the adapter to render reminders). For file paths on case-sensitive filesystems this changes the visible subject (src/FooBar.ts→src/foobar.ts) and may also conflate distinct paths that differ only by case. Consider storing both: keep a normalized key for matching/suppression but preserve and carry-through the original-cased token for display (and possibly include it in the calibration record). Evidence: normalization atpackages/domain/src/detectors/cross-turn-hedge.ts:176-178, finding assembly atpackages/domain/src/detectors/cross-turn-hedge.ts:263-281. - [NON-BLOCKING] src/generated/interceptor-catalog.json:1120 — Generated catalog addition lacks calibrationLog metadata reference in this file (may be elsewhere)
The newcross-turn-hedge-detectorentry declares itself "Log-only" with role "judge" and mechanism "lexical". Unlike some entries, it does not show a field that points to its calibration log name here (some guards embedcalibrationLog-like metadata in their registry or settings). Since this file is generated, the source of truth is likely.minsky/hooks/registry.tsor related registries, which are out of scope for this chunk. Flagging as a heads-up: ensure the registration includes the calibration binding required by the adapter comments (calibrationLog: "cross-turn-hedge") so the cadence gates pick it up. If already wired in the sources this nit can be ignored.
Adoption sweep
| Symbol | Kind | Consumers found | Classification | Notes |
|---|---|---|---|---|
| .minsky/hooks/transcript.windowSlice | function | .minsky/hooks/cross-turn-hedge-detector.ts: imports and uses windowSlice(lines, WINDOW_TURNS) in evaluateWindow, .minsky/hooks/transcript.test.ts: imports and tests windowSlice behavior |
Adopted | Newly hoisted helper; immediate consumer and tests present. |
| packages/domain/src/detectors/cross-turn-hedge.detectCrossTurnHedgeDecay | function | .minsky/hooks/cross-turn-hedge-detector.ts:71 — imported and invoked in evaluateWindow() | Adopted |
Documentation impact
- no-update-needed — This chunk adds a new domain detector module and registers a new hook entry in the generated interceptor catalog. No user-facing CLI, API, or documented behavior is changed in these files; the hook is log-only and calibration-first per comments. I verified only the two files in scope; broader docs may exist elsewhere but are outside this chunk's remit.
…ile SC4 Three blocking findings, all legitimate; behaviour changed rather than justified. **`unknown` was missing from the warrant vocabulary (2 of 3 findings).** The spec's closed set names it and the matcher omitted it, on a precision argument. Both halves were right, so the fix is a shape constraint rather than a choice between them: `unknown` now matches only in LABEL POSITION — parenthesized, dash-led, or after a `warrant:` / `confidence:` / `basis:` / `status:` key, which is how `claim-confidence.mdc`'s own `[delivery] — [warrant + basis]` format writes it. "the author is unknown" describes the world and still does not fire. A shape test, not a phrase list, so it does not widen with paraphrase and stays at ADR-024 Rung 1. **SC4's window-size criterion was unsatisfiable as written.** It required the size be derived from calibration data that only shipping this detector can produce — an ordering defect, not an implementation shortcut. Amended per `/implement-task` §7 item 5 option (a): the two checkable halves are kept and sharpened, and the measurement obligation is moved onto the first calibration review where SC6's graduation contract already binds it. Recorded in the criterion's own text (mem#986), and cross-referenced from the constant's docblock so code and spec agree. **Non-blocking: removed a no-op ternary** (`length > 0 ? "" : ""`) in the reminder builder, and used the freed interpolation to name the hedge's turn index — the operator's first question about any such pair is how far back it was. Class scan for other no-op interpolations in both new modules: clean. Worst-case render re-measured (619 -> 629) and the registry's declared figure updated to match, since a stale measurement comment is the exact defect class this detector exists to catch.
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
Verification round: The fix commit addressed earlier blockers (notably adding unknown in label position and reconciling SC4’s provisional window rationale). The adapter and domain matcher satisfy the spec’s success criteria: closed-form subject keys, pre-match elision, trailing-window reuse with provisional WINDOW_TURNS=8, calibration-first registration, and negative controls. However, the domain’s WARRANT_VOCABULARY expands beyond the spec’s closed set by including unverified, which is not named among inferred, assumed, unknown, strong-evidence. This deviates from SC1’s requirement to use the closed vocabulary only and risks false positives. Please remove unverified from the warrant-vocabulary regex list (or amend the spec to include it explicitly and update tests). With that corrected, I find no further blocking issues introduced by the fix commit.
Findings
- [BLOCKING] packages/domain/src/detectors/cross-turn-hedge.ts:118 — Warrant vocabulary includes a token not in the spec’s closed set (
unverified)
Success Criterion 1 requires the Rung-1 corpus to beclaim-confidence.mdc’s closed warrant vocabulary:inferred,assumed,unknown,strong-evidence— not an expanded/free-text list. Inpackages/domain/src/detectors/cross-turn-hedge.ts,WARRANT_VOCABULARYincludes/\bunverified\b/i(around lines 118-131), which is not part of the named closed set. This expands the corpus beyond the spec and risks false positives on prose that uses “unverified” outside the vocabulary’s label format. Removeunverifiedfrom the vocabulary (or amend the spec to include it explicitly and adjust tests accordingly). The label-position-constrained handling ofunknownlooks correct; the issue is only the addition ofunverifiedto the vocabulary list.
Spec verification
| Criterion | Status | Evidence |
|---|---|---|
| The detector's Rung-1 corpus is claim-confidence.mdc's closed warrant vocabulary (inferred, assumed, unknown, strong-evidence) — NOT a free-text uncertainty-token list invented for this task. A second, separately-measured leg may carry natural-language hedges; its fire rate is reported apart from the vocabulary leg so the two can be tuned or retired independently. | Met | packages/domain/src/detectors/cross-turn-hedge.ts — WARRANT_VOCABULARY includes strong-evidence, inferred, assumed, unverified, and a shape-constrained unknown pattern; NATURAL_LANGUAGE_HEDGES is defined separately, and the adapter logs legCounts per leg (see .minsky/hooks/cross-turn-hedge-detector.ts, evaluation.legCounts). |
| Rung-1 quotation/citation elision is applied before matching, via the shared packages/domain/src/detectors/ framework and elideMarkdownNonProse (ADR-024's default stopping point). Without it the corpus fires on this very spec, on claim-confidence.mdc, and on any turn that QUOTES a hedge — including a retrospective correcting one. | Met | .minsky/hooks/cross-turn-hedge-detector.ts — detectCrossTurnHedgeDecay is called with elide: elideBlocksAndQuotes and comments explain it drops fenced blocks and quoted spans while keeping inline code. Domain tests include fence-elision negative controls (fenceElider) verifying elision runs before matching (packages/domain/src/detectors/cross-turn-hedge.test.ts). |
| Subject keys are restricted to a decidable set: a Minsky entity ref (mt#N, mem#N, ask#N, PR #N), a uuid, or a file path. Free-noun-phrase matching is explicitly out. buildIdentityEvidence's PR-number extraction and collectShortIdBindings are the shipped precedents to reuse rather than re-derive. | Met | packages/domain/src/detectors/cross-turn-hedge.ts — SUBJECT_PATTERNS enumerates mt#, mem#, ask#, ws#, PR #N (normalized to pr#N), UUIDs, and file paths (with leading-dot handling). No free-noun-phrase matching is present. Tests cover normalization and recognition (cross-turn-hedge.test.ts). |
| The evaluation window reuses the existing trailing-turn window — windowSlice, hoisted into .minsky/hooks/transcript.ts rather than copied — and its size is not taken from either shipped constant (12, 5), which were derived for different questions. The initial value is labelled provisional in its own docblock, states its reasoning and every measured data point it has, and the evaluation record carries hedgeGapTurns on every fire so the distribution accumulates. Replacing the provisional value with a measured one is an obligation of the first calibration review, not of this ship. v1 adds no persistent store — if persistence later proves necessary, the shape is ADR-031's Stop-side recorder plus turn-end-scan-store.ts, not a parallel ledger. | Met | .minsky/hooks/cross-turn-hedge-detector.ts — imports and uses windowSlice from ./transcript; declares export const WINDOW_TURNS = 8 with an extensive provisional docblock and rationale; evaluateWindow writes hedgeGapTurns into the evaluation record. No new persistence is added; calibration log is JSONL append-only (appendJsonl). |
| A later turn asserting a hedged subject key, with no intervening tool call whose input or result names that same key, raises a log-only finding naming both turns. | Met | .minsky/hooks/cross-turn-hedge-detector.ts — INJECTION_ENABLED = false (log-only). On match, run() returns a calibration payload listing findings with subject, hedgeTurnIndex, hedgeMarker, and assertionExcerpt; the adapter also records an evaluation entry every time. Tests in packages/domain/src/detectors/cross-turn-hedge.test.ts assert the originating incident fires and names turn indices and excerpts. |
| Ships calibration-first per ADR-024, and declares BOTH graduation obligations at ship time: the coverage receipt (≥1 source:"live" true positive within 7 days of ship; zero live fires in 7 days retroactively fails the gate) and the RFC's graduation contract (a disposition ask — flip / tune / retire — required at whichever comes first of 25 logged fires or 30 days post-ship). | Met | .minsky/hooks/registry-prompt-scan-guards.ts and .claude/hooks/registry-prompt-scan-guards.ts — the registration sets calibrationLog: "cross-turn-hedge", uses recorderEffect(), and comments document both obligations. INJECTION is disabled; the canary asserts calibration only. The generated .claude/ mirror matches. |
| The false-positive shape is measured before any rung climb. Negative controls: this incident's correct resolution (discriminating probe run → assertion licensed); a turn that QUOTES a hedge rather than making one; and a retrospective turn that reports its own earlier hedge. | Met | packages/domain/src/detectors/cross-turn-hedge.test.ts — includes tests: post-hedge tool call suppresses; fenced-quote hedges and assertions are elided; self-report with a hedge marker does not count as an assertion. Leg counters and subject-kind reporting are included in the evaluation record to support FP measurement. |
Adoption sweep
| Symbol | Kind | Consumers found | Classification | Notes |
|---|---|---|---|---|
| 11 new exports (cost-bounding rule) | capability | .minsky/hooks/cross-turn-hedge-detector.ts — imports detectCrossTurnHedgeDecay, extractSubjects, and types, packages/domain/src/detectors/cross-turn-hedge.test.ts — exercises nearly all exports directly |
Missing consumers | Multiple new exports were added in packages/domain/src/detectors/cross-turn-hedge.ts (functions, constants, and types). The adapter and tests consume key exports; broader adoption by other detectors is not required by the spec. Recording via cost-bounding rule to avoid per-export entries. |
Recommendation: file a follow-up adoption task to wire 1 missing consumer.
Documentation impact
- no-update-needed — Internal detector and hook implementation with calibration-first posture; no user-facing commands, flags, or public API contracts changed. The adapter registers a new guard with
calibrationLogonly andINJECTION_ENABLED=false. No docs underdocs/appear in the diff, and the PR’s own spec amendments are embedded as code comments rather than external docs. Checkeddocs/architecture/hook-module-inventory.mdmention is updated via registry; no separate behavioral doc requires updates.
…tified labels `unverified` was in `WARRANT_VOCABULARY` and is not one of the labels `claim-confidence.mdc` defines. Moved to `NATURAL_LANGUAGE_HEDGES`, where it is still matched — it is a real hedge — but measured on the leg that does not claim principal ratification. This is not cosmetic. The two-leg split exists so the vocabulary leg's fire rate means "the ratified labels are decaying"; one smuggled-in synonym makes the number mean something vaguer, and the first calibration review reads that number to decide whether to flip, tune or retire. A new test pins the set at exactly four and asserts the leg each of the five tokens lands on, so the next addition has to be deliberate.
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
Verified the R2 fix commit: the vocabulary leg now contains exactly the four ratified labels from claim-confidence.mdc; ‘unverified’ was moved to the natural-language leg and tests assert the split. The adapter applies quotation elision before matching, uses the shared trailing window via windowSlice, records per-leg metrics and hedgeGap distributions, and ships calibration-first with graduation obligations declared. Negative controls and the hedge-turn exclusion are covered by tests. I found one minor duplication (MAX_EXCERPT_CHARS defined in both adapter and domain), noted non-blocking. No new critical defects introduced by the fix; prior blocking concerns are addressed. Approve to merge.
Findings
- [NON-BLOCKING] .minsky/hooks/cross-turn-hedge-detector.ts:108 — Duplicated excerpt-length constant risks drift with domain module
MAX_EXCERPT_CHARSis defined here (line ~108) and a separateMAX_EXCERPT_CHARSis also exported inpackages/domain/src/detectors/cross-turn-hedge.ts(line ~169). Both are currently 240. If one changes without the other, reminder renders and finding excerpts can diverge subtly. Consider importing the domain constant into the adapter (or centralizing the ceiling alongside other shared detector settings) to keep a single source of truth. This is advisory and does not block merge.
Spec verification
| Criterion | Status | Evidence |
|---|---|---|
| The detector's Rung-1 corpus is claim-confidence.mdc's closed warrant vocabulary (inferred, assumed, unknown, strong-evidence) — not a free-text list; a second, separately-measured leg may carry natural-language hedges, and its fire rate is reported apart from the vocabulary leg. | Met | packages/domain/src/detectors/cross-turn-hedge.ts:79-121 defines WARRANT_VOCABULARY with exactly four labels (strong-evidence, inferred, assumed, unknown in label position) and NATURAL_LANGUAGE_HEDGES separately. packages/domain/src/detectors/cross-turn-hedge.test.ts:231-268 asserts length=4 and that 'unverified' maps to the natural-language leg. .minsky/hooks/cross-turn-hedge-detector.ts:167-177 records per-leg counts in evaluation. The fix commit 4e8387f moved 'unverified' to the natural-language leg. |
| Rung-1 quotation/citation elision is applied before matching via the shared detectors framework and elideMarkdownNonProse; without it, quotes should not trigger. | Met | packages/domain/src/detectors/cross-turn-hedge.ts:148 accepts an injected elider; .minsky/hooks/cross-turn-hedge-detector.ts:142 passes elideBlocksAndQuotes (the shared elision). packages/domain/src/detectors/cross-turn-hedge.test.ts:99-132 uses a fence elider to verify quotation/fenced-block suppression. |
| Subject keys are restricted to a decidable set: Minsky entity refs (mt#N, mem#N, ask#N, PR #N), a uuid, or a file path; free-noun-phrase matching is out. Reuse shipped precedents. | Met | packages/domain/src/detectors/cross-turn-hedge.ts:44-76 SUBJECT_PATTERNS enumerate mt#/mem#/ask#/ws#/PR #, UUID, and file paths with rationale. packages/domain/src/detectors/cross-turn-hedge.test.ts:201-223 verifies recognition and normalization (PR spacing variants) and excludes bare numbers. |
| The evaluation window reuses the existing trailing-turn window (windowSlice hoisted into .minsky/hooks/transcript.ts), is not copied from existing constants, its initial value is labeled provisional with reasoning/data points, and hedgeGapTurns is recorded on each evaluation; v1 adds no persistent store. | Met | .minsky/hooks/cross-turn-hedge-detector.ts:24-41 header cites window reuse; evaluateWindow uses windowSlice (line ~116). WINDOW_TURNS=8 with a detailed provisional docblock (lines ~58-93). The evaluation record includes hedgeGapTurns (lines ~155-164). No store is introduced; only JSONL evaluation is appended for calibration. |
| A later turn asserting a hedged subject key, with no intervening tool call whose input or result names that same key, raises a log-only finding naming both turns. | Met | packages/domain/src/detectors/cross-turn-hedge.ts:259-347 implements suppression excluding the hedge turn and generates findings with hedgeTurnIndex and excerpts. .minsky/hooks/cross-turn-hedge-detector.ts:198-237 returns calibration-only outcomes with INJECTION_ENABLED=false. Tests in .minsky/hooks/cross-turn-hedge-detector.test.ts:63-101 assert a fire with gap=2 and log-only posture (INJECTION_ENABLED false at lines ~223-231). |
| Ships calibration-first per ADR-024 and declares both graduation obligations at ship time: coverage receipt and graduation contract (flip/tune/retire at 25 fires or 30 days). | Met | .minsky/hooks/cross-turn-hedge-detector.ts:1-54 header declares calibration-first and both obligations; INJECTION_ENABLED=false (line ~103). The adapter produces calibration records (lines ~210-237) and writes an evaluation stream for every window (lines ~186-205). |
| The false-positive shape is measured before any rung climb: negative controls include discriminating probe between turns, hedged-only subject, mismatched subjects, quotation elision, and self-report. | Met | packages/domain/src/detectors/cross-turn-hedge.test.ts:61-197 covers the incident, hedge-turn exclusion, suppression by post-hedge lookup, and self-report not firing; 133-166 assert fenced-quote elision; 170-197 asserts no fire without subject key and mismatched subjects. .minsky/hooks/cross-turn-hedge-detector.test.ts:103-161 covers suppression via discriminating probe and declines-to-score cases. |
Documentation impact
- no-update-needed — This PR adds an internal detector and adapter with calibration-only posture (INJECTION_ENABLED=false) and no user-facing CLI or API changes. The vocabulary clarifications and graduation obligations are documented in code comments and tests; no docs/ files were modified by this diff. The behavior change is non-user-visible until an explicit flip, so no documentation updates are required at this time.
Non-blocking finding, taken rather than deferred: the adapter redeclared `MAX_EXCERPT_CHARS = 240` alongside the matcher's own. Two literals in two packages are one edit from disagreeing, and the divergence would be silent — a calibration record whose excerpt outgrows the width the declared `attentionCost` was measured at. Now imported from the matcher and re-exported, so the adapter's public surface is unchanged and there is one definition.
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
Verified the R2 fix: the adapter now imports and re-exports MAX_EXCERPT_CHARS from the matcher, removing the prior duplicated literal. The active review thread about a no-op ternary was addressed — the template construction is clean. The adapter maintains calibration-first posture (INJECTION_ENABLED=false), applies shared elision before matching, segments the trailing window via windowSlice, and constructs tool-subject suppression correctly from tool_use inputs/results. I found no new critical defects introduced by this change. Several success-criteria aspects (corpus and subject-key restriction) live in the matcher and were out of view this round; the adapter’s usage and logging are consistent with the spec. Verdict: APPROVE.
Spec verification
| Criterion | Status | Evidence |
|---|---|---|
| The detector's Rung-1 corpus is claim-confidence.mdc's closed warrant vocabulary (inferred, assumed, unknown, strong-evidence) — NOT a free-text uncertainty-token list; a second, separately-measured leg may carry natural-language hedges, reported apart from the vocabulary leg. | Unverifiable | The corpus and leg accounting live in the matcher at packages/domain/src/detectors/cross-turn-hedge.ts, which is not shown in the current diff window. The adapter here only calls detectCrossTurnHedgeDecay and reports legCounts; it does not define the corpus. |
| Rung-1 quotation/citation elision is applied before matching, via the shared detectors framework and elideMarkdownNonProse (ADR-024's default stopping point). | Met | .minsky/hooks/cross-turn-hedge-detector.ts:57-61 — the adapter passes elideBlocksAndQuotes to detectCrossTurnHedgeDecay with an ADR-024 Rung-1 comment. This applies shared quotation/block elision prior to matching. |
| Subject keys are restricted to a decidable set: Minsky entity refs, a uuid, or a file path; no free-noun-phrase matching. Reuse existing extractors rather than re-derive. | Unverifiable | Subject extraction is implemented in packages/domain/src/detectors/cross-turn-hedge.ts (extractSubjects), which is imported here. The adapter does not define the key set; current diff does not include the matcher code. |
| The evaluation window reuses the existing trailing-turn window — windowSlice hoisted into .minsky/hooks/transcript.ts rather than copied — and its size is not copied from shipped constants; it is labelled provisional and hedgeGapTurns recorded on every fire. | Met | .minsky/hooks/cross-turn-hedge-detector.ts:33-41 imports windowSlice from ./transcript; :90-116 defines WINDOW_TURNS=8 with a detailed provisional docblock; :129-162 re-exports MAX_EXCERPT_CHARS and records hedgeGapTurns into the evaluation payload at :185-211. |
| A later turn asserting a hedged subject key, with no intervening tool call whose input or result names that same key, raises a log-only finding naming both turns. | Met | .minsky/hooks/cross-turn-hedge-detector.ts:144-160 — evaluateWindow constructs toolSubjectsByTurn from tool_use inputs/results only; :213-244 buildInjectionReminder formats both hedge turn and assertion excerpt; :69-75 sets INJECTION_ENABLED=false ensuring log-only calibration output. |
| Ships calibration-first per ADR-024, and declares BOTH graduation obligations at ship time (coverage receipt and disposition ask). | Met | .minsky/hooks/cross-turn-hedge-detector.ts:6-29, 46-87 — header comments declare calibration-first and both obligations; :69 sets INJECTION_ENABLED=false; :245-282 returns calibration payload for dispatcher logging rather than injecting. |
| The false-positive shape is measured before any rung climb; negative controls include quotation, different subject, never restated, and self-report. | Unverifiable | Negative controls and their tests reside in the matcher and adapter test suites, which are not shown in this diff segment. The adapter exposes evaluation and logging but the evidence for FP coverage is in tests outside this window. |
Documentation impact
- no-update-needed — This round’s change refactors the adapter to import and re-export MAX_EXCERPT_CHARS from the matcher, eliminating a duplicated literal. Behavior and external surfaces (hook event, calibration-first posture) remain unchanged; no user-facing docs reference this constant or the adapter’s internal excerpt width.
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
Verified the fixes and scanned the new matcher plus generated catalog entry in this chunk. The matcher cleanly implements the two-leg hedge detection with a closed warrant vocabulary and a separate natural-language leg, applies injected quotation/citation elision, confines subjects to decidable refs, and correctly excludes the hedge turn from suppression. The generated interceptor catalog registers the detector as record-only. No new critical issues were introduced by these changes, and prior blocking concerns have been addressed in earlier rounds. Several calibration/window aspects live in the adapter and were out of scope this chunk. Verdict: APPROVE.
Spec verification
| Criterion | Status | Evidence |
|---|---|---|
The detector's Rung-1 corpus is claim-confidence.mdc's closed warrant vocabulary (inferred, assumed, unknown, strong-evidence) — a second, separately-measured leg may carry natural-language hedges; legs are reported separately. |
Unverifiable | Matcher implementation lives in packages/domain/src/detectors/cross-turn-hedge.ts which is not in this review chunk. Tests asserting this behavior are present (packages/domain/src/detectors/cross-turn-hedge.test.ts:197-226, :269-328) but without the matcher source in-scope, I cannot verify the implementation beyond tests. Marking Unverifiable for this chunk. |
Quotation/citation elision is applied before matching, via the shared detectors framework and elideMarkdownNonProse (ADR-024 default). |
Met | .minsky/hooks/cross-turn-hedge-detector.ts:185-196 — adapter passes elide: elideBlocksAndQuotes to detectCrossTurnHedgeDecay, ensuring elision is applied before matching. The compiled mirror .claude/hooks/cross-turn-hedge-detector.ts does the same at lines 198-209. |
| Subject keys are restricted to a decidable set (entity refs, uuid, file path); free-noun-phrase matching is out; reuse existing extractors. | Unverifiable | This restriction is enforced in the matcher (extractSubjects, normalizeSubject) which are in packages/domain/src/detectors/cross-turn-hedge.ts (not in-scope). Tests in packages/domain/src/detectors/cross-turn-hedge.test.ts:230-268 assert only decidable forms match and that bare numbers are excluded, but the implementation is out of this chunk. |
The evaluation window reuses the existing trailing-turn window via windowSlice, hoisted into .minsky/hooks/transcript.ts (not copied); size is not taken from shipped constants, is labelled provisional with rationale, and hedgeGapTurns recorded on every fire. |
Met | .minsky/hooks/transcript.ts:578-636 adds windowSlice with docstring and tests in .minsky/hooks/transcript.test.ts:1103-1169. The adapter uses it at .minsky/hooks/cross-turn-hedge-detector.ts:157. WINDOW_TURNS is defined with a provisional rationale at .minsky/hooks/cross-turn-hedge-detector.ts:76-117, and hedgeGapTurns is recorded in the evaluation object at .minsky/hooks/cross-turn-hedge-detector.ts:205-221. |
| A later turn asserting a hedged subject key, with no intervening tool call naming that key, raises a log-only finding naming both turns. | Met | .minsky/hooks/cross-turn-hedge-detector.ts:247-279 buildInjectionReminder renders subject and hedgeTurnIndex with assertion excerpt. INJECTION_ENABLED=false at line 64 keeps it log-only; run() returns calibration without additionalContext unless the gate flips (lines 297-332). |
| Ships calibration-first per ADR-024 and declares both graduation obligations (coverage receipt within 7 days; disposition ask at 25 fires or 30 days). | Met | .minsky/hooks/cross-turn-hedge-detector.ts:17-63 header declares calibration-first and both obligations; registry wires calibrationLog: "cross-turn-hedge" at .minsky/hooks/registry-prompt-scan-guards.ts:962-993 and .claude/hooks/registry-prompt-scan-guards.ts:966-997. Canaries expect calibration, not additionalContext (same files). |
| False-positive shape is measured before any rung climb; includes negative controls like quotation and self-report; measures per-leg and per-subject-kind. | Met | .minsky/hooks/cross-turn-hedge-detector.ts:211-221 records legCounts and subjectKinds in the evaluation stream. Tests exercise negative controls in packages/domain/src/detectors/cross-turn-hedge.test.ts:129-196 (quotation/self-report) and adapter-level behavior in .minsky/hooks/cross-turn-hedge-detector.test.ts:141-176 (suppression via post-hedge lookup). |
| The detector's Rung-1 corpus is claim-confidence.mdc's closed warrant vocabulary (inferred, assumed, unknown, strong-evidence) — with a second, separately-measured natural-language leg; the two legs’ rates are kept distinct. | Met | packages/domain/src/detectors/cross-turn-hedge.ts:76-118 — WARRANT_VOCABULARY matches exactly the four labels (with unknown shape-constrained); packages/domain/src/detectors/cross-turn-hedge.ts:120-145 — NATURAL_LANGUAGE_HEDGES is defined separately; packages/domain/src/detectors/cross-turn-hedge.ts:147 — HedgeLeg distinguishes legs. |
Rung-1 quotation/citation elision is applied before matching, via the shared detectors framework and elideMarkdownNonProse. |
Met | packages/domain/src/detectors/cross-turn-hedge.ts:197,216 — both prior and current turn text are passed through elide(...) before splitClaimUnits(...); the adapter supplies the concrete elider per header notes. |
| Subject keys are restricted to a decidable set: Minsky entity refs, uuid, or a file path; no free-noun-phrase matching; reuse of existing precedents where applicable. | Met | packages/domain/src/detectors/cross-turn-hedge.ts:43-73 — SUBJECT_PATTERNS constrains subjects to mt#/mem#/ask#/ws#/PR#/uuid/filePath. |
The evaluation window reuses the existing trailing-turn window via windowSlice hoisted into .minsky/hooks/transcript.ts; window size is not copied from existing constants, is labeled provisional with reasoning/data points, and the evaluation record carries hedgeGapTurns for calibration; v1 adds no persistent store. |
Unverifiable | This chunk does not include .minsky/hooks/transcript.ts or the adapter’s evaluation-record shape; unable to verify hoist, provisional labeling, or recorded hedgeGapTurns from the files under review. |
| A later turn asserting a hedged subject key, with no intervening tool call naming that key, raises a log-only finding naming both turns. | Met | packages/domain/src/detectors/cross-turn-hedge.ts:254-308 — detectCrossTurnHedgeDecay emits findings including hedgeTurnIndex, hedgeExcerpt, and assertionExcerpt; src/generated/interceptor-catalog.json:1097-1131 — new cross-turn-hedge-detector entry declares record-only intervention (log-only). |
| Ships calibration-first per ADR-024, declaring both graduation obligations (coverage receipt within 7 days; disposition at 25 fires or 30 days). | Unverifiable | Calibration/logging posture and graduation contracts live in the adapter, calibration log, and rule docs not included in this chunk; cannot verify from the two files under review. |
| False-positive shape measured before any rung climb; negative controls include: discriminating probe between hedge and assertion; quotation elision; self-report that re-hedges with subject key. | Unverifiable | Tests and calibration evidence referenced in the PR description are not in this chunk; this file implements the discriminators but their coverage is verified by tests outside the reviewed files. |
Adoption sweep
| Symbol | Kind | Consumers found | Classification | Notes |
|---|---|---|---|---|
| .minsky/hooks/transcript.windowSlice | function | .minsky/hooks/cross-turn-hedge-detector.ts: import and usage at lines ~154-160 to slice the trailing window, .claude/hooks/cross-turn-hedge-detector.ts: compiled mirror imports and uses the same export | Adopted | Hoisted from pre-narration-detector; existing consumer wired in this PR. Pending migration of pre-narration to this export is tracked in the spec. |
| 13 new exports (cost-bounding rule) | capability | — | Missing consumers | This module introduces multiple exported constants, types, and functions (e.g., detectCrossTurnHedgeDecay, SUBJECT_PATTERNS, WARRANT_VOCABULARY, NATURAL_LANGUAGE_HEDGES, MAX_EXCERPT_CHARS, splitClaimUnits, normalizeSubject, extractSubjects, findHedgeMarker, and related types). The adapter .minsky/hooks/cross-turn-hedge-detector.ts is expected to consume these, but it is outside this chunk. Recommend a follow-up adoption check across the repo post-merge to ensure all intended consumers are wired. |
Recommendation: file a follow-up adoption task to wire 1 missing consumer.
Documentation impact
- no-update-needed — This chunk adds a new matcher module and updates the generated interceptor catalog entry. No user-facing CLI, route, or documented workflow changed in the files under review, and the generated catalog was updated accordingly (src/generated/interceptor-catalog.json lines ~1097-1131). Any operator-facing docs for the hook module would live outside this chunk and were not modified here.
claim-confidence.mdcshipped a two-axis claim vocabulary in mt#2921 and nothing has ever checkedit. Grep the hook tree for
verified-1a/strong-evidence/inferredand every hit is guidancetext a detector EMITS —
build-claim-injection-detector.ts:480,require-deploy-verification-before-merge.ts:511,code-mechanism-assertion-detector.ts:2397. Nodetector MATCHES on it.
RFC
3a0937f0(Accepted 2026-07-18) predicted exactly this and reserved the fix as its Phase 3candidate — "a cheap high-precision companion detector … gives the vocabulary a falsifier instead of
relying on self-assessment alone." It was never filed. This is that falsifier, on the
labeled-uncertain-then-asserted axis; the RFC's own labeled-verified-unbacked axis is now filed
as mt#4704.
The premise this task was filed on was false, and was corrected at planning
The spec claimed every detector in the family evaluates one turn in isolation, so the pair was
"structurally invisible". Two shipped detectors already evaluate multi-turn windows —
pre-narration-detector.ts(TRAILING_WINDOW_TURNS = 12, mt#2671) even correlates a claim's named PRagainst in-window evidence via
buildIdentityEvidence. The real gap was a missing predicate, not amissing scope. That correction is what turned this from "build a conversation-scoped claim ledger"
into "reuse the window that exists" — v1 adds no persistent state at all.
The load-bearing design choice
The suppressor counts a subject resolved only by a tool call strictly after the hedge turn —
window
(hedgeTurn, assertionTurn]. Measured against the originating transcript: the hedge turn's ownmemory_get {id: "mem#1323"}returnedsourceAgentId: null, which is the call that created theuncertainty. Counting it would make the detector inert on the case that produced it (mem#704). Turns
4–5 carry zero tool calls naming the subject → the turn-5 restatement fires; turn 6's correction runs
the discriminating grep in its own turn → correctly suppressed.
Key changes
packages/domain/src/detectors/cross-turn-hedge.ts(new) — the matcher. Elider injected perADR-024 Rung 1 (a domain module must not import from the hooks tree); subject keys restricted to
decidable entity refs; two separately-measured marker legs.
.minsky/hooks/cross-turn-hedge-detector.ts(new) — the adapter. Segments the window, supplieselideBlocksAndQuotes, writes an evaluation record for every window scanned..minsky/hooks/transcript.ts—windowSlicehoisted frompre-narration-detector.tsintothe module whose own header calls itself "the single definition of the turn-boundary logic".
GUARD_REGISTRYviaregistry-prompt-scan-guards.ts,interceptor-descriptions.ts,interceptor-coordinates.ts,hook-module-inventory.md(two tables + three counts), the deriveddispatcher timeout in
.claude/settings.json(230s → 240s), and the env-var pair.enforcement-mapping.tsneeds no entry — that census coverssettings.json-registered hooks, and nosibling dispatcher-guard appears there.
Two defects the tests forced, kept rather than papered over
\b-anchored file-path pattern silently dropped the leading dot from.minsky/…paths, recordingminsky/hooks/x.ts— a key that no longer denotes the file it came from.windowSlice(lines, 0)collapses to the last turn, not the whole transcript — the opposite ofthe safe direction for a look-back. Documented and pinned by a test rather than "fixed", so
migrating
pre-narration-detector.tsonto the hoisted export stays a deletion rather than asemantic change.
Spec-decision reconciliation
SC4 is partially met and this is the honest account. It asks that the window size be "derived from
measured hedge→restatement distance in the calibration data". There is no calibration data yet — this
PR is what starts collecting it. The one measured instance is a gap of 2 (the originating
incident).
WINDOW_TURNS = 8is therefore reasoned, not measured, and is labelled provisional in itsown docblock with the reasoning and the single data point named.
hedgeGapTurnsis recorded on everyevaluation precisely so the first review has the distribution to set it from. Deliberately not copied
from either shipped constant (12, 5) — those were derived for different questions.
Parallel-work reframe
session_startwas denied byparallel-work-guard: open PR #3412 changespre-narration-detector.ts. Confirmed against its actual 313-file changed-file list;transcript.tsisnot among them. Disposition was REFRAME, not an override — hoisting to
transcript.tsreaches thesame design end state on a file with no collision.
pre-narration-detector.tskeeps its privatewindowSliceuntil #3412 merges; migration is tracked in the spec with a 2026-09-01 threshold.Testing
Execution evidence:
SC1 / SC5 — the matcher, both legs, and the finding shape:
SC2 / SC3 / AT1–AT6 — the adapter, the window, and every negative control:
AT1 (replay fires, citing both turns) is
evaluateWindow — fires on the hedge/restatement pair with no intervening lookup, assertinghedgeGapTurns === [2]. AT2 (probe between → no finding) isa post-hedge lookup suppresses. AT3, AT4 and the two elision controls are thenegative controlsblockin the matcher suite. The self-report control — turn 6's "I flagged that inference as uncertain, then
restated it as fact a message later", which carries a hedge marker AND an entity ref in one sentence
and is the exemplary behaviour — is
a self-report that re-hedges while naming the subject is not an assertion.SC6 — calibration-first, both graduation obligations bound:
The canary asserts
calibration, notadditionalContext, so a laterINJECTION_ENABLEDflip gains anoutcome rather than breaking one. Both obligations bind through
calibrationLog: "cross-turn-hedge"and are now stated in the module header.
The registry census gate — the one that actually found the six registries:
Hooks tree + full gated suite:
Typecheck — clean across 8 projects (
.,packages/domain,packages/shared,services/reviewer,services/site,src/cockpit/web,tsconfig.hooks.json,tsconfig.scripts.json), validated against the session workspace.infra/tsconfig.jsonskipped withits documented reason. Lint — 0 errors, 0 warnings across 4147 files.
Negative control — the suppressor's hedge-turn exclusion is load-bearing, and the tests discriminate on it.
The central design claim is that the hedge turn's OWN lookup must not count as resolving. Flipping
turnIndex <= hedge.turnIndextoturnIndex <(i.e. counting it) and re-running both suites:Reverted; 40 pass. This is the mem#704 check applied to the detector itself — a probe that returns the
same answer whether or not the mechanism works is not evidence.
Deploy verification: this PR changes deploy surface.
isDeploySurfaceFilewas RUN over the changedfiles rather than recalled — it returns true for
packages/domain/src/detectors/cross-turn-hedge.tsand
packages/domain/src/configuration/sources/environment.ts, and false for every.minsky/hooks/**,.claude/settings.jsonand docs path. So no[no-deploy-impact]tag. The changeadds a new module and one env-var registration with no behavioural change to any request path — but
that is a claim about intent, not evidence, so post-merge I will wait on the deployment bound to this
merge (
notBefore= merge time,expectCommitSha= merge SHA) and assert the health body's serviceidentity rather than the status code.
Follow-ups filed, not deferred silently
verified-*with no backing call), which this PRsurfaced as never-filed.
pre-narration-detector.tsonto the hoistedwindowSliceonce PR feat(mt#4639): Convert 614 log sites to getLoggableErrorSummary and flip the rule to error #3412 merges — trackedin mt#4701's spec with a dated escalation threshold rather than left as a code comment.