Skip to content

Commit c396e34

Browse files
feat(mt#4393): Join the observer roster to the interceptor catalog, and close the real gap
## Summary mt#4393 was filed on a subtraction: catalog `detector`/`injector` entries minus roster entries, read as "roughly 13 observers with no roster entry." That number is an artifact, because the two artifacts **share no join key and do not enumerate the same population.** This PR builds the join, measures the real gap, and closes it. - **No join key.** The roster labels entries in prose; the catalog keys on `guardName`. `- **Subagent model verification**` is `verify-subagent-model`. - **Many-to-one.** `- **Injection (per-turn)**` documents four hooks; `- **Operator deferral**` says "Six surfaces". A count difference is off by at least six before it starts. - **Wrong population, both directions.** `detector ∪ injector` includes blocking merge gates and excludes the recorders and stampers the roster already documents (`families: []`). The roster's real population is what its header states — non-blocking, i.e. `interventions` carrying no `deny`. ## Criterion accounting - **SC1** — **discharged.** Every non-blocking interceptor now has a roster entry or a reasoned exemption. The criterion itself was **amended** (R1): it required the `detector ∪ injector` population, which is wrong in both directions, and the spec now records why. - **SC2** (every roster entry carries a `Detail:` pointer) — **`[sc2-deferred: mt#4992]`** for the 19 pre-existing entries. The 14 entries this PR ADDS each carry an explicit no-page-yet note, so it does not add to that debt (R3). - **SC3** — **discharged.** `audit:observer-roster` runs enforcing and exits 0; the enforcing form joins `audit:interceptors`, already run at `ci.yml:338` and in `validate-all`. - **SC4** (measurement re-run and recorded, not asserted) — **discharged**, against current main. ## What ships - **`scripts/observer-roster-map.ts`** — the join: roster label → `guardName[]`, 19 exemptions each with a reason, and `ROSTER_NO_CATALOG_PEER` for the inverse direction. - **`scripts/audit-observer-roster.ts`** — the reconciliation, seven classes in both directions. - **`scripts/audit-observer-roster.test.ts`** — 19 tests. - **14 new roster entries** in `hook-observers.mdc`, plus compile outputs. ## The 20 gaps were not 20 — working them individually changed the answer - **Four were already documented** inside entries whose text names them — a many-to-one the map missed. Map fix, no roster change. - **Two were MIS-ATTRIBUTED, and this is the find that mattered.** `Agent-dispatch record` mapped to `record-subagent-invocation`, and `SubagentStop recording` mapped to nothing. They are swapped. **A wrong mapping is worse than a missing one — it reports clean.** - **One is plumbing** (`record-turn-anchor`), now exempt with that reason. - **Fourteen were genuinely undocumented** and now have entries. ## `Consumer-account` needed the inverse declaration It has no catalog peer by design — it rides `require-execution-evidence-before-merge` as that gate's fifth calibration surface (mt#4493). Left alone it is a permanent finding, and **a check that can never reach zero is one nobody can gate on.** `ROSTER_NO_CATALOG_PEER` declares it with a reason; a declaration that contradicts the map is its own finding class. ## Three defects found **In the check, while writing it.** A label mapped to an EMPTY guard list PASSED — silently, for exactly the case the class exists to catch. **In the check, at review.** `parseRosterLabels` split on a bare `---`, so a horizontal rule in the body truncated the label list — in the direction that reports FEWER labels and misses drift. **In the repo.** `hook-observers.mdc` still documented `Stop-at-decision`, whose detector mt#4978 retired the same day (`eece1b535`). Removed here — exactly the class mt#4852 was filed for. ## Execution evidence **AT1 — the check fails against the uncovered tree and passes once covered. Both runs recorded; the failing run is the negative control.** Note AT1's "13 uncovered entries" is itself the artifact of the subtraction this task disproved; the real figure under the join was 22 findings. # BEFORE (this branch, rebased onto current main, coverage not yet written) $ bun scripts/audit-observer-roster.ts --report [observer-roster] 59 roster entries against 101 non-blocking catalog entries (18 exempt) 20 non-blocking interceptors with no roster entry and no exemption 2 roster entries the catalog does not enumerate at all $ bun scripts/audit-observer-roster.ts # enforcing ENFORCING EXIT=1 # AFTER (14 entries, 4 map corrections, 1 exemption, 1 no-peer declaration) $ bun scripts/audit-observer-roster.ts [observer-roster] 73 roster entries against 101 non-blocking catalog entries (19 exempt) [observer-roster] reconciled — no gaps. EXIT=0 **AT2 — a synthetic non-blocking catalog entry with no roster entry makes the check fail**, so it detects the class rather than passing on a healthy tree. Asserted as a named unit test: (pass) reconcile > AT2 — a NON-BLOCKING catalog entry with no roster entry and no exemption is reported A second negative control at the integration level: removing one mapped entry from the REAL roster failed exactly the integrity test and only that one — (fail) SC3 ... > the four join-integrity classes are empty 16 pass / 1 fail — after which the file was restored and verified clean. **AT3 — compile stays clean and the roster still compiles with its `paths:` frontmatter intact.** $ bun run minsky compile [compile] Target "claude-rules": 17 file(s) written [compile] Target "claude-hooks": 184 file(s) written $ head -3 .claude/rules/hook-observers.md --- paths: ['.minsky/hooks/**', '.claude/hooks/**'] --- **Full suites:** $ bun test --preload ./tests/setup.ts --timeout=15000 ./scripts/audit-observer-roster.test.ts 19 pass / 0 fail / 30 expect() calls Ran 19 tests across 1 file. $ bun scripts/run-related-tests.ts .minsky/rules/hook-observers.mdc scripts/observer-roster-map.ts scripts/audit-observer-roster.ts 89 pass / 0 fail / 193 expect() calls Ran 89 tests across 4 files. $ bun run audit:interceptors # the CI step, now carrying the roster check UNRESOLVED : 0 / OK / [observer-roster] reconciled — no gaps. `reconcile` now takes its maps as a parameter, defaulting to `SHIPPED_MAPS` — several classes are by design empty against the shipped maps, so a test that can only see them passes because the input is clean rather than because the logic is right. Typecheck clean over 8 projects (`validatedWorkspace` confirmed as the session). Lint clean over 4,384 files, 0 errors, 0 warnings. ## Deploy verification `isDeploySurfaceFile` returns **true for `package.json`** — run over the actual changed-file list, not recalled — so this PR IS deploy surface and no `[no-deploy-impact]` tag applies. The change to that file is script-only (`audit:interceptors` gains the roster check; `audit:observer-roster` drops `--report` and gains a `:report` sibling), which does not touch the runtime, but that is a reason to expect the deploy to be clean, not a reason to skip checking it. **After merge I will run `mcp__minsky__deployment_wait-for-latest` and confirm SUCCESS plus a started runtime** before treating mt#4393 as done. A tool or auth flake is a blocker to reconnect and retry, not a licence to defer; "applied" is the action, not the outcome. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: minsky-ai[bot] <minsky-ai[bot]@users.noreply.github.com>
2 parents 8b4e9c2 + a278294 commit c396e34

7 files changed

Lines changed: 578 additions & 5 deletions

File tree

.claude/rules/hook-observers.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ Detail: `guard-dispatcher-framework.md`.
5555
- **Secret-request-in-chat** (mt#2428) — the assistant asking the principal to hand over a secret through the conversation ("paste your bot token here"); the transcript is persisted AND ingested, so `credentials.request` is the surface. Suppresses the agent REFUSING, and prose DESCRIBING the antipattern — the dominant FP class, since the corpus discusses it at length. Carved from operator-deferral by verb class: deposit verbs and generic-verb-plus-recipient here, bare `provide the <secret>` stays a deferral. Calibration-first. `MINSKY_SKIP_SECRET_REQUEST_IN_CHAT`. Detail: `secret-request-in-chat-detector.md`.
5656
- **Turn-end-bare-ref-scan** — Stop scan (mt#3286): the closing message carries an entity ref the reader cannot click. Per finding class: `bare-short-id`, `malformed-target`, `raw-uuid-label` LIVE, the rest RECORD-ONLY. `MINSKY_ACK_BARE_ENTITY_REF`. Detail: `turn-end-bare-ref-scan.md`.
5757
- **Turn-end-unescalated-incident** — Stop scan (mt#3593): final message reports an incident and names the remediation as the principal's, with no `asks_create` carrying `severity: "incident"`. LIVE. `MINSKY_ACK_UNESCALATED_INCIDENT`.
58-
- **Stop-at-decision** — Stop scan (mt#3653): the turn's mutations are evidence-writes and it ends minting nothing and saying nothing — the silent stop at a ripe decision. Log-only. `MINSKY_SKIP_STOP_AT_DECISION`.
5958
- **Turn-end-stale-state-assertion** (mt#4199) — Stop scan: closing message says an ask/task awaits the principal; substrate disagrees. Two classes (mt#4375): terminal state, or an ask's text declaring it resolved. Calibration-first. `MINSKY_SKIP_STALE_STATE_ASSERTION_SCAN`. Detail: `turn-end-stale-state-assertion-scan.md`.
6059
- **Ask-routing deferral** — chat-prose deferral bypassing Asks. LIVE mt#2694 (not log-only). `MINSKY_ACK_ASK_ROUTING_DEFERRAL`. **Precedence (mt#4531):** when this fires and the principal's last message is about HOW you are communicating, the principal wins — answer it and stop, do not act on this advisory. It pushes toward action; a concision complaint asks for none. R7 (mem#664) is both live at once with the wrong one winning. Not rendered in the payload (that guard's size ceiling is a measurement its own declaration says not to raise); the rule is `communication-contract.mdc §A message about how you are communicating authorizes nothing`. **First detector in this family to climb to ADR-024 Rung 2 (mt#4404):** its settled-decision SUPPRESSOR — which stops it warning you for stating a decision you already took — now has an embedding nominator beneath the pattern list, because three windows measured the same behaviour recurring in renderings no first-person pattern reaches. Opt-in, off by default: `MINSKY_ARD_RUNG2_NOMINATION`. A degraded provider suppresses nothing and still injects, so the failure mode is a false positive, never a silenced deferral. **Sibling climb SHIPPED (mt#4649):** `operator-deferral`'s permission-ask surface now carries the same rung, behind its OWN flag `MINSKY_ODD_RUNG2_NOMINATION` — separate deliberately, because ADR-024 gates each climb on its own detector's evidence and one switch would make either residual unattributable. Both ship inert; the sibling's threshold is additionally `NaN` and UNMEASURED, because replaying its log through current code cut the must-suppress corpus to three records and the principal chose to pause rather than fit a band to three points (mt#4920 owns the measurement).
6160
- **Operator deferral** — an ACTION deferred to the principal without a same-turn capability probe; sibling of ask-routing-deferral, which covers a DECISION. Six surfaces. Calibration-first (mt#2459). `MINSKY_SKIP_OPERATOR_DEFERRAL`. Detail: `operator-deferral-detector.md`.
@@ -126,4 +125,18 @@ Detail: `guard-dispatcher-framework.md`.
126125
`startGuardEventsSweepBackstop` is the correctness layer (SessionEnd unreliable, ADR-017/mt#2313).
127126
`MINSKY_SKIP_GUARD_EVENTS_INGEST_HOOK`.
128127
- **Calibration (log-only)** — causal-premise/cadence/build-claim/knowledge-acquisition. `MINSKY_ACK_*`/`MINSKY_SKIP_*`.
129-
- **Guard-health tracker** — guard failure streaks tagged `infra`/`logic`; escalation banner cools down per-session up to 1h (mt#3072). Since mt#3892 the summary carries `liveness` + `lastCleanRunAt`, so `recovered` and `dormant` are no longer one state. none. Detail: `guard-health-tracker.md`.
128+
- **Guard-health tracker** — guard failure streaks tagged `infra`/`logic`; escalation banner cools down per-session up to 1h (mt#3072). Since mt#3892 the summary carries `liveness` + `lastCleanRunAt`, so `recovered` and `dormant` are no longer one state. none. Detail: `guard-health-tracker.md`.
129+
- **Cross-turn hedge** — UserPromptSubmit: a claim HEDGED in an earlier turn and restated as FACT in a later one, with no tool call naming that subject in between — the falsifier `claim-confidence.mdc`'s warrant vocabulary previously had none of. Subjects are decidable entity refs only, and the hedge turn's OWN lookup deliberately does not count as resolving, since it is usually what produced the hedge. Log-only. `MINSKY_ACK_CROSS_TURN_HEDGE`. No detail page yet — this entry is the whole documentation; a page is owned by mt#4992.
130+
- **Pre-narration** — a turn that states a tool outcome — created, merged, tests pass — before that result is in hand. Log-only. `MINSKY_ACK_PRE_NARRATION`. No detail page yet — this entry is the whole documentation; a page is owned by mt#4992.
131+
- **Two-strikes record** — PostToolUse: records every tool error and accumulates per-conversation error streaks, feeding the calibration data behind the 2-strikes rule. Observation mode by default — it logs what WOULD have fired without acting, so the discipline itself stays the agent's to keep. `MINSKY_TWO_STRIKES_MODE`. No detail page yet — this entry is the whole documentation; a page is owned by mt#4992.
132+
- **Warn main-workspace mutation** — PostToolUse on `Bash`: reads the MAIN workspace's `git status` and names tracked files that became modified DURING that call. It observes git STATE, not the command string, which is the point — the recurrence it was built for used `sed -i`, `cat >>` and a `python3` heredoc, none of which any destructive-verb list contains. Fires at most once per file (the reported set is a diff against the last observation), and untracked files are exempt. Log-only. none. No detail page yet — this entry is the whole documentation; a page is owned by mt#4992.
133+
- **Standalone duplicate matcher** — PreToolUse: the advisory duplicate probe on a standalone `tasks_create`. Embedding-based, and provably unable to discriminate at the distances real duplicates sit at (mem#819) — which is why the deny-tier sibling in `hook-files.mdc` is a presence check instead. none. No detail page yet — this entry is the whole documentation; a page is owned by mt#4992.
134+
- **Post-merge unasked-direction scan** — PostToolUse: after a session PR merges, runs an AI analyzer over that session's transcript to surface preference-bound decisions the agent made without asking, written for weekly operator triage. Observational — the merge has already happened, so it never blocks. `MINSKY_UNASKED_DIRECTION_DETECTOR`. No detail page yet — this entry is the whole documentation; a page is owned by mt#4992.
135+
- **Bridge-memory retirement** — PostToolUse: when a task reaches DONE or its PR merges, names any bridge memory tagged with that task, so the retirement decision is made at the moment it becomes answerable. A bridge memory whose structural fix has shipped is stale guidance that still reads as current. `MINSKY_SKIP_BRIDGE_RETIREMENT`. No detail page yet — this entry is the whole documentation; a page is owned by mt#4992.
136+
- **Deploy-verification after merge** — PostToolUse: after a merge touching a deploy surface, injects the reminder that the task is not done until the post-merge deploy is verified healthy. It injects rather than blocks because DONE is set atomically at merge and the deploy only exists afterwards. `MINSKY_SKIP_DEPLOY_VERIFY`. No detail page yet — this entry is the whole documentation; a page is owned by mt#4992.
137+
- **Success-criteria injection** — PreToolUse on PR create: emits the bound task's success criteria verbatim, so they are confronted at ship time rather than recalled from having written them. The create call is already in flight when it fires, so it prompts a follow-up edit rather than shaping the body. none. No detail page yet — this entry is the whole documentation; a page is owned by mt#4992.
138+
- **MCP daemon staleness** — UserPromptSubmit: warns when the running MCP daemon is older than the built source, so a tool result reflecting stale server code is attributable rather than mysterious. none. No detail page yet — this entry is the whole documentation; a page is owned by mt#4992.
139+
- **Memory search** — UserPromptSubmit: auto-injects top-K memory-search results for non-trivial prompts, restoring preamble-parity for Claude Code without the agent having to remember to search. Explicitly TEMPORARY and harness-specific; retirement tracked at mt#1588 (`CLAUDE.md §Memory Usage`). none. No detail page yet — this entry is the whole documentation; a page is owned by mt#4992.
140+
- **Typecheck on edit** — PostToolUse: runs an incremental typecheck after a TypeScript edit, surfaces filtered errors as context, and records which project root was touched. Never blocks; the root it records is what its Stop-time sibling reads to know what to check. none. No detail page yet — this entry is the whole documentation; a page is owned by mt#4992.
141+
- **Conversation run-state record** — forwards one observed harness event to the cockpit daemon over HTTP, so the fleet view knows what each conversation is doing. One script registered under every observed event, branching on the event name; it POSTs rather than writing the DB directly because a cold hook process pays ~695ms for a domain bootstrap against ~20ms for the POST. none. No detail page yet — this entry is the whole documentation; a page is owned by mt#4992.
142+
- **Auto session title** — UserPromptSubmit: emits a derived title for the conversation. Outputs a scalar, not a judgment about the trajectory. none. No detail page yet — this entry is the whole documentation; a page is owned by mt#4992.

0 commit comments

Comments
 (0)