What the plugin records, and the summary
Rejected replies
Section titled “Rejected replies”Vale checks what its styles can detect, and a reader notices more than that. When a reply breaks the rules in a way you noticed, run /just-say-so:restate, with a reason if you have one: /just-say-so:restate verbal shorthand. The model rewrites the reply under the condensed rules and changes nothing else. A wrong answer is a different problem: correct it in an ordinary prompt, so the records stay about prose.
Restate exists to record where just-say-so failed you, so the records can show what to change. That change takes one of three shapes, and only the first touches Vale: tune a Vale rule (its level, a term, an allowed collocation); add or edit a judgment bullet in the rules file, for a failure no linter can see; or write a new Vale rule, when the failure turns out to be checkable. The judgment bullets are not a fallback. They cover what Vale cannot, and the manifest holds them beside the linted ones with an empty vale list.
Tags. A #word anywhere in the reason is a tag: /just-say-so:restate #undefined-term what is a "pin"? stores the tag undefined-term and the reason as typed, hash included. Tags are whatever you type; there is no list to pick from. The summary counts them, and when a tag is the id of a bullet in the rules file, the summary says whether that bullet is a judgment bullet or a Vale-backed one and how often its rules fired on the replies you rejected. Each rejection is checked against the rules file of its own project, including that project’s rules.extra, so a tag that one project covers and another does not prints once per coverage with the projects named. A tag that matches no bullet is reported under “what the rules do not cover yet”, which is the list of bullets and rules still to write.
The plugin records each rejection. The prompt hook reads the rejected reply from the transcript, so the text is what the model wrote and not what it remembers writing. It lints that text at every Vale level and appends one JSON line to rejections.jsonl in the state directory: $CLAUDE_PLUGIN_DATA/state under a plugin install, otherwise ~/.local/state/just-say-so. The Stop hook adds the rewrite and its alerts to the same line when the turn ends. In block mode a rewrite that the gate rejects is not the one saved; the record closes with the reply the gate lets stand. The rejected reply’s own warn counts go with the record instead of riding the next prompt, so a later report describes the rewrite and not the reply you already rejected. A record holds the session id, the project directory, the prompt index, the reason as typed and its tags, both texts with their alerts, the plugin version, and the settings in force. Replies can quote code, logs, paths, and customer data, so the file stays on your machine. Nothing writes it into a project or sends it anywhere.
What the file is for: a rejected reply that Vale passed points at a missing rule, a level set too low, or a judgment bullet still to write. A reason or tag that recurs suggests a bullet needs different wording, or needs to exist. Rejected and rewritten pairs form a test set for any judge that scores replies against the rules. Rejections divided by prompts is a drift rate for one user’s sessions, which compares reminder settings for that user. These records tune your configuration. They do not fine-tune a model.
The two files
Section titled “The two files”Two files in the state directory hold everything the plugin keeps beyond the current session. Nothing leaves your machine, and nothing is written into a project.
rejections.jsonl holds one record per /just-say-so:restate, described in Rejected replies. Since 0.8.0 a record also carries the prompt that produced the rejected reply, the model id, and the number of prompts between the last rules injection and that reply. The prompt and both replies are prose, so this file can hold anything a session did. Scrub it before you share it.
events.jsonl holds one line per countable thing a hook saw. It carries rule names, counts, and settings, never prose, so a rejection can be read as a rate. Every line has a timestamp, an event type, the session id, the project directory, and the plugin version. The types:
| Event | When | Fields |
|---|---|---|
session |
SessionStart | source, whether Vale was found, and the reminder, reply-check, edit-check, and Vale level settings in force |
reminder |
the rules go out | promptIndex, trigger (report, session, restate), withReport |
reply |
every Stop | promptIndex, sinceReminder, outcome (pass, block, standdown), the model id, alerts folded to counts per rule and level, and rewriteOf when the reply closed a rejection record |
restate |
/just-say-so:restate |
promptIndex, sinceReminder, the record id, reasonGiven |
allow |
/just-say-so:allow |
the term, where it went, and whether it was new |
A reply line folds Vale alerts at every level, not only errors, so a warning that fired on a reply you then rejected is visible. A match that equals one of the shipped banned terms appears on its entry; any other match is reply text and is left out, whichever style produced it. Two styles that flag one span count once. A block cycle writes one reply line per attempt, so the last line for a prompt index is the turn’s outcome. With the log on, the Stop hook lints the reply even when reply.mode is off. That is one local Vale run per turn and no tokens. alerts is null when Vale is missing, so unlinted turns are countable too.
The file is never pruned. A line is under 300 bytes, so a year of heavy use is a few megabytes. Set events.mode to off to write nothing; the log-only reply lint stops with it.
The summary
Section titled “The summary”/just-say-so:summary reads both files and prints facts with their counts. A turn is a reply event whose outcome is pass or standdown; a block line is an attempt and is counted apart, and no count comes from a prompt index, which resets on compaction and /clear. A rejection is a record in rejections.jsonl, which exists even with the events log off. Unlinted means alerts was null, and never counts as clean or as fixed. Every rate counts only the rejections that join to a turn inside the window, the turn one prompt before the restate in the same session, so a rejection from a session the log never saw, or one whose rejected turn precedes the window, appears in the raw counts and in no rate. A rate prints only when its denominator is in the window; otherwise the line says it is unavailable. A reply’s mode is the session setting in force when the reply happened, so a compaction that changed the mode counts from that point on.
The sections: the window and its sessions; volume; rejections per 100 turns, overall, per project, and per week once the window spans a month; rejections per 100 turns in each distance bucket from the last rules injection, which is association only, because a violation itself resets the distance; the rules with an error on final replies, with their top terms and the reply mode of those sessions; the rules on rejected replies by level, the rejected replies Vale passed clean with their reasons, and the ones that carried only warnings; rewrites that came back clean, kept an error, or were unlinted, and the rules that survive a rewrite; the reasons you typed, grouped; the tags, each with what covers it, a judgment bullet, a Vale-backed bullet with how often its rules fired on those replies, or nothing yet; what each recurring tag points at, described below; allows by term and project; sessions per setting. The report ends with observations stated with their counts and what the data does not establish. It recommends nothing; /just-say-so:advise does.
What the tags point at. A tag with 3 or more rejections in one project lands in one situation, decided in this order. Uncovered: no bullet has the tag as its id, which points at a judgment bullet or a Vale rule. Judgment: a bullet with no Vale rules covers it, and the line says whether the bullet has a short text, since only bullets with one are in the condensed reminder, and only a Vale alert re-injects the rules. For a Vale-backed bullet, two kinds of alert count, and the report labels them: restate-time alerts come from /restate linting the old reply under the config in force then, and show whether the rules detect the problem; ship-time alerts come from the reply event the rejection joined to, and show what happened when the reply went out. Insufficient: fewer than 3 linted rejected replies, or no joined reply that was linted, so the line gives the counts and no diagnosis. Stood down: the rules fired at ship-time at error level under reply mode block, and every such turn was the reply gate standing down, which is the gate’s own escape when a rewrite keeps the same alerts; blocking and the level were in force, so the line points at the rule or the bullet, not at a setting. A stand-down under warn mode, or on a warning-level hit, is the file gate’s and counts as shipped. A ship-time event that holds only other rules’ alerts is ambiguous, because the event folds overlapping alerts to one; it never counts as proof the rule was silent, and a tag whose ship-time events are all ambiguous prints as insufficient. Fired, still shipped: the rules fired at ship-time and the reply went out, with the reply mode and levels in force and any stood-down turns counted, which points at reply.mode = block or the rule at error level. Silent: the rules caught fewer than half of the linted rejected replies at restate-time and none at ship-time, which points at a Vale tune. Detects now: the rules catch half or more at restate-time and fired on none at ship-time, so they were off or lower then. Rejections with no tag that Vale passed clean are listed per project with their reasons, so they can be tagged in one pass. The threshold applies within one project; the same tag twice in one project and once in another earns nothing.
--since takes an ISO date or a span such as 30d or 6w, --project narrows to one project path, and --json prints the object the text is rendered from. The report prints rule names, shipped terms, counts, project paths, and your typed reasons. It never prints a prompt or a reply.