Skip to content

feat(flowrite): a standalone redundancy editor, ported from writer-assistant - #62

Merged
khajavi merged 3 commits into
zio:mainfrom
khajavi:feat/redundancy-editor
Aug 25, 2026
Merged

feat(flowrite): a standalone redundancy editor, ported from writer-assistant#62
khajavi merged 3 commits into
zio:mainfrom
khajavi:feat/redundancy-editor

Conversation

@khajavi

@khajavi khajavi commented Aug 25, 2026

Copy link
Copy Markdown
Member

Gap §4 of WRITER-ASSISTANT-MIGRATION.md, ported before that repo is deleted. grep -ri redundan src/ returned nothing before this: the reviewer grades structure, coverage and the 28 style rules; the fact-checker asks whether a claim is true, not whether it is repeated; rule 3 covers the shallowest slice of one of the three kinds.

flue run src/redundancy.ts -m "reduce redundancy in docs/reference/ledger.md" \
  --data '{"projectPath":"/path/to/checkout"}'

It is not a phase of the write flow

The spec merged in #60 put it at step 5 of all three instruction files, as a role in ROLES with 'redundancy' in the SkipPhase union. That was rejected before implementation and the spec is rewritten here rather than left to contradict the code — with the superseded placement kept as its own section, because the reasoning generalises.

The page it edits already exists, already compiled, already passed review. Charging every write run a delegation to re-read it for repetition spends tokens on every run for a problem that appears on some — and the drafter that just wrote the page is the party better placed not to create the repetition in the first place. Building it as a phase would also have closed §4 while leaving audit gap §8 ("flowrite has no standalone entry points") exactly as wide.

