# Upgrade Guide — v0.28.43

<!-- bump: patch -->

## What Changed

Two layers of work landed in this release.

**Layer 1 — Outbound messaging, reshaped under the signal-vs-authority principle.**

The outbound agent-to-user messaging path (`/telegram/reply`, `/slack/reply`, `/whatsapp/send`, `/imessage/validate-send`, `/telegram/post-update`) had grown three independent blockers sitting in front of the `MessagingToneGate` — a junk-payload guard, an outbound dedup gate, and the tone gate itself. Each held its own 422-block authority. Each misfired independently. Combined, they produced the over-block pattern observed on 2026-04-15 where legitimate messages were being rejected and the fixes for each layer kept producing side effects in the next.

All three blockers have been reshaped into a single authority. `isJunkPayload()` and `OutboundDedupGate.check()` are now pure signal producers — they return structured observations (junk detection with reason, duplicate detection with similarity and matched prior) but do not emit 422 responses. `MessagingToneGate.review()` is the single outbound authority. It receives the signals alongside recent conversation context, and makes one block/allow decision with reasoning traceable to an enumerated rule list (B1–B9).

Rule-id enforcement is now structural. If the LLM returns a block decision citing a rule not in the enumerated set, or with no rule id at all, the gate fails open with `invalidRule: true` on the result. This catches the "LLM invents a rule not in its prompt" drift mode directly. Every decision the gate makes is logged to stderr in structured form so over-block audits can detect drift patterns over time.

Signal-driven rules B8 (`LEAKED_DEBUG_PAYLOAD`) and B9 (`RESPAWN_RACE_DUPLICATE`) require non-empty conversational context to fire. On channels without a conversation source (e.g., fresh sends to unfamiliar channels), these rules cannot apply, preventing context-blind judgment calls.

Also in layer 1: a pre-respawn drain in `SessionRecovery` that polls topic history for up to 7 seconds after killing a context-exhausted session, captures any in-flight reply that lands after detection, and passes it into the fresh session's bootstrap prompt. This prevents the respawned session from duplicating a reply the dying session already committed.

**Layer 2 — Structural process enforcement: the `/instar-dev` skill and its hooks.**

A new skill is introduced at `skills/instar-dev/` that is the required entry point for every change to the instar source tree. It composes `/build` as its execution engine and adds instar-specific phases around it: principle check (read `docs/signal-vs-authority.md` and ask whether this change touches a decision point), planning, build, side-effects review (written artifact answering seven structured questions), second-pass review subagent for high-risk changes (block/allow decisions, session lifecycle, dispatch, information flow), and a trace+commit phase that verifies the artifact covers the staged files.

Enforcement lives in `.husky/pre-commit` (via `scripts/instar-dev-precommit.js`) and in `scripts/pre-push-gate.js`. The pre-commit hook refuses commits of in-scope source files (`src/`, `scripts/`, `.husky/`, `skills/*/SKILL.md` and support scripts) unless a fresh trace file exists in `.instar/instar-dev-traces/` pointing at a filled-in artifact that covers the staged files and matches the artifact's content sha. The pre-push gate refuses release pushes whose upgrade notes' "What Changed" section contains fix/feature keywords if no matching artifact exists in `upgrades/side-effects/`. Bypass via `git commit --no-verify` is still technically possible but leaves a visible trail in git history.

The skill is NOT user-invocable. It is for the instar-developing agent (Echo, or any agent assigned instar-dev responsibilities). End users of instar should never invoke `/instar-dev` and will not see it in their slash-command menu.

Documentation landed:

- `docs/signal-vs-authority.md` — the architectural principle the skill enforces.
- `docs/decision-surface-inventory.md` — a living inventory of every decision point in instar with classification (signal / authority / violator / audit).
- `skills/instar-dev/SKILL.md` — the skill definition.
- `skills/instar-dev/templates/side-effects-artifact.md` — the required review template.

Side-effects review artifacts for this release are in `upgrades/side-effects/`:

- `outbound-signal-authority-rework.md` — review of the layer 1 outbound rework, including independent second-pass findings and resolutions.
- `instar-dev-skill.md` — review of the skill and enforcement infrastructure itself.
- `retrospective-drain-and-principle.md` — retrospective review of the pre-respawn drain and scaffold-template principle changes that pre-dated the skill.
- `skill-audience-clarification.md` — review of the follow-on language tightening.

## What to Tell Your User

