changelog

{data.name} Sami Fouad made a commit to samifouad/cqx

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.

Verified
+796 -161
Sep 21

{data.name} Sami Fouad made a commit to samifouad/cqx

Merge pull request #45 from samifouad/grok/mcp-server

an agent can ask cqx what it thinks

Verified
+720 -1
Sep 21

{data.name} Sami Fouad made a commit to samifouad/cqx

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.

Verified
+64 -20
Sep 21

{data.name} Sami Fouad made a commit to samifouad/cqx

Merge remote-tracking branch 'origin/main' into review/mcp

Verified
+538 -3
Sep 21

{data.name} Sami Fouad made a commit to samifouad/cqx

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.

Verified
+676 -1
Sep 21

{data.name} Sami Fouad made a commit to samifouad/cqx

Merge pull request #44 from samifouad/claude/the-ratchet

the ratchet: an agent may tighten a rule, only a person may loosen one

Verified
+538 -3
Sep 21

{data.name} Sami Fouad made a commit to samifouad/cqx

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.

Verified
+89 -3
Sep 21

{data.name} Sami Fouad made a commit to samifouad/cqx

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.

Verified
+449 -0
Sep 21

{data.name} Sami Fouad made a commit to samifouad/cqx

Merge pull request #43 from samifouad/claude/scan-as-a-library

the scan pipeline is a library, not a command

Verified
+31 -21
Sep 21

{data.name} Sami Fouad made a commit to samifouad/cqx

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.

Verified
+31 -21
Sep 21