Move the human checks to CI, but only for accidents: a week of agent PRs (karasu weekly)

2026-11-08来源 dev.toEnglish
Move the human checks to CI, but only for accidents: a week of agent PRs (karasu weekly)

karasu is a text modeling language for system architecture, inspired by the C4 model.
You describe your system in .krs files and karasu draws the views: system, deployment, and team structure.
I haven't written its code by hand; everything goes through Claude Code.

This is the fifth weekly update.
It covers changes merged from November 1 to 7.
This time I start with what I learned building with agents, then go through what shipped.

This week in numbers: 114 PRs merged (6 of them Dependabot) and 22 decisions recorded as ADRs.
19 of the ADRs came in ADR-only PRs, and 3 were added inside implementation PRs.
No monthly release this week.
On November 4 the weekly prerelease of the Visual Studio Code extension came out for the first time (0.5.2026110401).

Lessons from building with agents this week

What I checked by hand on Medium PRs has moved to CI.
karasu's PRs fall into one of four risk tiers by the paths they touch.
Medium PRs don't go through CodeRabbit.
I read the local review record and make the final call.

Looking back over recent Medium PRs, some changes landed there that didn't belong.
The /render?src= fix described below was merged as Medium.
A change touching a security boundary still became Medium if it didn't hit one of the listed paths.
A change to parser behavior also stayed Medium as long as it didn't touch the language spec documents.

So on November 1, I wrote down what I was checking by hand on Medium PRs and decided which of it to hand to machines.
Three of those landed this week.

  • Tiering changed. Changes to paths on a security boundary, and changes that add or remove risky patterns like innerHTML or eval(, go to High and also require /security-review. Paths for parser and language behavior go to High too.
  • Test handling is checked. A PR titled fix needs added tests or a No test: <reason> line. Weakening an existing test needs a Test change: <reason> line. A refactor PR must not change existing tests.
  • Unchanged rendering is checked. A check called Render equivalence renders every example and every acceptance-test krs file. It renders them at both the merge base and the PR head, and compares the SVG, the diagnostics, and the formatter output. A refactor PR with any difference fails. A PR that changes rendering gets a rendering-change label. The label doesn't block the merge; it only prompts me to look at the preview.

Render equivalence compares 3,281 outputs locally, at about 2 seconds per side.
As a test, changing one layout constant produced differences in 2,129 outputs.
The test-handling check was tried against past PRs.
The 10 PRs from this week's file split (below) that only move code passed as refactor.
Two PRs that deleted or rewrote tests failed as refactor.

That leaves two things I no longer have to check on Medium PRs.
Whether the PR description matches the diff, and whether declining a finding in the local review was reasonable.
Render equivalence isn't a required check yet.
I'll watch it for a few weeks for false positives before making it required.

The machine checks only for accidents.
A subagent review of the check that detects weakened tests raised 36 findings.
Most were ways around it.
Wrapping a test in a block comment, inserting a return, writing ctx["skip"](), and the like.
Forms you could write by accident are closed.
Forms you'd only write to disguise a change on purpose are left alone.
Those are left to the step where a person reads the diff.
PRs from untrusted authors land in High to begin with.

The block on gh pr ready was rebuilt again this week.
Last week I wrote that the check refusing gh pr ready before the local review was rebuilt as a gh wrapper.
Closing loopholes found in the command text kept failing; each CodeRabbit review found a new one.

This week it turned out the wrapper let the command through without any check when given --repo.
The first fix compared the value of --repo with the origin URL.
There was also a fallback that took the base for comparing the review record from a branch of the same name on origin.
When pointed at a PR in another repository, that fallback reads the wrong branch.
Besides --repo, you can point at another repository with the GH_REPO environment variable, by giving the PR as a URL, or with gh repo set-default.
Each way needed its own guard, and CodeRabbit found them one per round.
In the end, both the URL comparison and the fallback were dropped.
Now the wrapper reads the PR that gh pr ready targets and checks the review record against that PR's base commit.

It was the same shape as last week.
Listing exceptions to close them one by one never ends.
Both times, it settled once the check read the thing itself (the arguments gh received, the PR's base commit).

CodeRabbit found a bug machines wouldn't have.
The first-run tour (more under "Coming next") had a bug that lost edits.
A tour step reloads the open file.
Pressing Next before a save finished reverted the editor to its unsaved contents, and the next keystroke overwrote the edit.
CodeRabbit pointed this out while reviewing the design document PR that extends the tour.
The agent reproduced it with a throwaway hook test, then fixed it in the implementation PR.
It also confirmed that the test fails with the fix removed.
The app at karasu.kompiro.dev deploys from main, so the first tour had this bug from November 3 to 7.
It isn't in any CLI or Visual Studio Code extension release.

Render equivalence can't catch this bug.
Rendering doesn't change; it comes from the order of saving and reloading the file.
On the same PR, the local CodeRabbit CLI had raised something similar.
The agent first declined it for the wrong reason, and changed its call after CodeRabbit raised the same point on the PR.

I followed up on last week's open question.
Last week I wrote that 4 High PRs never got a CodeRabbit review.
On a closer look, 3 of them were reviewed by CodeRabbit with 0 findings.
With no findings, CodeRabbit leaves a comment on the PR but no review.
I was counting reviews, so I missed those 3.
The remaining one hit CodeRabbit's usage limit and was never reviewed.
That PR was merged on the local review alone (a subagent and the CodeRabbit CLI).

This week had 33 High PRs, and 11 of them have no CodeRabbit review.

  • 6 are from Dependabot, which is excluded from CodeRabbit in its config. Dependency updates are triaged together with /hane:dependabot.
  • 3 had 0 findings.
  • 1 hit the usage limit.
  • 1 requested a review, but CodeRabbit's review never ran. I haven't found out why yet.

CodeRabbit's limit was also hit once more this week, in the local CLI.
CI checks like Render equivalence have no such limit.

This week 117 PRs were closed and 114 of them merged (a 97% merge rate).
By tier, 33 were High, 15 Design, 39 Medium, and 27 Low.
Medium went up from last week's 12 because the 15 file-split PRs were Medium.
I merged the 27 Low PRs without reading the diff, so the auto-merge rate is 24% (last week, 20%).
18 were ADR PRs, 7 were the daily regeneration of the ADR and TPL indices, and 2 tidied TPL documents.

Measure on the crowded view, and take a new layout only when it's strictly better

This week changed three things about how views with many edges look.
All three were measured on UmamiApp, a view in a test model based on Umami.
It's a crowded view with 41 edges.

The UmamiApp view before and after, drawn at the same scale. Above, bundles of lines cut through cards; below, there are fewer lines and the labels line up

The top is the start of the week (main on October 31), and the bottom is the end of the week (main on November 7).
Both are drawn at the same scale, so the bottom one is larger by the room opened up for labels.

Two-way edges are now drawn as one line with an arrowhead at each end.
This applies to pairs that have both A -> B and B -> A and look the same.
The cards don't move.
The two edges stay separate in the data, and each takes half of the line.
Hover and diff markings attach to each half.
Each half's label starts with an arrow showing its direction.
I picked this look by laying out the options on a spike branch.
Pairs that differ in color, width, or diff state are still drawn as two lines, since merging them would hide the difference.

In UmamiApp, the lines went from 41 to 27 and the crossings from 134 to 58.
Last week's preview gave prototype numbers of 157 crossings down to 61.
The numbers changed because I measured again after fixing the next bug.

Edges between wrapped rows ran through their own cards.
To keep a view from getting too wide, karasu wraps one layer's cards onto several rows.
Edges between cards in the same layer but on different rows were treated as side by side and attached to the left and right sides.
So the straight line ran through the cards at both ends.
Cards are drawn after lines, so the arrowheads were hidden under the cards.
In UmamiApp, 12 of the 41 edges ran through their own cards, and 6 arrowheads were hidden.
Now, cards on different rows are connected at their top and bottom sides.
Edges through cards and hidden arrowheads both went to 0, and crossings went from 126 to 110.

This turned up while reviewing the PR that merges two-way edges.
For the Boards and Identity pair, both arrowheads of the merged line were hidden.
With both ends hidden, a line has no direction.
Row wrapping predates 0.10.0, so the released 0.10.0 has this bug too.
Comparing the 543 views in examples/ and the renderer's test models, only UmamiApp rendered differently.

Views now make room for labels they couldn't show.
With label-display: auto, a label that would overlap other labels or cards is omitted.
In a view with omitted labels, karasu now tries two alternative layouts.

  • A: widen the gaps between rows by the height of the omitted labels.
  • B: A, plus widen the spacing between cards in a row by the widest label.

An alternative is taken only when it omits strictly fewer labels than the original layout.
Otherwise the original layout stays.
Views with no omitted labels render as before.

Model Labels shown (before → after) Layout taken
UmamiApp 17 → 30 (of 41) B
Dify (405 views) 206 → 216 (of 296) A
WordPress (1,031 views) 156 → 185 (of 258) A
examples/ unchanged (byte-identical output) —

In exchange, rendering is slower.
DifyDB in Dify went from 173 ms to 403 ms, and WordPressApp in WordPress from 173 ms to 298 ms.
Only views with omitted labels get slower.

All three are already in the app at karasu.kompiro.dev, which deploys from main.
In the Visual Studio Code extension, they're in the November 4 prerelease.
They reach the stable CLI and extension on the next monthly train (planned for November 29).

Write the split plan into Issues, and let each PR lower the pin

Last week, package sources got a limit of 600 lines per file.
The 33 files over it were pinned at their current length.
By this week, the four biggest of them were split to fit under the limit.

File Before After
parser/parser.ts 3,066 lines 335 lines
renderer/svg-renderer.ts 2,965 lines 460 lines
view/view-extract.ts 2,042 lines 523 lines
renderer/layout.ts 1,719 lines 590 lines

All four change often.
In the last 90 days, layout.ts had 46 commits, parser.ts 36, and svg-renderer.ts 34.
Left pinned, every new feature would run into the line limit.

First, I opened one Issue per file (on the afternoon of October 31).
Each Issue has a table of line counts by section of the file, and the steps for what to extract in what order.
Each step became one PR, and that PR lowers the pin too.
If a pin is left higher than the actual line count, a test fails.
There were 19 PRs in all, and the four files' series ran in parallel.
The last PR merged at 23:45 on November 1, about 34 hours after the Issues were opened.

The steps are ordered to avoid import cycles.
karasu checks for cycles with madge, which counts type-only imports too.
So for view-extract.ts, a module holding only types was extracted first.
Without it, the extracted modules would read types from view-extract.ts, while view-extract.ts read functions from them.
For parser.ts, the parser's state and basic operations were gathered into one object (a parse context), and the extracted functions take it as an argument.

Every PR moves code without changing behavior.
That was checked by tests and snapshots passing unchanged, the cycle check, and a script that captures renders before and after showing no difference.
The PR merging two-way edges, in progress at the same time, merged main 5 times along the way.
Its code diff didn't change; only the line-limit pins moved.

Files with a pinned limit went from 33 to 28.

This week a person checked that behavior didn't change.
From November 2, the Render equivalence check described above does it.

Other changes this week

karasu serve could be read from other machines on the same network.
karasu serve listened on every network interface.
Anyone on the same network could read .krs and .krs.style files through /api/*.
Pages using DNS rebinding were also treated as the same origin.
It now listens only on 127.0.0.1 by default, and --host widens it.
/api/* answers only when the Host header is an IP address, localhost, or a name given with --allowed-host.

/render?src= could reach services on your own machine.
/render?src= checked the hostname of the given URL once, at the start, and then followed redirects as they came.
A name that resolves to loopback, an IPv4-mapped IPv6 address, or a redirect could reach other services running locally.
The result came back with Access-Control-Allow-Origin: *.
It now checks the address it connects to on every redirect, and follows redirects itself, up to 5.
The response body is also capped at 256 KiB.

Both are in the released 0.10.0.
The fixes ship on the next train.
Until then, it's safer not to run karasu serve while connected to a network you don't trust.

Syntax in the wrong place no longer sets off a chain of errors.
Say you write a database with a body inside a service.
Until now, only the keyword was skipped, and the rest spilled into the enclosing block.
That gave 9 errors, or a following domain read as a top-level element.
Now the misplaced syntax is skipped as a whole, with a single error.

Smaller changes.
Tapping a card to drill down left the ⇄ Relations pill on the next level. That's fixed.
@azure/identity is now 4.14.1, which resolves a vulnerability in its dependency @azure/msal-node (GHSA-xcx6-jx8p-x3h5). The two remaining alerts need a major version bump, so I've left them for now.

Coming next

I've started working on the first-run experience.
The goal is for someone opening karasu.kompiro.dev for the first time to get, in about 5 minutes, the value of a team looking at the same architecture model.
This week the implementation got this far:

  • On first open, a Welcome screen offers three ways in. Choosing "Design a service split" opens a bundled sample, and a small panel guides you through 5 steps. It adds no new syntax or mechanisms; it's built only from what the app already does.
  • You can restart the tour, resume it after a reload, and see a ✓ when you've done what a step asked.
  • The other two ways in (Understand and Evolve) have tours too.

Next, a PR that runs the tour on a second subject (a payroll system) is in review.
To see whether the first-run experience works, I'm also designing usage measurement that doesn't identify individuals.

Also coming:

  • After watching Render equivalence for a few weeks, make it a required check.
  • The next monthly train is planned for November 29. This week's karasu serve fixes and the rendering changes above should reach the stable CLI and Visual Studio Code extension on it.

原文:https://www.a8q9.cn/en/article/move-the-human-checks-to-ci-but-only-for-accidents-a-week-of-agent-prs-karasu-we-8b4bc3.html

相关文章