propose_rule: an agent can raise the standard and cannot lower it
The thesis, executable. A reviewer saying "do it this way" to an agent, over and over, is waste — that instruction should become a rule, once, and then the agent is steered by it rather than corrected by it. This is the tool that lets it say so.
propose_rule oversized-files.weight 40 because "we split files at review anyway" → written, 25 → 40, tighter
propose_rule oversized-files.free 9 → Refused: this would lower the standard, and only a person may do that.
Letting an agent write to cqx.json is safe rather than merely guarded, and the reason is worth stating. Tightening a rule makes the number the agent is measured by harder to reach, so it is never in its short-term interest — the direction it might abuse is the one it cannot go. What it is good for is the thing a reviewer does by hand today: noticing that a standard should be higher, and saying so once.
because is required and is written into cqx.json beside the rule. A
threshold somebody finds in a year with no explanation is a threshold nobody
dares change, and the person who set it has forgotten. cqx does not read the
key — serde ignores what RulePatch does not know — so it costs the line it
occupies.
Three refusals rather than one, because they are three different things to tell an agent:
a loosening "this would lower the standard, and only a person may" an unclear "this cannot be shown to raise the standard" a no-op "that is already the value. Nothing to change."
The second matters: max_lines 1000 → 400 IS stricter, and refusing it while accusing the agent of lowering the bar would be wrong as well as unhelpful. The third matters more — an agent told "you are lowering the standard" about a change that changes nothing will try again forever.
Every refusal names the command a person can run instead.
The editing itself moved into cqx_score::edit, shared by all three callers.
cqx config set had two answers to the same question in one function: the
document was edited one way and a copy of the configuration another, and they
agreed only because one person wrote both on the same afternoon. Now a change
is proposed — parsed, validated, classified, turned into the document that
would be written — and writing it is a separate step, because whether it may
be written is a different decision for a person at a terminal than for an
agent.
The MCP server drops its cached scan after a write. The tree is identical and the score is not, and there is a test that catches it: weight the security rule the fixture trips and the security score must move.
21 tests in cqx-score, 14 in cqx-mcp, workspace green, clippy clean.
Merge pull request #45 from samifouad/grok/mcp-server
an agent can ask cqx what it thinks
give the MCP server a real error type, because cqx asked it to
The crate went up with Result<_, String> throughout, and the check on its own
pull request failed: seven new findings from result-string-density, a rule
cqx has been shipping since before this crate existed.
That is the loop this product is about, running on itself. Nobody reviewed the code and said "use an error type". The tool said it, on the pull request, with the file and the line — and the fix is thirty lines.
It is also genuinely better. explain failing because a rule does not exist
and explain failing because the folder cannot be read are different answers
to an agent, and as prose they were distinguishable only by reading the
sentence. Four variants — Unreadable, Analysis, Missing, NoSuchRule — can be
matched on, which is what will let a later version answer "did you mean
exit-in-library?" without parsing its own error message.
The wording still lives in one place, Display, so the sentence an agent reads
is decided once rather than at each Err.
Verified: the seven findings are gone, the quality score is unchanged at 68 because the rule was already capped by the twenty in the rest of the workspace, all four error paths still answer as tool errors rather than protocol errors, and the eight protocol tests pass.
The other twenty are a separate matter and belong in their own change.
Merge remote-tracking branch 'origin/main' into review/mcp
an agent can ask cqx what it thinks
A coding agent on the machine should be able to ask what cqx thinks of a
repository without a network round trip. cqx mcp is a read-only MCP
server over stdio: JSON-RPC 2.0, one object per line, four tools (score,
findings, rules, explain), and nothing written to disk.
The scan is cached on the canonical root for the life of the process — the simple key, not mtimes. Two questions about the same tree in one session must not walk it twice.
Merge pull request #44 from samifouad/claude/the-ratchet
the ratchet: an agent may tighten a rule, only a person may loosen one
say which way a rule just moved, and let a caller refuse the wrong way
The ratchet was a function nothing called. This is the first thing that calls it, and the smallest useful one: the command that already changes a rule.
Every cqx config set now prints the direction.
cqx config set oversized-files.weight 40 cqx.json · oversized-files.weight = 40 25 → 40 tighter: more points are at stake
That line costs nothing and is the difference between a decision and a drift. A person loosening a rule is allowed to — and should be told they did.
--tighten-only turns being told into being stopped, and it is the flag an
agent is given:
$ cqx config set oversized-files.free 9.0 --tighten-only cqx config set: refused — --tighten-only, and this does not tighten. oversized-files.free 1 → 9 more is tolerated for free
$ cqx config set oversized-files.max_lines 400 --tighten-only cqx config set: refused — --tighten-only, and this does not tighten. oversized-files.params.max_lines 1000 → 400 which way a parameter tightens depends on what it measures
The second is the one worth reading. Four hundred is stricter than a thousand and the refusal is still right: the rule holds because it never guesses, and a rule that guesses correctly today is a rule that guesses wrongly about the parameter somebody adds next month.
The classification runs against the configuration as it would be afterwards, built by applying the same field and the same parsed value the writer applies, so the direction reported and the file written cannot disagree.
the ratchet: an agent may tighten a rule, only a person may loosen one
This is the asymmetry the whole idea of letting an agent touch cqx.json rests on. Without it, "make the score go up" has two solutions — write better code, or lower the bar — and the second is faster, always available, and looks identical in a diff to anybody skimming. An agent that can only ratchet has one solution.
So the direction of a change is decided in one place, beside the rules it classifies, rather than in whatever code happens to be calling. A second opinion about which way is which is the bug that makes the guarantee worthless.
What it knows:
free lower tighter less is tolerated for nothing full lower tighter the whole deduction arrives sooner weight higher tighter more points are at stake enabled false→true tighter a rule that was off is on min_score higher tighter the floor refuses more exclude removed tighter code that was not scored now is
and the reverse of each is a loosening. Excluding a path is a loosening however reasonable it is — vendored code and generated files are exactly what a person should exclude and exactly what an agent should not be able to.
Three things are Unclear, which is refused the same way a loosening is.
A rule parameter, because the direction depends on what the parameter means
and this file knows only its name: max_lines 1000 → 800 is stricter, and
min_matches 3 → 2 is also stricter, and the next parameter will be neither.
Guessing would be right most of the time, and a guarantee that is right most
of the time is not one.
A ramp with free above full, because that is not a stricter ramp, it is a broken one — reported as its own objection so a refusal names the real problem rather than netting two changes out against each other.
A rule that has gone, because what it falls back to is a table this function was not given.
tightens is an all, not an any, and deliberately false for an empty
proposal: answering "yes, apply it" to a change that changes nothing teaches a
caller to stop checking.
Twelve tests, including every rule that ships — so a rule added later with an
odd shape cannot quietly escape the classification — and the case somebody
would actually try: three real tightenings with one quiet exclusion among
them. Proved by inverting the free comparison: three tests fail.
Merge pull request #43 from samifouad/claude/scan-as-a-library
the scan pipeline is a library, not a command
the scan pipeline is a library, not a command
cqx scan was the only way to reach the extractor, the scorer and the report
together. That was fine while CI was the only caller. It is not fine now: the
desktop application scores the tree somebody is editing, and the MCP server an
agent talks to answers questions about that same tree, and neither of them has
a terminal to print to or an exit code to set.
So scan becomes pub fn tree, and Scanned becomes public with its fields.
Nothing about the pipeline changed — this is the same function with a different
visibility and a better name for what it does.
One thing moved rather than merely opened. The scan block in the report —
files, lines, milliseconds, the cqx version — was stamped by the command after
it called the pipeline, which meant a second caller would produce a report
missing it and nobody would notice until something downstream asked. It is
stamped by tree now, so every caller gets it and no caller has to remember.
Verified against fixtures/basic: the same scores, and the scan block still
present in --json.