Files
jason.woltje 2c38325eca row 66: managed sessions run the unslop check on both harnesses, the operator note on the RESULT, a correction notice on the next turn, and unslop.jsonl counts (#1559)
Lands Sage's sage/row66-r5 at 7ffb640ac, round 4 (r4d 82c830779) rebased onto a36523713 after row 197,
approved by Dewey (comment 30085) and Filbert (30086) on manifest 6cfd812da4ca (23 paths, rev 2905). The
rebase had no conflict. The range-diff is = except one pi-session.test.mjs hunk header. Patch-id without
agents/ is d67275ac4251 on 7b9737953, a36523713 and this commit's parent. Gate (mos-1186): unslop,
harness, cli and every scripts/test-*.sh green on r4d, harness 149/0 again on r5. Landing sessions on a
scratch root from the candidate tree: Claude Code r4ab24630d323 showed the note, the injected notice and
counts checked 2, flagged 1, notices 1. Pi r1dd94171bede is launch only until #383 (Vikunja #454).
2026-10-21 13:51:28 -05:00

129 lines
7.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# unslop-hook
Mechanical AI-tell enforcement for pi seats and for managed sessions on
both harnesses (row 66, #1559). Anti-drift gate for the writing
standard in SYSTEM.md / ms-unslop: prose distribution alone decays over long
sessions; this check cannot forget.
- `lists.json`: committed machine source for every list the checker enforces:
words, phrases, punct rules, regex patterns, density thresholds. Each entry
carries provenance (`ms-unslop:<pattern id>` or `system-md`), the mention
convention, and the documented divergence of the density gate from
SYSTEM.md's outright em-dash ban. Edit lists here, not in code.
- `unslop-check.js`: dependency-free checker (node CLI + module) driven by
lists.json. Loads and schema-validates the lists on first use and hard-fails
closed: empty, unparseable, or invalid lists throw. Detects banned vocabulary,
chatbot/sycophancy phrases, filler phrases, em/en dashes, curly quotes,
`not just X but Y`. Strips fenced and inline code first, so quoted code is
never flagged. Exit 0 clean, 1 violations, 2 gate broken (lists unreadable,
never a clean verdict).
- `extension.ts`: pi extension. `message_end` checks finalized assistant text
and notifies the operator (TUI/RPC). `before_agent_start` reads the most
recent assistant reply from the session file (walking `getBranch()` from
its end: pi returns the branch root first, and before row 66 the walk
read the session's first reply every turn) and, if it carries tells,
injects a correction notice the model sees on its next turn. `/unslop`
reports session stats. Violation state lives in the session file, so the
injection path survives restart, resume, fork, and reload (an in-memory
pending flag was measured dead across print-mode turns, 2026-08-19). A
broken lists.json fails closed: checks stop, `broken_lists` /
`skipped_broken` events log the reason, operator notified once, seat keeps
running.
- `claude-hook.js`: the same check for Claude Code (row 66). A managed
session's `settings.json` runs it twice: `stop` checks each assistant
message of the turn from the transcript and logs `checked` or `flagged`,
and `prompt` (UserPromptSubmit) prints the extension's correction notice
as `additionalContext` when the last reply had tells, once per reply.
Claude Code writes one transcript line per content block, so adjacent
lines with one message id are one reply, keyed by the first line's uuid.
A broken lists.json logs `broken_lists` and checks nothing; any other
fault logs `hook_failed`. It always exits 0 and never blocks.
- Each finding carries `source`, its entry's pattern id in lists.json
(`ms-unslop:7`), so the counts can be read per pattern.
- `test-unslop-check.js`: unit tests with red and green controls.
## Use
```bash
node test-unslop-check.js # suite
node unslop-check.js <file> # CLI check
UNSLOP_LISTS=<path> node unslop-check.js <file> # alt lists location
pi -e ~/.mosaic/tools/unslop-hook/extension.ts # ad-hoc load
# deploy: copy dir to ~/.pi/agent/extensions/unslop-hook/ or seat .pi
# equivalent, or list it in settings.json "extensions"
```
Env: `MOSAIC_UNSLOP_HOOK=0` disables. `MOSAIC_UNSLOP_LOG=<path>` appends JSONL
events (loaded / flagged / notice_injected / checked / broken_lists /
skipped_broken) for headless evidence. `UNSLOP_LISTS=<path>` overrides the
lists.json location for both CLI and extension.
In a managed session the runner sets `MOSAIC_UNSLOP_LOG` to
`<run>/unslop.jsonl`, reads it after each turn and adds a short note
to the next reply the session sends, which `mosaic talk` and `mosaic follow`
print. A broken check is told there once a run. `unslopCounts()` in
`packages/harness/src/runner.mjs` reads the counts after the session ends.
The session environment's allowlist doesn't pass `MOSAIC_UNSLOP_HOOK` or
`UNSLOP_LISTS`, so a managed session can't turn the check off or swap its
lists.
## Verified here (2026-08-19)
- Unit suite 24/24 (8 behavioral, 11 loader/CLI, 5 review follow-up), red and
green controls both exercised, including exit-2 on broken lists and on an
unreadable input file (S3).
- CLI: slop file exit 1, clean file exit 0, broken lists exit 2 with the fault
named on stderr.
- Extension, healthy path (print mode, zai/glm-5.3:low): startup probe loads
lists.json, reply checked clean.
- Extension, broken-lists path (print mode): `broken_lists` at startup,
`skipped_broken` per turn, seat survives, reply still delivered.
- Earlier live evidence (pre-C1, inline lists): forced-slop turn flagged;
fresh-process follow-up injected the notice and the reply came back clean;
full TUI trial (notify line, injection, /unslop stats) on session vision-unslop.
- Log evidence in session scratchpad.
## Verified here (2026-10-20, row 66)
- Unit suite 28/28: the `source` field, `replies()` grouping, the Claude
Code hook's stop and prompt paths, its off switch, a broken lists.json at
stop and at prompt, and bad hook input (`hook_failed`, exit 0).
- Managed sessions on both harnesses against the test API
(`packages/harness/tests/pi-session.test.mjs`, `claude-session.test.mjs`):
a reply with a tell gets the notice on the next turn, a clean reply gets
none, the operator note rides the next reply, a broken lists.json fails
closed with the session still answering, and the counts read back after
the session ends.
## Limits
- `/unslop` command not tested headless (print mode has no command surface);
it is a thin stats wrapper.
- En dash flag fires on typographic ranges too (2–3); acceptable for fleet
prose, revisit if it noisifies technical writing.
- Notice injection is a nudger, not a blocker. Output already streamed to the
user stays as-is; correction lands on the next turn.
- A broken lists.json latches for the session: repairing the file mid-session
does not revive checks until the seat restarts. Acceptable for an advisory
gate (review S1).
- The fail-closed operator notification requires a UI. Print-mode sessions
log `skipped_broken` but notify nobody (review S2). Managed sessions are
the exception: the runner tells the operator in the next reply (row 66).
- In a managed session the log sits in the run directory, which the session
can write. The counts are the session's own report, not host evidence.
- The extension's per-reply dedupe is in memory. After a restart the same
reply can get a second notice.
- On the first turn of a new Claude Code session the transcript may not
hold the reply when Stop runs; the hook then checks
`last_assistant_message` and logs an empty `entryId`.
- The word/phrase lists are the mechanical subset of ms-unslop only, keyed to
pattern ids in lists.json. Style judgments (voice, rhythm, structure) stay in
the skill, not the gate.
## Promotion path
Stack issue (A4): checker shared as the single source for a matching Claude
Code Stop-hook script; lists versioned beside SYSTEM.md contract text. Row 66
did the first part: `claude-hook.js` runs the same checker and lists.
Blocking a reply (Q84 option B) is a later decision.