- **No more over-blocked replies.** Earlier patches to outbound messaging had layered several independent filters in front of the message-review gate, and each one could reject a legitimate message on its own. All of those filters now hand observations to the review gate, which makes one decision with full conversational context. Technical depth in a reply to a technical question no longer gets rejected for being too detailed.
- **The review gate now shows its reasoning.** When a message is blocked, the response names exactly which rule caught it. If the gate tries to block with reasoning that isn't one of its written rules, the message passes through and the event is logged for later audit. The gate can no longer invent new rules in the moment.
- **Post-respawn duplication is mitigated.** If a session gets restarted mid-reply because the conversation got too long, the new session learns what the old one was about to say so it doesn't repeat itself.
- **No visible user-facing change for the process infrastructure.** The `/instar-dev` skill and its enforcement hooks are for developers working on instar itself — you will never see or need to invoke them.

## Summary of New Capabilities

| Capability | How to Use |
|-----------|-----------|
| Outbound signal-to-authority flow | Automatic on every outbound messaging route |
| Rule-id enforcement on tone gate decisions | Automatic; blocked responses now include a `rule` field |
| Structured decision log for over-block audit | Automatic; decisions written to stderr |
| Signal-driven rules B8/B9 gated on conversation context | Automatic; no effect on channels without a conversation source |
| Pre-respawn drain in context-exhaustion recovery | Automatic during respawn |
| `/instar-dev` skill | Required for every change to instar source; not user-invocable |
| Side-effects review artifact requirement | Enforced via pre-commit and pre-push hooks |
| Decision-surface inventory | Living document at `docs/decision-surface-inventory.md` |

## Evidence

### Outbound signal-authority rework

Dry-run verification against real topic history (documented in `upgrades/side-effects/outbound-signal-authority-rework.md`):

- **Verbatim-resend case**: `OutboundDedupGate.check()` flagged similarity=1.000 and populated `signals.duplicate.detected=true`. Gate received the signal alongside recent conversation context and made the decision — no 422 from the detector layer.
- **Paraphrase case** (similarity=0.611): detector did not flag, `signals.duplicate.detected=false`. Gate passed the message. Trade-off documented — paraphrases below 0.7 pass through.
- **Junk-payload case**: 11/11 cases classified correctly by `isJunkPayload()`. Signal routes to the authority; the authority decides based on context.

Live verification against the running echo server (post-deploy):

- A block-case test with a literal path and a CLI command returned 422 with `rule: "B1_CLI_COMMAND"` — the new enumerated rule id is populated, confirming the rework is active. Latency ~13s (real LLM call).
- A short `"test"` payload sent to a fresh topic (no conversation context) passed the gate. Old behavior would have auto-blocked at the junk-payload layer; new behavior honors the prompt constraint that B8 requires conversation context.

### Reasoning-discipline enforcement

Unit test coverage of the invalid-rule drift path (`tests/unit/MessagingToneGate.test.ts`):

- LLM response `{"pass": false, "rule": "B_INTERNAL_DETAILS"}` → gate fails open with `invalidRule: true`.
- LLM response `{"pass": false}` (no rule) → gate fails open with `invalidRule: true`.
- LLM response `{"pass": false, "rule": "B8_LEAKED_DEBUG_PAYLOAD", ...}` → gate honors the block, `invalidRule` is undefined.

All 24 tests pass.

### Pre-respawn drain

Unit test coverage: 27 tests in `tests/unit/context-exhaustion-recovery.test.ts` covering drain window timing, empty-window fallback, in-flight capture, recovery-prompt assembly, and respawn-fresh vs legacy respawn paths.

Full end-to-end verification of the drain requires a naturally-occurring context-exhaustion event with an in-flight reply landing in the 7-second grace window. Not reproducible in dev without disrupting an active session or burning quota on a throwaway. The `CompactionSentinel`'s structured log will capture the first real trace when it fires.

### `/instar-dev` gate

End-to-end dry run (documented in `upgrades/side-effects/instar-dev-skill.md`):

- Block case: staged `src/core/types.ts` with no artifact → pre-commit hook exits 1 with "commit BLOCKED" banner identifying the in-scope file and missing-trace reason.
- Pass case: staged matching artifact, ran `write-trace.mjs` to produce a trace covering the staged files → hook exits 0 with confirmation line identifying the trace and artifact.
- Post-bootstrap verification: after the initial self-commit landed, attempted a marker change to `src/core/types.ts` without an artifact. Hook correctly blocked it in the standard commit flow.

Pre-push gate Section 5 verified against a working-tree state where `upgrade-notes` qualify for review but no matching artifact exists — reports the missing-artifact error and exits 1.

### Remaining known gap

Full automated test suite reports 15847 tests passing, 7 failed, 7 skipped. The one identified failure (`tests/unit/security.test.ts` flagging `execSync` calls in `commands/listener.ts`) predates this release and is unrelated to these changes. The other 6 failures were truncated from the test run output; targeted reruns of the test groups this release touches (MessagingToneGate, OutboundDedupGate, junk-payload, server.test.ts, messaging-routes.test.ts) all pass.
