fix(mt#4810): Read the claim's subject from its sentence, not its matched token - #3518
Conversation
…ched token [no-deploy-impact] The pre-narration correlation read `matches[].phrase`. For the dominant `review-approved` pattern that phrase is the bare token `APPROVED`, which carries no subject, so such a claim could never be identity-backed. The record has carried the containing sentence as `context` since mt#3198 and nothing read it. The call site now reads phrase first, then the sentence, with a uniqueness guard: a sentence naming two PRs stays ambiguous and the claim fires. That is PR #3484 R1's blocking finding in miniature - a merge of one PR must not back a claim about another. `matches[].phrase` is untouched, so mt#4074's diversity-axis constraint holds by construction rather than by design. `scripts/diagnose-pre-narration-window.ts` keeps a duplicate of this correlation, and its own comment says a divergence there would misreport the very fix it exists to measure. Updated in the same commit. Measured: 73 firing -> 73 firing over the union calibration corpus. The tune does NOT move the number, and mt#4810 SC5's condition has fired. The subject was never the binding constraint: the 7 fires whose sentence names a PR are all replayable, and none has an in-window merge of that PR to correlate against. Deploy verification: not applicable - isDeploySurfaceFile returns false for all four changed files.
Minsky Reviewer StatusReview in progress... |
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
Solid, disciplined change: subject extraction now reads the matched sentence and records a distinct suppression reason, with tests and the diagnostic script kept in lockstep. Notably, SC2 is explicitly not met and the PR documents that outcome per SC5. I found two non-blocking concerns: (1) subject extraction operates on elided text, so PR numbers appearing only in quoted spans within the sentence are blanked and become undiscoverable; sourcing from the raw pre-elision context (or trying both) would avoid losing identity evidence. (2) The sentence matcher requires the literal “PR” token; if bare “#1234” or numeric forms occur in-context, they will be missed — clarify intent or expand carefully while maintaining the uniqueness guard. Documentation impact appears nil; there’s no doc for this hook and no posture flip. Overall, the PR is mergeable as-is with the above caveats noted.
Findings
- [NON-BLOCKING] .minsky/hooks/pre-narration-detector.ts:720 — Subject extraction runs on elided context — quoted PR numbers are blanked and become undiscoverable
IndetectPreNarrationWithSuppression, thecontextpassed toextractUniqueClaimedPrNumber(matched.context)is produced byextractMatchContext(text, …), wheretextis the ELIDED assistant prose (elideMarkdownContexts+elideDoubleQuotedSpans). This means PR numbers that appear only inside quotes in the containing sentence will have been replaced with whitespace before subject extraction runs, soextractUniqueClaimedPrNumberwill never see them. That narrows the new correlation path unintentionally and can mask legitimate identity-backed cases (e.g.,APPROVED — "PR #123"will lose the#123). Consider sourcing the subject from the UN-elided raw context (or running the extraction over both elided and raw forms) while keeping elision for the pattern match itself. This preserves the anti-quote FP property for matching without discarding potentially-usable identity evidence. - [NON-BLOCKING] .minsky/hooks/pre-narration-detector.ts:221 — Regex for sentence-based PR extraction ignores bare-number forms without the 'PR' label
extractUniqueClaimedPrNumberonly matches/\bPR\s*#?(\d+)/gi. Some sentences may name a PR number without thePRtoken (e.g., “Approved — #3484 is good to go.”, “Approved — 3484 passed,” or repo conventions that use just “#1234”). The code comment says it handles “either spelling” in tests, but the implementation requires thePRprefix, so those bare-number shapes are missed. If intentional, add a comment clarifying the intended scope; otherwise, consider broadening the pattern (e.g., also accept/#(\d+)/when clearly in a PR context) with appropriate ambiguity guards to preserve the uniqueness constraint.
Spec verification
| Criterion | Status | Evidence |
|---|---|---|
| A direction is chosen and the reason recorded, against the leak decomposition above rather than against a fresh guess. | Met | Reasoning and decision are embedded in code comments and PR description; the implementation chooses read-site (sentence) extraction via extractUniqueClaimedPrNumber and documents the ceiling. Evidence in .minsky/hooks/pre-narration-detector.ts:210-236 (new function docblock) and the call-site change at :713-723. |
Whichever lands, re-run scripts/diagnose-pre-narration-window.ts and record before/after. The bar mt#4498 failed is the one that matters: the firing count must actually move. |
Not Met | PR body explicitly states “The tune did not move the number: 73 firing → 73 firing,” and updates scripts/diagnose-pre-narration-window.ts to mirror the detector’s two-step subject extraction (extractUniqueClaimedPrNumber), but the before/after counts are unchanged. A follow-up posture decision is filed as mt#4819. Code evidence: scripts/diagnose-pre-narration-window.ts:556-563 mirrors the change; execution evidence in PR description records no movement. |
Suppressed count does not fall, and no claim stops matching — a tune achieved by suppressing everything is the failure mode (mt#4498's ## Outcome shows what this looks like when it nearly happened). |
Met | PR description shows unchanged suppressed (21) and no-longer-matched (2) counts on the same corpus before/after. The detector’s logic preserves match-first semantics and only changes identity-correlation source; no broadening of requiredTools occurs. No code path was added that would stop matching outright; see unchanged OUTCOME_CATEGORIES and pattern list. |
| A negative control per new suppression path: the same claim without its evidence still fires. | Met | New tests in .minsky/hooks/pre-narration-detector.test.ts:1169-1211 include two negative controls: (1) evidence for a different PR does not suppress; (2) a sentence naming two PRs is treated as ambiguous and still fires. These assert the new path does not overreach. |
| If the chosen direction ALSO fails to move the number, say so and stop rather than iterating — that would be the third tune on this detector and the signal to reconsider the detector, not the patterns. | Met | PR body explicitly records SC2 as not met and defers posture/next steps to mt#4819 rather than iterating further. No further code churn; only the read-site change and mirrored diagnostic change shipped. |
Adoption sweep
| Symbol | Kind | Consumers found | Classification | Notes |
|---|---|---|---|---|
| .minsky/hooks/pre-narration-detector.extractUniqueClaimedPrNumber | function | .minsky/hooks/pre-narration-detector.ts:720 — used to extract PR number from matched sentence when phrase lacks one, scripts/diagnose-pre-narration-window.ts:559 — mirrored in diagnostic script to avoid measurement drift, .claude/hooks/pre-narration-detector.ts:723 — generated mirror uses it identically | Adopted | New public export used immediately in the same module and mirrored in the generated hook and diagnostic script per design comments. |
Documentation impact
- no-update-needed — Behavioral change is internal to detection/suppression logic (source of subject for identity correlation). No CLI, config, or user-facing contract changed; exported API surface adds a helper but module semantics remain the same, and no docs file for this hook exists under
docs/architecture/hooks/(verified — nopre-narration-detector.md). The PR does not flip posture. Therefore no docs updates are required.
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
Verification pass: prior NON-BLOCKING findings still apply, but there were no prior BLOCKING findings to re-check, and this fix commit introduces no new critical defects. The read-site change is implemented correctly: sentence-based subject extraction (extractUniqueClaimedPrNumber) with a conservative uniqueness guard, wired phrase-first/sentence-fallback, and a distinct suppression reason. Tests exercise the new path and its negative controls; the diagnostic script mirrors the logic to avoid measurement drift. I’m leaving two NON-BLOCKING notes: (1) subject extraction runs on the elided sentence, so PR numbers appearing only inside double-quoted spans are blanked and unreachable; consider attempting extraction against the raw sentence too. (2) the sentence extractor requires a literal “PR” token and will miss bare “#1234” shapes; if intentional, document; otherwise consider broadening carefully while preserving uniqueness. With SC2 explicitly not met but recorded per SC5, and no new critical issues found, this PR is approved.
Findings
- [NON-BLOCKING] .minsky/hooks/pre-narration-detector.ts:328 — Subject extraction still runs on ELIDED context, so quoted PR numbers in the sentence are blanked and undiscoverable
IndetectPreNarrationWithSuppression, thecontextused forextractUniqueClaimedPrNumber(matched.context)is produced fromtext, which iselideDoubleQuotedSpans(elideMarkdownContexts(rawText))(.minsky/hooks/pre-narration-detector.ts:300-309). This means any PR numbers appearing only inside double-quoted spans within the containing sentence will have been replaced with whitespace before subject extraction runs. As a result, identity-scoped suppression can miss legitimate cases where the only subject mention is quoted (e.g.,APPROVED — "PR #123"). Consider sourcing the subject from the un-elided raw sentence (or attempting extraction against both raw and elided forms) while retaining elision for the pattern match itself, so you keep the anti-quote FP property for matching without discarding potentially-useful identity evidence. - [NON-BLOCKING] .minsky/hooks/pre-narration-detector.ts:230 — Sentence-based PR extractor requires a literal “PR” token; bare “#1234” or numeric-only mentions are ignored
extractUniqueClaimedPrNumberscans with/\bPR\s*#?(\d+)/gi, so it will not recognize cases where the containing sentence names a PR only as “#3484” (or a repo-convention bare number) without thePRlabel. Tests cover “PR 3484” and “PR #3484” but not “#3484”. If this omission is intentional (to avoid ambiguous issue refs), consider documenting that scope at the function docblock. Otherwise, consider carefully broadening the pattern (e.g., also accepting/#(\d+)/when clearly in a PR-context clause) while preserving the uniqueness guard to avoid accidental cross-pairing.
Spec verification
| Criterion | Status | Evidence |
|---|---|---|
| A direction is chosen and the reason recorded, against the leak decomposition above rather than against a fresh guess. | Met | Direction (read-site subject from sentence) is implemented and documented in-code. See .minsky/hooks/pre-narration-detector.ts:218-247 (extractUniqueClaimedPrNumber docblock) and its invocation at .minsky/hooks/pre-narration-detector.ts:365-383 (phrase-first, sentence-fallback). PR body records rationale and ceiling. |
Whichever lands, re-run scripts/diagnose-pre-narration-window.ts and record before/after. The bar mt#4498 failed is the one that matters: the firing count must actually move. |
Not Met | PR body records: "The tune did not move the number: 73 firing → 73 firing." The diagnostic script mirrors the new two-step subject extraction at scripts/diagnose-pre-narration-window.ts:556-563, but before/after counts are unchanged. |
Suppressed count does not fall, and no claim stops matching — a tune achieved by suppressing everything is the failure mode (mt#4498's ## Outcome shows what this looks like when it nearly happened). |
Met | PR body reports suppressed (21) and no-longer-matched (2) unchanged before/after over the same corpus. Code preserves category patterns and only changes identity-correlation source; see unchanged OUTCOME_CATEGORIES and suppression gating at .minsky/hooks/pre-narration-detector.ts:334-363 and :385-418. |
| A negative control per new suppression path: the same claim without its evidence still fires. | Met | New tests at .minsky/hooks/pre-narration-detector.test.ts:1188-1211 verify two negative controls: (1) evidence for a different PR does not suppress; (2) sentence naming two PRs remains ambiguous and fires. |
| If the chosen direction ALSO fails to move the number, say so and stop rather than iterating — that would be the third tune on this detector and the signal to reconsider the detector, not the patterns. | Met | PR body explicitly acknowledges SC2 not met and records the stop decision with follow-up (mt#4819). No further widening attempted; only the read-site change and mirrored diagnostic update shipped. |
Adoption sweep
| Symbol | Kind | Consumers found | Classification | Notes |
|---|---|---|---|---|
| .minsky/hooks/pre-narration-detector.extractUniqueClaimedPrNumber | function | .minsky/hooks/pre-narration-detector.ts:365-383 — used at the correlation call site (phrase-first, sentence-fallback), scripts/diagnose-pre-narration-window.ts:556-563 — mirrored in diagnostic script to avoid drift, .claude/hooks/pre-narration-detector.ts:372-390 — generated mirror uses it identically | Adopted | Helper is exported and immediately consumed by the detector and its diagnostic script; no external wiring required by the spec. |
Documentation impact
- no-update-needed — Internal behavior change to suppression identity-correlation source; no CLI flags, config, or operator-facing posture changes. No
docs/architecture/hooks/pre-narration-detector.mdexists (checkeddocs/architecture/hooks/), and posture (INJECTION_ENABLED) remains false. Therefore no documentation updates are required.
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
Verification round: no prior BLOCKING findings to re-check; this iteration introduces no new critical issues. The detector now sources the claim subject from the matched sentence via extractUniqueClaimedPrNumber, wired phrase-first/sentence-fallback, with a distinct suppression reason and mirrored logic in the diagnostic script. Tests cover the new path and negative controls. SC1/SC3/SC4/SC5 are Met; SC2 is explicitly Not Met and recorded per the spec’s stop condition. One NON-BLOCKING observation remains: subject extraction runs over elided text, so quoted-only PR numbers are blanked; consider a raw-sentence fallback. Documentation impact: none (internal change, no posture flip). Approved to merge.
Findings
- [NON-BLOCKING] .minsky/hooks/pre-narration-detector.ts:345 — Subject extraction still runs on ELIDED context; quoted PR numbers in the sentence are blanked and undiscoverable
extractUniqueClaimedPrNumber(matched.context)reads frommatched.context, which is sourced viaextractMatchContext(text, …)wheretextis the ELIDED assistant prose (elideMarkdownContexts+elideDoubleQuotedSpans). Any PR numbers appearing only inside double-quoted spans within the containing sentence will have been replaced with whitespace before subject extraction runs, so identity-scoped suppression can miss legitimate cases where the only subject mention is quoted (e.g.,APPROVED — "PR #123"). Consider attempting extraction against the raw pre-elision sentence as a fallback (or capturing an un-elidedrawContextalongsidecontext) while keeping elision for the pattern match itself, so you preserve the anti-quote FP benefit for matching without discarding potentially-useful identity evidence.
Spec verification
| Criterion | Status | Evidence |
|---|---|---|
| A direction is chosen and the reason recorded, against the leak decomposition above rather than against a fresh guess. | Met | Direction (read-site subject from sentence) implemented and documented inline. See .minsky/hooks/pre-narration-detector.ts:218-247 (docblock for extractUniqueClaimedPrNumber) and its use at .minsky/hooks/pre-narration-detector.ts:365-383 (phrase-first, sentence-fallback). PR body explains rationale and ceiling. |
Whichever lands, re-run scripts/diagnose-pre-narration-window.ts and record before/after. The bar mt#4498 failed is the one that matters: the firing count must actually move. |
Not Met | PR body states: “The tune did not move the number: 73 firing → 73 firing.” The diagnostic script mirrors the two-step subject extraction at scripts/diagnose-pre-narration-window.ts:556-563, but the before/after counts are unchanged. Follow-up posture decision filed as mt#4819. |
Suppressed count does not fall, and no claim stops matching — a tune achieved by suppressing everything is the failure mode (mt#4498's ## Outcome shows what this looks like when it nearly happened). |
Met | PR body records suppressed (21) and no-longer-matched (2) unchanged before/after on the same corpus. Implementation only changes the identity-correlation source; category patterns and suppression gating remain intact (see .minsky/hooks/pre-narration-detector.ts:334-363, :385-418). |
| A negative control per new suppression path: the same claim without its evidence still fires. | Met | Tests at .minsky/hooks/pre-narration-detector.test.ts:1188-1251 include two negative controls: evidence for a different PR does not suppress; a sentence naming two PRs remains ambiguous and fires. |
| If the chosen direction ALSO fails to move the number, say so and stop rather than iterating — that would be the third tune on this detector and the signal to reconsider the detector, not the patterns. | Met | PR body explicitly acknowledges SC2 not met and records the stop decision (no further widening), with follow-up filed as mt#4819. No additional mechanism churn beyond the read-site change and mirrored diagnostic update. |
Adoption sweep
| Symbol | Kind | Consumers found | Classification | Notes |
|---|---|---|---|---|
| .minsky/hooks/pre-narration-detector.extractUniqueClaimedPrNumber | function | .minsky/hooks/pre-narration-detector.ts:365-383 — used at correlation call site (phrase-first, sentence-fallback), scripts/diagnose-pre-narration-window.ts:556-563 — mirrored in diagnostic script to avoid drift, .claude/hooks/pre-narration-detector.ts:372-390 — generated mirror uses it identically | Adopted | Helper is exported and immediately consumed by the detector, its generated mirror, and the diagnostic script. |
Documentation impact
- no-update-needed — Change is internal to detection logic (source of identity-correlation subject: phrase-first, sentence-fallback). No CLI flags, config, or operator posture changed; no
docs/architecture/hooks/pre-narration-detector.mdexists (checkeddocs/architecture/hooks/). Diagnostic script updated to mirror behavior. Therefore, no documentation updates are required.
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
Verification complete: the change reads a claim’s subject from its matched sentence (via extractUniqueClaimedPrNumber) and wires it phrase-first/sentence-fallback with a conservative uniqueness guard. The diagnostic script and generated hook mirror the logic to prevent measurement drift; a distinct suppression reason makes this path measurable. Tests exercise the new path and negative controls. SC1/SC3/SC4/SC5 are Met; SC2 is explicitly Not Met and recorded per the spec’s stop condition. No prior BLOCKING items to re-verify, and no new critical defects found in this diff. Approving as correct and safe to merge.
Spec verification
| Criterion | Status | Evidence |
|---|---|---|
| A direction is chosen and the reason recorded, against the leak decomposition above rather than against a fresh guess. | Met | The implementation adopts the read-site approach and documents it inline. See .minsky/hooks/pre-narration-detector.ts:196-233 for the extractUniqueClaimedPrNumber docblock explaining the rationale and ceiling, and its use at .minsky/hooks/pre-narration-detector.ts:673-681 (phrase-first, sentence-fallback). The same helper and rationale are mirrored in the generated hook at .claude/hooks/pre-narration-detector.ts:199-236 and used at .claude/hooks/pre-narration-detector.ts:676-684. |
Whichever lands, re-run scripts/diagnose-pre-narration-window.ts and record before/after. The bar mt#4498 failed is the one that matters: the firing count must actually move. |
Not Met | The PR body records: “The tune did not move the number: 73 firing → 73 firing.” The diagnostic script mirrors the detector’s two-step extraction at scripts/diagnose-pre-narration-window.ts:556-563, ensuring the replay measures the intended behavior, but the before/after counts remain unchanged. |
Suppressed count does not fall, and no claim stops matching — a tune achieved by suppressing everything is the failure mode (mt#4498's ## Outcome shows what this looks like when it nearly happened). |
Met | The behavior change is limited to identity-correlation source selection and a new suppression reason; pattern lists and outcome categories are unchanged. The code distinguishes SUPPRESSION_IDENTITY_SCOPED_CONTEXT at .minsky/hooks/pre-narration-detector.ts:689-699, preserving existing reasons otherwise. The PR description reports suppressed (21) and no-longer-matched (2) unchanged on the same corpus. |
| A negative control per new suppression path: the same claim without its evidence still fires. | Met | New tests assert non-overreach: ambiguous sentences and evidence for a different PR still fire. See .minsky/hooks/pre-narration-detector.test.ts:1207-1251 — tests “NEGATIVE CONTROL: evidence for a DIFFERENT PR does not silence the claim” and “NEGATIVE CONTROL: a sentence naming two PRs stays ambiguous and fires.” |
| If the chosen direction ALSO fails to move the number, say so and stop rather than iterating — that would be the third tune on this detector and the signal to reconsider the detector, not the patterns. | Met | The PR explicitly records that SC2 did not move (73→73) and stops per SC5, deferring the posture decision to mt#4819. No further widening or iteration is attempted in code; only the read-site change and mirrored diagnostic update are present. |
Adoption sweep
| Symbol | Kind | Consumers found | Classification | Notes |
|---|---|---|---|---|
| .minsky/hooks/pre-narration-detector.extractUniqueClaimedPrNumber | function | .minsky/hooks/pre-narration-detector.ts — used in detectPreNarrationWithSuppression (phrase-first, sentence-fallback), scripts/diagnose-pre-narration-window.ts — mirrored in diagnostic script to avoid drift, .claude/hooks/pre-narration-detector.ts — generated mirror uses it identically |
Adopted | Helper is exported and immediately consumed by the detector and the diagnostic script; no external wiring required by the spec. |
Documentation impact
- no-update-needed — Internal logic change only: reads claim subject from the matched sentence with a new suppression reason. No CLI flags, config, or operator posture changed (INJECTION_ENABLED remains false). There is no dedicated doc for this hook under
docs/architecture/hooks/(checked directory listing). Diagnostic script updated solely to mirror detector behavior; no user-facing semantics altered.
Summary
pre-narration's identity correlation readmatches[].phrase. For the dominantreview-approvedpattern that phrase is the bare token
APPROVED, which carries no subject — so such a claim couldnever be identity-backed, and the merge evidence mt#4498 added to
identityScopedToolswas inertfor it. The record has carried the containing sentence as
contextsince mt#3198, and nothing readit.
The headline number did not move: 73 firing → 73 firing. mt#4810's SC2 asks for movement and
does not get it. That is reported here rather than worked around, and mt#4810's SC5 names this
outcome in advance as the signal to stop — so the follow-up is a posture decision, not a fourth
tune. Filed as mt#4819.
Key changes
extractUniqueClaimedPrNumber— the PR number a claim's SENTENCE names, or null when it namesnone or more than one. The uniqueness guard keeps PR fix(mt#4498): Scope the pre-narration merge evidence and reach the second MCP alias #3484 R1's finding closed: a sentence
naming two PRs is ambiguous, so it yields nothing and the claim fires. For a suppressor the safe
degrade direction is MORE fires (ADR-024's fail-to-Rung-1 invariant).
matches[].phraseis untouched, somt#4074's SC3 constraint (
extractDistinctPhraseskeys the review-due diversity axis on thatfield) holds by construction rather than by careful design.
SUPPRESSION_IDENTITY_SCOPED_CONTEXT— a distinct suppression reason, so this path's delta ismeasurable rather than inferred from an unchanged total (mem#1208).
scripts/diagnose-pre-narration-window.tskeeps a duplicate of this correlation, and its owncomment says a divergence there "would misreport the very fix this script exists to measure."
This change would have created exactly that divergence; the script is updated in the same commit.
Why the number did not move
Suppression needs a subject and evidence to correlate it against. Both halves were measured
separately for the first time, over the union corpus (1124 records; 177 recorded fires; 96
phrase-anchored):
review-approvedfires, 22 (71%) name no subject at all — no PR number, notask id. No subject-capture fix reaches them at any window width. This ceiling was stated before
implementing.
fallback ran against them. None was suppressed: the named PR has no in-window
session_pr_merge/merge_pull_requestto correlate against.identity-backedis 0 across the still-firing set, before and after — including after thediagnostic script was taught the same two-step, so this is not a measurement artifact.
The leak decomposition in mt#4810 mis-attributes the cause. The gap is not "the claim's subject is
unreachable"; it is "there is no evidence in the window to correlate a subject against."
Direction (b) (window width) does not help either — those windows are empty, not narrow.
Corpus note (mem#1236). mt#4748 moved calibration telemetry to
~/.local/state/minsky/projects/<hash>/. The repo-local.minsky/pre-narration-calibration.jsonlstill returns real-looking records that stop silently at the cutover. The first measurement in this
session read only that frozen half and under-reported the firing count by 3; every figure above is
over the union of the frozen and live logs, which are contiguous and non-overlapping.
Testing
Execution evidence:
AT1 (replay before/after on the same log) — union corpus,
bun scripts/diagnose-pre-narration-window.ts --log <union>:AT3 (suppressed count did not decrease; matched count did not decrease) — satisfied by the same
run: suppressed holds at 21, no-longer-matched holds at 2.
Unit + integration,
bun test --preload ./tests/setup.ts ./.minsky/hooks/pre-narration-detector.test.ts:73 of those 79 predate this PR; 6 are new.
AT2 — negative control, below.
Negative control: the two-step call site reverted to the phrase-only read
Stated honestly: the two tests named NEGATIVE CONTROL stay green in both states by design — they
assert the claim still FIRES (evidence for a different PR; a sentence naming two PRs), which is also
the pre-fix behaviour. They guard the fix against over-reach; they do not demonstrate it. The one
red-to-green test above is what the control actually proves. The revert was of the behavioural
change only (both edits together, so nothing half-reverted); a full file revert additionally breaks
the test file's imports.
Typecheck: 8 projects, 0 errors (session workspace, incl.
tsconfig.hooks.jsonandtsconfig.scripts.json). Lint: 4246 files, 0 errors.Success criteria
## Measurement and direction decision.before/after recorded; the count did not move. NOT MET.
met: this PR says so, and mt#4819 carries the posture decision instead of a fourth tune.
Deploy verification: not applicable.
isDeploySurfaceFilereturns false for all four changed files(
.minsky/hooks/pre-narration-detector.ts, its test, the generated.claude/hooks/mirror,scripts/diagnose-pre-narration-window.ts).