So a write run is byte-for-byte what it was: instructions/*.md, agent.ts, composition.ts, run-context.ts, self-report.ts and run-telemetry.ts are untouched, and agent.test.ts's harness-tool invariant still expects exactly ['review_page', 'fact_check_page']. The trade is explicit: the pass is available, not automatic.

What writer-assistant's version had, and what came over

Its ~350-line phase — a fresh scanner session per round, [REDUNDANCY] lines regex-parsed by parseFindings(), a reused fixer session, three rounds, an unresolvable set — is one agent here. That machinery existed because Flue 1.x sessions returned text and a fresh scan needed a fresh session. No harness tool either: nothing gates on redundancy, so the result only ever goes into the model's head and comes back as prose, and a schema would cost two relay turns for nothing.

The one bound tightened against the original: never edit a code block. writer-assistant's strategy table said to keep the best of several repeated examples and remove the rest. mdoc blocks share one scope down the page, so that cut can break every block below it — and there is no mdoc verify after a standalone run to catch it. Repeated examples stay, and the guide says why.

Where the content lives

Answering a review question from #60's discussion: the guide is a skill resource, imported as a string, not a mounted skill.

src/skills/reduce-redundancy/SKILL.md            stub + frontmatter; nothing mounts it today
src/skills/reduce-redundancy/references/guide.md  detecting, fixing, bounds, receipt
src/instructions/redundancy.md                    who the agent is; does not restate the guide

This is the pattern runtime/kind-docs.ts already uses for six reference files, and it carries the measurement: mounting costs "three tool round-trips to activate a skill and read its resource, each re-sending its whole accumulated context … 2 round-trips wasted in write-data-type-ref-turn20 and 5 in write-module-ref-turn5, which is why 600f48a unmounted it". Worth it for a writer carrying seven skills it uses some of; pure loss for an agent whose only job is this one — and it would add a failure mode, the model declining to activate and then editing the page without ever reading the bounds. writing-style is mounted, for the mirror-image reason: its 28 rules are consulted only when a cut happens to touch one.

The directory stays a skill so that the day the drafter should pre-empt redundancy rather than have it cleaned up afterwards, useSkill(reduceRedundancy) is a one-line change with no content moved.

Tier, and why not Haiku

writer-assistant ran this on Haiku. The work looks cheap — find repetition, delete it — and is not: every cut is a judgement about whether the words carry anything, and this is the only role in the repo that edits a page no gate re-checks. Sonnet / low, overridable with REDUNDANCY_EDITOR_MODEL / _EFFORT.

app.ts leaves it unmounted, deliberately

Mounting two agents needs a Hono instance with a route(...) each, and hono is not a dependency here — it reaches this repo only as @flue/runtime's peer. Adding it to expose an agent nobody calls over HTTP is a dependency bought for a comment; hand-rolling a Fetchable that dispatches by path prefix replaces the dependency with untested routing code on the one path flowrite's own runs never take. flue run src/redundancy.ts reaches it directly, and the 'use agent' scan registers it either way — confirmed in the build output, so dispatch(...) and schedules can drive it. app.ts records all of this where the next person will look.

Verification

Offline, done: tsc --noEmit clean, 102 tests pass, vite build succeeds and the bundle contains the new agent.

test-fixtures/redundancy/verify.sh is the behavioral test and needs a live model, so it is blocked until the key renews (2026-09-01 — the same block as #59). It plants a page with 7 seeded redundancies and 5 decoys and diffs the result. The decoys are the half that matters, and they are chosen to be exactly the mistakes the bounds exist to prevent: three code blocks that look alike and are not, a because that carries causation, a first … then sequence, the word "tally" recurring across distant sections, and one sentence — that seeded takes a Long, not an Int — which is the only place that fact appears, sitting in a section that reads repetitive.

Its mechanical checks compare code-block text and heading text, never line numbers: removing a prose line shifts every number below it, so a numbered comparison would report every successful run as a failure. That was caught by running the checks against a simulated good run and a simulated bad one before committing them — the good run passes, the code-block cut reports KILL.

BACKLOG.md finding 9 records the measurement this owes, and states kill criteria rather than tuning knobs. A fact-checker that invents a drift wastes a round; an editor that cuts the wrong sentence ships a page with a hole in it. If the seeded run touches a code block, moves a heading, or cuts a decoy, the answer is to delete the agent, not tune it.

Not in this PR

Running that test. Deleting writer-assistant. The two remaining salvage candidates from the audit (§1, the 569-line mechanical style checker; §2, the implicit-trace rule) — both deterministic and neither blocked by the key.

Nothing in flowrite notices that a page defines Ledger in the Overview,
again in Use Cases, and a third time above the API table. The reviewer
grades structure, coverage and the 28 style rules; the fact-checker asks
whether a claim is true, not whether it is repeated; rule 3 covers the
shallowest slice of one of the three kinds of redundancy.

Ported from writer-assistant, which is about to be deleted (see
WRITER-ASSISTANT-MIGRATION.md §4). Its ~350-line phase — a fresh scanner
session per round, [REDUNDANCY] lines parsed by regex, a reused fixer
session, three rounds, an unresolvable set — is one agent here. That
machinery existed because Flue 1.x sessions returned text and a fresh
scan needed a fresh session; one agent can read, decide and edit.

A standalone entry point, not a phase of the write flow. The page it
edits already exists, already compiled, already passed review — charging
every write run a delegation to re-read it for repetition spends tokens
on every run for a problem that appears on some. It also closes half of
audit gap §8: writer-assistant could be pointed at a page nobody was
writing, and flowrite so far could not.

The guide lives in a skill directory and is imported as a string rather
than mounted, which is the pattern kind-docs.ts already uses for six
reference files. Mounting was measured at "three tool round-trips to
activate a skill and read its resource" — worth it for a writer carrying
seven skills it uses some of, pure loss for an agent whose only job is
this one, and it would add a way to fail: the model declining to
activate, then editing the page without reading the bounds. The
directory stays a skill so the drafter can mount it the day pre-empting
redundancy beats cleaning it up.

One bound is tightened against the original: never edit a code block.
writer-assistant's strategy table said to keep the best of several
repeated examples and remove the rest. mdoc blocks share one scope down
the page, so that cut can break every block below it — and there is no
mdoc verify after a standalone run to catch it. Repeated examples stay.

Sonnet rather than the Haiku writer-assistant used. The work looks cheap
and is not: every cut is a judgement about whether words carry anything,
and this is the only role here that edits a page no gate re-checks.

app.ts leaves the second agent unmounted and says why. Mounting two
agents needs a Hono instance, hono is not a dependency here, and buying
one to expose an agent nobody calls over HTTP is a dependency bought for
a comment. `flue run src/redundancy.ts` reaches it directly; the 'use
agent' scan registers it either way, so dispatch and schedules can too.
The question a run has to answer is not "did it cut something" but "did
it cut the right things and leave the rest alone", and neither half is
decidable on a page whose redundancy nobody planted. seeded-page.md
carries 7 known redundancies and 5 decoys, and the decoys are the half
that matters: an editor that cuts meaning from a correct page is worse
than no editor, because the page it damages already passed review and
nothing downstream re-checks it.

The decoys are chosen to be the mistakes the guide's bounds exist to
prevent: three code blocks that look alike and are not, a "because" that
carries causation, a "first … then" sequence, the word "tally" recurring
across distant sections, and one sentence — that `seeded` takes a Long,
not an Int — which is the only place that fact appears on a page whose
surrounding section reads repetitive.

verify.sh plants the page in the tinyproject fixture, runs only this
agent, prints the diff and the receipt, and removes the page from a trap
so an interrupted run cannot leave the fixture holding a page the next
run would then be editing.

Its mechanical checks compare code-block text and heading text, never
line numbers: removing a prose line shifts every number below it, so a
numbered comparison would report every successful run as a failure. That
was caught by exercising the checks against a simulated good run and a
simulated bad one before committing them — the good run now passes and
the code-block cut reports KILL.

The label assertion rides in agent.test.ts's existing archive-label test
rather than a new file. reduce-redundancy is not a KINDS row, but it is
archived by the same script and fails the same silent way: a typo breaks
`<label> token consumption:` parsing and nothing complains.
The spec merged yesterday put this at step 5 of all three instruction
files, as a role in ROLES with 'redundancy' in the SkipPhase union. It
shipped as a standalone agent instead, so the spec is rewritten rather
than left to contradict the code — with the superseded placement kept as
its own section, because the reasoning generalises: a maintenance pass
over a page that already passed review is not a production step, and
building it as a phase would have closed audit gap §4 while leaving §8
exactly as wide.

BACKLOG finding 9 records the measurement this owes and why it is worse
than finding 7's. A fact-checker that invents a drift wastes a round; an
editor that cuts the wrong sentence ships a page with a hole in it. The
bounds are the whole defence and they are unmeasured prose, so the
finding names kill criteria rather than tuning knobs: a touched code
block, a moved heading, or a cut decoy means delete the agent.

Two smaller unknowns are named there too — the three-occurrence
threshold for a repeated phrase is a guess, and whether the receipt's
"left" lines actually get written. The second is the interesting one: an
instruction asking for a report of inaction is exactly the kind that
gets quietly skipped, which agent.ts already measured once with
ask_for_clarification.
@khajavi
khajavi merged commit f12b231 into zio:main Aug 25, 2026
1 check passed
@khajavi
khajavi deleted the feat/redundancy-editor branch August 25, 2026 07:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant