The first state's first call is begun as soon as the state is ranked, against its revision. The
search settling the task, or a kept step moving the state, cancels it; a cancelled call closes its
stream and leaves an estimated line in the spend file. Past its cap the search goes on while that
call has not answered, to 8 s, and a visible test too slow for the cap is narrowed to the failing
tests it names instead of the search being skipped. The tests around the failing ones run beside
the visible test, and what failed among them before any edit is read only when they fail after
it. A test run in the tree leaves no file git neither tracks nor ignores, and Hypothesis keeps its
examples in the run's own temporary directory.
Model-free on 60 dev-split tasks on evo, A/B interleaved: search-settled tasks 1.74x faster (95%
1.64-1.85, 13 of 13), solves 13 to 19: five exact pyrsistent fixes no longer fail on the
.hypothesis files the runs wrote, and a lark task with an 18 s test file is searched.
hanzoai/codex names nothing outside its tree now (d1d710452): Dev's names,
defaults, model catalog, `/model <name>` match, controller interface and
version live in its `hanzo` crate, and Dev's crates depend on that, never
the reverse. hanzo-loop moves there whole; hanzo-tui keeps only the Hanzo
account; hanzo-config reads API_BASE, DEFAULT_MODEL and is_hanzo from it.
hanzo-dev's main registers what Dev adds before anything starts:
`codex_app_server::register(hanzo_kai::install)` puts Kai on every thread,
and `codex_tui::register(Account { login, logout, show })` puts the Hanzo
CLI's sign-in behind /login, /logout, /auth and /status. launch.rs holds
that /login is offered, which it is only when an account is registered.
make test-upstream runs locked against the fork's own lock.
upstream/codex is now hanzoai/codex, whose main is openai/codex with
Hanzo's commits on it, and dev builds that checkout as committed: no
`make prepare`, nothing rewritten, `git -C upstream/codex status` empty
after a build and a test run. A bump is `git merge upstream/main` on the
fork's main, with conflicts in git, then the gitlink here.
openai/codex moves from 11da6b9e to 3ee47c105 (288 commits). Of the 106
anchored edits one had lost its ground, the --sandbox-state-json help that
upstream reworded, and was re-anchored before the delta became commits on
the fork: hanzo (Dev's crates in its workspace), tui, cli, config, core,
words, brand (the name rule's 227 literals) and version (the version
rule's 126 reads). The edit that set upstream's workspace version to
0.7.0-dev.1 is dropped: no upstream code reads its own version any more.
None of the anchors touched the zsh fork, shell-escalation or gateway_auth
code upstream removed or renamed, and the Hanzo crates built against the
moved APIs unchanged.
The version: hanzo-version moves into the fork, hanzo-dev hands it its own
CARGO_PKG_VERSION at startup, and DEV_VERSION is gone. The root
[workspace.package] version is the release's, and release.yml holds the
tag to it as well as to npm/package.json. Dev's libraries stay 0.0.0,
since upstream crates depend on them, so a release still recompiles
hanzo-dev alone. cli.rs fails on any upstream read of its own version.
Gone: patches/ (upstream.json, brand.json, and the Bazel patches only the
root MODULE.bazel names, which no build here runs), crates/hanzo-upstream
and crates/hanzo-version. The V8 fetch for `make test-upstream` is
scripts/v8.sh; `make bump` prints the merge steps; `make reset` goes. The
root Cargo.toml mirrors the fork's workspace dependencies: shell-escalation
and mxc-sdk out, the split mxc crates, gix 0.89, rmcp 3.5.1 and rustls
0.23.46 in.
Nothing built from it: its crates carry their own codex-core, so linking
them beside ours would put two cores in one binary. Its clone lives at
~/work/just-every/code; what was ported from it is recorded in migration/.
The submodule sat at 41f66ca5d, their main.
Each call is an action in the ledger (stage first, repair or fallback) and the trace line
lists every call. The generator is never an Anthropic model. An answer of no edits is not
said again. The edit is tested as before.
A candidate that leaves the code unreadable or adds tests that cannot run is no
step. kill -9 -PGID without -- killed nothing, so a looping candidate kept its
pipe and hung the run: every test the flow starts is now killed with its
children, and a search stops its running tests at the cap. A test the runner
says takes longer than the cap holds is not searched. A revert returns to the
base revision, one search a state. The model is asked for one JSON object, with
a timeout a call, and is told which edits were already tried.
dev flow fix reads the failing output against the tree (locate: the gateway's
learned ranker, features held to its own by a test), puts the functions tgrep
finds spelling the failure's messages into the ranking, and lets Kai choose the
site, the action and the effort in one request, or decides alone when the
ranking is sure. The action search tries generic token transforms at the best
sites against the visible test, eight at once; a candidate that leaves fewer
tests failing is kept as a step. Else a model writes one edit (applied by text,
never a test file), the test runs, Kai reads a failed run in one request, and
recover fingerprints the failure and never replays a strategy on it.
Every action is a ledger line with a stable id, its revision, preconditions,
authorization, source and the states it passed through; credentials are
redacted. Kai's transport is a trait, health is per program.
Cargo.lock is not committed: hanzo-trigram is read from hanzoai/mcp commit
e9e6096, which is not pushed yet.
On api.hanzo.ai, automatic approval review and memory extraction and
consolidation ask for enso-auto, not OpenAI's ids. Off OpenAI, the reviewer
counts a model as served only when the catalog lists it: the bundled OpenAI
entries ride along unlisted for lookups, so codex-auto-review looked served on
every provider and a LAN engine was reviewed by a model it does not have. Now
it falls back to the model the run is on.
Co-authored-by: Hanzo Dev <[email protected]>
hanzo-kai linked hanzoai/decision's control and program crates by git.
That repository is private: the public build, CI, and every cargo install
of dev failed to fetch it. Kai is now a client of POST /v1/decisions at
api.hanzo.ai, signed with the Hanzo credential Dev already sends; nothing
of hanzoai/decision is linked, .cargo/config.toml goes, and Cargo.lock
loses 89 packages and every private source.
The programs Dev asks (tools.select, router.model, reasoning.budget,
context.select, agent.command-risk, agent.progress, agent.complete, and
the fix flow's fix.pick and fix.next) ship in crates/hanzo-kai/programs
as typed questions with their thresholds, verdict rules and calibration
pins. The gate that reads the answers is Dev's own: a noul holds at its
threshold, a choice or score is accepted at its certainty, a verdict
rule joins the policy on allow < ask < deny only when enforced, and an
answer under another calibration runs in shadow.
When Kai does not answer (refused, timed out, 401, 404, 5xx, or no
state of the decision answered) the decision fails at once and Kai is
down for 60 s: no request is made in that window, the loop does exactly
what it does without Kai, one line goes to the log and no trace line is
written. Only a decision asked after the failure brings Kai back. A
decision's states are asked in parallel; tier and budget together. The
tool shortlist is decided again when the tools on offer change, since an
MCP server that finishes starting mid-turn adds its tools.
dev flow is now dev flow fix, a fixed pipeline rather than a Decision
Program run by the private executor: retrieval, Kai's pick, Zen's diff,
the tests, Kai's next. --control rule, or Kai not answering, leaves the
tests and the budget to decide, and the report says so once. An attempt
whose patch no longer reverses ends the flow instead of retrying over it.
Tests serve the Decisions API and chat completions from wiremock: Kai
on (tools shortlisted, tier and effort hinted, verdict tightened, one
trace line per decision with its distributions), Kai answering 404,
Kai unreachable, Kai answering with a page that is not the API, tools
arriving mid-turn, and an answer under another calibration.
Claude-Session: https://claude.ai/code/session_018Gt5TvrE2rxXuZikWSauv3
hanzo-loop is the contract the loop applies: a tool shortlist per step, a
command verdict joined with the policy on allow < ask < deny, and routing
hints on the turn metadata. hanzo-kai links hanzoai/decision's control and
program at ec8207d: one controller per thread answers those three through
seven anchored upstream edits, and extension contributors feed it context,
progress and completion. kai.toml turns it on, every operation in shadow by
default. Each decision is one trace line in the shape Enso's control writes.
dev flow runs a Decision Program over the repository. The shipped fix flow
retrieves candidate files, Kai picks one, Zen writes a diff, the tests run,
and Kai says done, retry or escalate within what the tests and the budget
permit. go/dev.wasm is rebuilt because Cargo.toml, Cargo.lock and the
Makefile are among its recorded inputs.
Claude-Session: https://claude.ai/code/session_018Gt5TvrE2rxXuZikWSauv3
`codex-cli/` published `@hanzo/dev`, so the directory carrying our npm
package was named after someone else's product. It is `npm/`.
`patches/codex.json` is `patches/upstream.json` — it anchors edits against
upstream, and the file should say so rather than name one of them.
Deleted outright: `justfile`, whose first line was
`set working-directory := "codex-rs"` for a directory this repository has
never had; `announcement_tip.toml`, an example file for a TUI we do not
ship; and `.codex/` and `.code/`, which held upstream's own process skills
— babysit-pr, codex-bug, pushing-ci-changes — that were never ours.
Three branding leaks a real run turned up, none of which the help text
showed: `dev exec` announced "OpenAI Codex v0.7.0-dev.1" as its first
line, the status card was missed when the session header was anchored,
and both bubblewrap warnings said "Codex's Linux sandbox".
README: say what ships. It claimed the agent drives a browser, which is
the port that has not landed, and showed a screenshot of it doing so.
Both are gone until the bridge is back. The model count is "hundreds" —
`/v1/models` answers 458 today, an earlier line here said "over 500", and
hanzo.ai derives its own count precisely because typed ones drift.
Document the npm install beside the curl one; both give the same build.
LLM.md still described code-rs, build-fast.sh and a VT100 harness under a
directory this repo no longer has — and AGENTS.md, CLAUDE.md and GEMINI.md
all symlink to it, so it is the file every agent reads first. Rewrite it
for the workspace that is actually here, including the rule that upstream
is never edited by hand because `make bump` discards such edits.
Fix five broken images: the README pointed at docs/logo.png and
docs/screenshots/, and they live in docs/images/. Predates the cutover.
State the catalog honestly. "enso and the zen5 family" undersold a gateway
that answers for over five hundred models. Round, not exact — hanzo.ai
derives its count from the pricing snapshot and fails the build on a
hand-typed one, and a number typed here would drift the same way.
The chat surface is the one the 0.6.8x builds had: `>_ hanzo dev (vX)`
and the question, the transcript flush against the edge, the composer a
line with its running status on the row above, "What would you like to
work on?" as the greeting. The status bar, the composer frame, the
gutter and its icons, and the block-letter splash are gone; the merge
that brought them in is what this walks back, on top of the current
TUI.
While here: the free-pool notice is decided by the configured run — the
Hanzo provider on the pool's model — not by which key happens to be in
the environment, so a run against another provider no longer announces
itself as free; the trust screen names Hanzo Dev, not Code.
Snapshots follow the look. Tests that read the gutter for meaning now
read the record (mid-turn state) or the exec label ("Running..."), and
the spacer hysteresis test derives its viewport heights from the
transcript instead of assuming the tall splash.
Co-authored-by: Hanzo Dev <[email protected]>
The GitHub Actions lane was dropped and nothing replaced it, so v0.6.95 was
tagged and never published; npm and every installer still hand out 0.6.94.
The reason given — no runner for four of the five targets — is not true of a
Mac: it builds darwin natively, both linux-musl through cargo-zigbuild and
windows-msvc through cargo-xwin, all verified from this machine. So the
release is .hanzo/workflows/release.yml on the dbc-metal host runner, one
job, every push to main: build, GitHub Release + forge release, npm, Homebrew,
then the bump commit and tag. hanzo_identity guards the new file.
Two things found while proving it end to end:
- `dev login` on the Hanzo path died on a Cloudflare 502 page mid-poll —
"unexpected token response: <!DOCTYPE html>". A body that is not an OAuth
error is the edge, not the issuer; keep polling and let the device code's
lifetime end it.
- The troubleshooting link in agent_tool still pointed at just-every/code.
Merges the forge's 0faa6fe so both remotes share one history; the blob-size
gate it ported into hanzo.yml stays.
Co-authored-by: Hanzo Dev <[email protected]>
codex-rs/ is a read-only mirror of openai/codex that just-every refreshes and
we never edit. It sat at the repo root next to code-rs/, the product, and the
only thing separating them was that everyone knew. Nothing checked it.
That knowing is what makes branding endless. There are ~400 upstream-brand
literals in code-rs today and ~224 of ours, hand-placed on the same lines the
sync delta lands on, so every sync re-fights the same 17 files. You cannot fix
that while the line between "theirs" and "ours" is a convention.
vendor/codex/ makes it a boundary. Everything under vendor/ is upstream: never
branded, never edited, replaced wholesale on sync. Everything outside it is
ours and must carry our name — which is now a question a test can ask.
code-rs coupled to the mirror through exactly one line, an include_str! of
models.json, and it still does. The other 17 files are path strings.
Syncing changes by one sed: just-every's delta names codex-rs/, so rewrite the
patch paths to vendor/codex/ before applying it.
63 commits since the recorded sync point, including just-every's own refresh
of the codex-rs mirror to openai/codex:main. 371 files, but 361 of them are
mirror; the product delta is 8 files in code-rs/ (remote model discovery,
client retry, theme persistence, rollout recorder) plus a package bump.
codex-rs/models-manager/models.json — the one mirror file compiled into the
shipping binary — is unchanged by this delta and already byte-identical to
upstream, so the mirror refresh is hygiene, not function.
Note for the next sync: generate the delta with `git diff --binary`. Two .zst
schema fixtures reject a plain patch, and git apply is atomic, so it rolls
back all 371 files while printing "cleanly" for each one.
The screenshot publisher landed in 3a72312dda, a commit whose subject is about
the Hanzo provider's machine credential and which mentions none of it. This
finishes it and writes down what it is.
Two things it was getting wrong, both found by mutating the code and watching
which test failed:
The payload promised workspace-relative filenames and there was nothing making
that true. A turn diff names files relative to the git root when there is one and
absolutely when there is not, so an agent run outside a repository would have
published the operator's home directory — and their account name — into a log the
whole org reads. `ui::named` relativizes against the workspace and falls back to
the bare filename for anything still absolute, which a diff can produce because
it can name a file the workspace does not contain. The old test asserted the
absence of "/home/..." over a fixture that was already relative, so it passed
whatever the code did; the new one starts from absolute inputs.
Nothing exercised the three calls to cloud. They are now driven against wiremock,
which pins the door (`/v1/s3/buckets/sessions/objects`, then the signed PUT, then
`/v1/agents/sessions/{id}/events`), the exact payload, and the part that actually
matters: the org bearer goes to the two API calls and NOT to the presigned URL,
because the signature is that URL's credential and our token would be a second
one handed to whatever host cloud signed against. Adding `.bearer_auth` to the
upload turns that test red, which is the whole point of writing it.
Also covered: a session id out of the environment reaching a URL path, refused
unless it is alphanumeric-plus-dash-underscore, so `../../admin` cannot retarget
the append at another tenant's session; and cloud's 422 refusal surfacing as an
error rather than being read as success.
LLM.md records the subsystem, since its arrival in the history is mislabelled and
the next reader has no way to find it from the log.
Two things in the Syncing section were wrong in ways that would send the next
person down a multi-month path.
The recorded base was stale. It named e4fe007150 / d1f1f51a41, but main is
synced to just-every 00910569e0 — the v0.6.170 sync (7c9138a677) moved it and
did not update this file. It also prescribed a graft-and-merge recipe that the
last sync did not use; against a disjoint history a graft merge degenerates to
a 3-way apply of the delta, so the doc now describes what actually happens.
And it left "do not merge openai/codex" as prose in another section while this
section explained how to merge, which is how a request to "sync 4d7e3e90d from
openai/codex" reaches someone as a reasonable instruction. Measured today, that
merge is not a sync: no shared merge base, 341 shadowed files and ~175k lines
of divergence in the mirror, 72 upstream crates with no counterpart, and every
subsystem people ask for by name (tool_search, thread_manager, multi_agents_v2,
MCP connection_manager, resize_reflow) absent from code-rs entirely, because
upstream refactored core into ~15 crates while this fork kept the monolith.
The numbers are in the file now so the next person does not have to re-measure
before declining.
Three things the last pass left flagged. Two are done; the third turned out to
belong somewhere else and is reported rather than forced in.
## exec --json emits the schema instead of the SDK guessing it
`exec_events.rs` and `experimental_event_processor_with_json_output.rs` had no
`mod` declaration, so 683 lines defining the `thread.*`/`item.*` schema never
compiled. `sdk/typescript/src/events.ts` says "based on event types from
code-rs/exec/src/exec_events.rs" and then rebuilt those events client-side from
raw core events — the contract existed only in TypeScript, and nothing checked
the two halves agreed.
Declared the modules and fixed what drifted while dead: TaskStarted became a
unit variant, WebSearchEnd became WebSearchComplete, StreamError folded into
Error, ExecCommandEndEvent lost `aggregated_output`, print_config_summary lost a
parameter. `--json` now uses it (`--experimental-json` is an alias, matching
upstream), and the old raw-event processor is deleted rather than left as a
second way to do this. thread.started needed a route because exec consumes
SessionConfigured before the event loop, so EventProcessor grew a defaulted
`on_session_configured`.
Making it real immediately found a lie: the SDK's `Usage` declares five fields
and we emitted three. core::TokenUsage had `cache_write_input_tokens` and
`reasoning_output_tokens` all along; the processor dropped them. Against a mock
model:
{"type":"thread.started","thread_id":"2bd35c77-..."}
{"type":"turn.started"}
{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"Hello from the model."}}
{"type":"turn.completed","usage":{"input_tokens":11,"cached_input_tokens":2,
"cache_write_input_tokens":0,"output_tokens":5,"reasoning_output_tokens":1}}
The SDK now passes those through instead of reconstructing them. Its test suite
did not compile — at HEAD and in just-every upstream: runStreamed.test.ts uses
`path` and `createMockClient` without importing either, testCodex.ts passes
`config` to a type declaring `configOverrides`, and findCodexPath() referenced
four names (`moduleRequire`, `CODEX_NPM_NAME`, `createRequire`,
`PLATFORM_PACKAGE_BY_TARGET`) that were never declared anywhere. Declared them
against @hanzo/dev, added `env` to CodexOptions, pointed the harness at
code-rs/target/debug/dev instead of codex-rs/target/debug/codex.
pnpm exec jest tests/runStreamed.test.ts --testTimeout=120000
3 passed (asserts thread.started, turn.started, item.completed, turn.completed
and item.type === "agent_message" against the real binary)
## queueUserMessage: steer a turn instead of tearing it
`sendUserMessage` submits Op::UserInput, and core/src/codex/streaming.rs:998
calls `sess.abort()` before spawning a new turn — over app-server you could
interrupt a turn, never redirect one. Op::QueueUserInput folds items into the
running task instead, and only the TUI used it. It is now an app-server method.
Separate method, not a flag: interrupting and steering are different acts and a
caller should not pick the destructive one by accident.
→ {"method":"queueUserMessage","id":3,"params":{"conversationId":"999222eb-…",
"items":[{"type":"text","data":{"text":"steer me"}}]}}
← {"id":3,"jsonrpc":"2.0","result":{}}
← {"error":{"code":-32600,"message":"conversation not found: 00000000-…"},"id":4}
## zrok: not ours to build
Assessed and deliberately not implemented here. `hanzo link` already composes
ttyd + share::start + publish_terminal into exactly this feature, and
cli/src/commands/share.rs calls itself "the ONE implementation of 'publish a
local port on the fabric'". `dev` has no framable surface anyway — app-server is
raw JSON-RPC — and no IAM identity to reach POST /v1/share/enable. The gap is in
cloud/cmd/boxd, which already execs `dev`. Details in the report.
Also: git.hanzo.ai release migration precondition written into LLM.md (four
call sites, forge serves /v1/ not /api/v1/, publish to both for a cycle before
moving the updater). SDK package was @openai/codex-sdk; now @hanzo/dev-sdk.
cargo test -p {code-core,code-protocol,code-cli,code-app-server,
code-exec,code-mcp-server,code-tui} -- --test-threads=1
1311 passed; 1 failed
The failure is git_info::test_collect_git_info_with_remote, still environmental:
the new narrower insteadOf rule only covers hanzoai/*, and the test's URL is
github.com/example/repo, which the broad rule still rewrites.
Version 0.2.189 -> 0.2.190.
Co-authored-by: Hanzo Dev <[email protected]>
The TUI header literal came across in the merge as upstream's own product
name. That is the most-looked-at string in the program:
code-rs/tui/src/chatwidget.rs "Every Code" -> "Hanzo Dev"
Regenerating snapshots proves the blast radius exactly — 34 snapshot files
changed, and the whole diff is 46 title lines plus 6 lines where the settings
overlay now reads "Model: ZEN5-Coder" instead of "Model: GPT-5.5". Nothing
else moved, which is the point of running them.
Two more prompts the .md sweep could not see because they live in Rust:
DEFAULT_PERSONALITY_HEADER (prepended to BASE_INSTRUCTIONS whenever a
personality is set, and it asserted "based on GPT-5" even for zen5-coder),
and the sample skill's restart hint.
Three tests arrived with the merge asserting upstream's defaults. Each is
given the provider or model it actually means to exercise, rather than having
its assertion weakened:
app-server get_auth_status - requires_openai_auth is false for the `hanzo`
provider, so the ChatGPT branch is dead by
design. Test now names `openai`.
tui context_mode override - 272k is the GPT-5 window; zen5-coder has none.
Test now names `gpt-5.5`.
cli bash completion - asserted `_code()` while CLI_COMMAND_NAME has
been "dev" since the rename. This one was red
before the merge too. Now asserts our name.
LLM.md's claim that codex-rs changes nothing a user runs was false until this
merge — `dev` used to compile ten crates out of it. Upstream finished the
migration; recorded what is actually left (one models.json via include_str!).
cargo test -p {code-core,code-tui,code-cli,code-app-server,
code-exec,code-mcp-server,code-protocol} -- --test-threads=1
1310 passed; 1 failed
The one failure is git_info::test_collect_git_info_with_remote, which asserts
a literal https remote. This machine rewrites it:
[email protected]:.insteadof https://github.com/
$ GIT_CONFIG_GLOBAL=/dev/null cargo test -p code-core --lib -- \
git_info::tests::test_collect_git_info_with_remote
test result: ok. 1 passed; 0 failed
Co-authored-by: Hanzo Dev <[email protected]>
Our history is just-every's with author emails rewritten, so the graphs are
disjoint and git finds no merge base. Supplied one by hand: d1f1f51a41, the
just-every commit matching our Oct-2025 fork point (22 files apart). 9618
upstream commits, 7004 files, +1.81M/-159k, 37 conflicts.
Resolution:
- Upstream code where our side was the Oct-2025 prototype of a feature they
have since reimplemented 3-5x larger (agent_runs, browser_sessions,
history_cell/{agent,browser}, the vt100 harness and its snapshots).
- Upstream's config.rs module split (builder/defaults/sources/validation)
kept; zen5-coder and the provider-follows-model rule re-applied on top.
- Ours where the file IS the identity: README, package.json, postinstall,
release notes pipeline, integration-zed.
- Hybrid where upstream improved a file we had branded: updates.rs (their
release-repo migration, our hanzoai/dev), release.yml (their fail-fast npm
auth check, our package name), chatwidget.rs (their turn handling, our
HANZO_USER_KEY hint), exec human output (their 237 lines, our banner).
Superseded and dropped deliberately: our resume_command_name() coder-alias
hack — upstream's derive_resume_command_name() reads the invocation basename
generically, which yields "dev" for us.
LLM.md now records the real topology and the sync point; AGENTS.md and
CLAUDE.md are symlinks to it again. Single parent on purpose: adopting
jecode/main as a second parent would re-import 13,742 commits carrying the
upstream author emails a prior rewrite deliberately removed.
Version 0.2.188 -> 0.2.189.
Co-authored-by: Hanzo Dev <[email protected]>
Records what dev's defaults ARE, that code-rs/ is the workspace that ships
(codex-rs/ is the vendored upstream and editing it changes nothing a user
runs), and why hanzo_identity.rs exists: upstream-merge lands openai/codex
every 30 minutes, so an edit to a default is a suggestion and only a red build
is a decision.
Also: model ids come from GET /v1/models with a bearer, never from memory --
the old default 400s against our own gateway -- and postinstall.js plus the TUI
updater are supply chain, not packaging trivia.
Co-authored-by: Hanzo Dev <[email protected]>
One file per repo, three names for it. The names exist because different tools
look for different filenames; the CONTENT existing three times is how they drift,
and a drifted agent guide is worse than none — it is confidently wrong.
Where a name already held the only guide, that file BECAME LLM.md byte for byte
rather than being replaced by a copy, so nothing was rewritten or lost.
Repos where two names held genuinely DIFFERENT guides were skipped, not merged:
symlinking there would delete one of them, and choosing which paragraphs survive
is not a sweep's decision.
Co-authored-by: Hanzo Dev <[email protected]>