Skip to content

perf: faster CI and one scanner identity read, and repo files rewritten under a lock - #744

Merged
REPPL merged 40 commits into
mainfrom
integ/land-15
Sep 29, 2026
Merged

REPPL merged 40 commits into
mainfrom
integ/land-15

Conversation

@REPPL

@REPPL REPPL commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

This lands four reviewed lanes as one change. The test suite stops paying for a flush to disk that no test checks, and CI starts its slowest race packages first. The files abcd rewrites inside a repository (its config, the .gitignore block, the plugin marker, the changelog a release cut writes) are rewritten under a file lock, so two runs at once no longer lose one another's change. Four places where shipped behaviour had drifted from what its intent promised are fixed, and three shipped intents carry their fidelity audit.

ciSpeed (CI speed). A test binary skips the flush to stable storage when the test targets opt in, so the macOS leg no longer spends a sync of about 9 ms on every write a test makes. A built or released binary always flushes, whatever its environment says. Every flush in the module goes through one gate, and a test refuses a flush that bypasses it. The race step names its nine slowest packages first, so they start with the step instead of running alone at its tail. This integration widens the bypass check to catch a datasync, the raw fsync syscall and an fsync taken as a value (the review's LOW), with a test naming each.

lmw127 (repo-local locks). ahoy rewrites the repository's config, .gitignore block and plugin marker under a file lock, and launch ship takes one around the changelog it writes. A change another run made in the meantime is kept, not overwritten. The rules skeleton ahoy writes no longer replaces a rules.json written after it looked.

drainDrift (fidelity drift). A session that backs off on contention logs why and for how many minutes, a session joining the run included, and a hand-logged backoff must name both. An unreadable report in the inbox still names its sender. abcd launch manifests --tree public|dev runs the manifest lockstep check on its own. The public banlist's name gate reaches every surface the shipped artefact carries, held by a test to the launch payload's own include list. One record was already met. Five are deferred as rulings owed to the product thinker.

audits10 (fidelity audits). itd-63, itd-2609212137116617 and itd-131 carry their fidelity audit. The audit files three minor divergences as open captures. One of them is itd-63 criterion 4, a NOT_MET: the release notes for itd-63 should not say the safety gate goes through the install wizard.

Reviews: flakeOracle SHIP (security). scanFold SHIP (security). ciSpeed SHIP (two LOWs, one addressed here). lmw127 SHIP, then fix2 DONE. drainDrift SHIP, then fix2 DONE (MINOR 1 and 3 fixed). audits10 has no code and no review.

Integration. The drainDrift merge conflicted with main's move of the contributing guide to .github/. It was resolved by hunk: the name gate keeps .github/CONTRIBUTING.md and adds the lane's new roots, and the test fixtures read the roots from the config. The Makefile conflict kept both main's pinned toolchain for the preflight and the lane's flush opt-in. Every caller of the changed implement lock compiles and passes. No script in the tree hand-logs a backoff. The reading windows were re-measured at the merged tip: widening 1,327,107 tokens (window 1,350,000, up from 1,340,000), entailment 383,721 (390,000) and detection 1,336,143 (1,350,000). The five deferrals name v0.11.0 and all are minor.

scanFold (one git exec for the scanner's identity). Building a scanner asks git for the caller's identity once instead of four times, or twice when the parent process carries git -c configuration, and gives the same answers: a table of 37 configuration shapes pins them, and a test counting git processes holds the number. Ruling AR (2026-09-28, the user as technical facilitator) asked for the fold and treated it as a trust path. Security review SHIP. Measured on the cli package: identity reads fell from 3,416 git processes to 854, all git processes from 14,698 to 12,130, and a plain test run from 166 s and 183 s to 145 s and 145 s (about 17% faster).

Re-merge after #741. Main's implement log checks (the required fields per event, including the new intervention, stop and decision events, and the agent ceiling held at agent_start) and this branch's backoff check (reason and minutes) both run on every logged event, under the lock that takes the session. The command page, the brief chapter and the verb's help carry both sides: the required-fields table gains a backoff row. The name-gate test fixtures read every configured root from the config, now including the role ban's extra roots from main. The reading windows were re-measured at the merged tip: widening 1,346,734 tokens (window 1,370,000, up from 1,360,000), entailment 388,426 (400,000) and detection 1,355,770 (1,370,000).

flakeOracle (concurrent config reads). Two abcd processes on one machine no longer make one refuse its own config when the other rewrites it. Reading a file under ~/.abcd (the config, the credentials, the rules) or a guarded file inside a checkout checks the file and then opens it; when another abcd process atomically replaced the file in between, the read refused it as swapped. The read now checks the replacement from scratch, up to eight times, and reads it when it passes every check. A symlink, a special file, a file others can write, a file another account owns, or a file that never stops changing is still refused. This is the race that ejected this change from the merge queue twice (TestConcurrentConnectsKeepEveryKeyAndBlock). Security review SHIP.

Re-merge after #746. Main (#742, #743 and #746) merged with one conflict, the widening window's measured figures, where main's were taken before re-measuring. The race fix merged cleanly beside this branch's flush skip in the same file. The race test passes 200 of 200 runs under the race detector on the merged tree with the flush skip CI sets. The reading windows were re-measured at the merged tip: detection 1,358,267 tokens (window 1,380,000, up from 1,370,000); widening 1,349,231 and entailment 390,570 keep their windows.

Resolves: iss-2609281715083637
Resolves: iss-127
Resolves: iss-2609281931185016
Resolves: iss-2609231206196407
Resolves: iss-2609231206207995
Resolves: iss-2609240133234244
Resolves: iss-2609261423222935
Resolves: iss-2609261457358637
Resolves: iss-2609281546130900
Resolves: iss-2609290518278152
Refs: iss-2609231206190526
Refs: iss-2609261423214723
Refs: iss-2609261457353277
Refs: iss-2609261457365969
Refs: iss-2609261457366568
Refs: iss-2609281911015826
Refs: iss-2609281911024838
Refs: iss-2609281911024185
Refs: iss-2609251455354719

Assisted-by: Claude:claude-opus-5-5

itd-74's first criterion gates a banned public token in README, docs/ and
the shipped artefact. The banned_tokens `names/` family read the docs-lint
roots and this repository's name_roots (.abcd, AGENTS.md, CONTRIBUTING.md,
scripts), so a banned token in commands/, agents/ or hooks/, which the
plugin payload carries, shipped ungated. name_roots now lists commands,
agents and hooks: the `names/` family alone widens to them, not the writing
rules. This repository has no skills/ tree, and a root that does not resolve
fails the lint, so the coverage test pins every plugin surface the checkout
carries rather than a fixed list.

Refs: iss-2609261457358637
Assisted-by: Claude:claude-opus-5-5
itd-2609221656373558's sixth criterion: on contention of any kind the second
session backs off and the log names the reason and the minutes spent. Three
gaps closed:

- A run state locked past the lock's timeout by another session's change
  returned contention and wrote nothing. withLock now carries the session it
  acts for, and the second session's backoff from the lock is logged with
  `on: run_state`, the reason and the minutes it waited. The append takes no
  lock, so the line lands while the lock is held.
- The claim-denied backoff carried `minutes: 0` as a constant. It now carries
  the minutes the attempt spent, measured from the claim's start; the
  unreadable-claim contention inside its grace is logged the same way.
- A hand-logged backoff, the only path for contention the verb cannot see
  (the merge queue), could omit both. `implement log backoff` now refuses a
  line without a reason or without minutes as a number no smaller than zero.

The plugin page, the CLI help and the brief's implement surface say so.

Refs: iss-2609231206196407
Assisted-by: Claude:claude-opus-5-5
itd-2609221656361680's fourth criterion has the inbox name the sender
repository of every waiting report. A report the parser refuses (a later
template version, a malformed reporter field) was listed with a 12-hex key
prefix and no name, because the parser judges schema_version before any
other key and never reaches the envelope. That is exactly the report that
most needs a reply: a sender running a newer abcd.

The envelope is abcd's own writing, so report.EnvelopeSender reads it apart
from the reporter's block: the name is taken only when the block names
sender_key once, equal to the key the file is filed by, and sender_name once
in the shape a filing writes. The inbox listing and `inbox show` carry it,
and the text listing's unreadable line names the sender beside the key.

Refs: iss-2609240133234244
Assisted-by: Claude:claude-opus-5-5
…ct thinker

Each record finds a shipped intent's acceptance criterion promising more than
the code does, and making the promise true is a product choice rather than an
implementation step: a workflow cost on the second session, a new writer to
a committed file, a placement fork the record already lists, a verb the
product thinker has not asked for, or a lab runner the intent scopes out.
None of the shipped criteria is edited. Each record carries
deferred_after v0.11.0 and a deferral_reason naming the one question owed
(rulings-owed section D, narrowing a shipped promise).

Refs: iss-2609231206190526, iss-2609261423214723, iss-2609261457353277
Refs: iss-2609261457365969, iss-2609261457366568
Assisted-by: Claude:claude-opus-5-5
…ded already

The fidelity audit read the tree at cede78b and found one live `epic` noun
inside the fenced lifecycle diagram on the brief's intent surface page. The
owed-review drain (af1c19d) rewrote that passage, and a word-bounded grep of
the brief outside the glossary's own declarations finds none, so itd-43's
first criterion holds at the tip with no change here.

Resolves: iss-2609231206207995
Assisted-by: Claude:claude-opus-5-5
…ed plugin surfaces

Resolved by 868b9c3: name_roots carry the names/ family over commands/, agents/ and hooks/, the plugin surfaces the shipped artefact carries.

Resolves: iss-2609261457358637
Assisted-by: Claude:claude-opus-5-5
…and minutes

Resolved by fbbd0d7: a locked run state, a held claim and an unreadable claim each log the second session's backoff with its reason and measured minutes, and a hand-logged backoff without either is refused.

Resolves: iss-2609231206196407
Assisted-by: Claude:claude-opus-5-5
…sender

Resolved by efe9aa6: the inbox reads an unreadable report's envelope apart from its reporter block and names the sender when the envelope does, under the key the file is filed by.

Resolves: iss-2609240133234244
Assisted-by: Claude:claude-opus-5-5
itd-69's first two criteria promise a checker run with `--tree public` and
`--tree dev`. The check (launch.CheckLockstep) took the tree as a Go
parameter and had no front door: the preview and the cut ran it at dev over
the source tree, and only the payload render ran it at public over its own
output, so a public checkout (a marketplace install, a release source
archive) could not be checked at all.

`abcd launch manifests --tree public|dev [--root <dir>]` runs the check over
a named tree at the chosen polarity, through launch.CheckTree, which reads
that tree's own version-location contract and artefact declaration exactly
as the preview does. It reads and never writes, exits 0 consistent, 1 drift
(one line per field) and 2 unreadable or an unknown polarity, and has no
flag that waives a finding. The verb is named for what it checks: a
backticked `lockstep` is the artefact declaration's key in the launch
chapter's prose, which the shape-in-prose gate would read as the verb.

Wired on the plugin page, the sentence manifest and a worked example; the
reference page, the surface snapshot and the chapter appendix are
regenerated, and the chapter's sub-verb table gains its row.

Refs: iss-2609261423222935
Assisted-by: Claude:claude-opus-5-5
…door

Resolved by 9a33bdc: abcd launch manifests runs the manifest lockstep check over a named tree at the polarity the caller chooses, exiting with its 0/1/2 code.

Resolves: iss-2609261423222935
Assisted-by: Claude:claude-opus-5-5
…ads fold

A table of 37 shapes, each built from real configuration files and run
against the real git binary: no configuration, each key alone, multi-valued
keys, every file scope and an includeIf persona, the scrubbed parent
variables (GIT_DIR, the legacy GIT_CONFIG, GIT_CONFIG_* injection, both -c
forms, a malformed -c entry, a -c entry that changes discovery), hostile
values (escaped newline, control bytes, an equals sign, a 70 KB value),
unreadable and malformed configuration, the remote.origin.url rules, and
git absent. Every case passes at the base; the fold that follows must pass
it unchanged. The issue the fold resolves is captured alongside.

Refs: iss-2609281546130900
Assisted-by: Claude:claude-opus-5-5
ProbeIdentity ran four git processes per scanner.New: --get-all user.name,
--get-all user.email, the -z persona listing over user/author/committer,
and --get remote.origin.url. The first, second and fourth read the same
configuration under the same scrubbed environment as the third whenever the
parent carries no `git -c` configuration, so one NUL-delimited listing,
git -C <root> config -z --get-regexp over those seven keys, answers all of
them. The user.* values are joined on newlines and split exactly as the
--get-all listing was; the remote is the last url listed, as --get resolved.

With -c configuration in the parent the persona listing still runs apart,
under the scrubbed env plus only those entries: a -c entry can move
discovery (safe.bareRepository, safe.directory) or fail the whole command
when malformed, so folding it in would let it move or blind the effective
identity and the remote. That case costs two processes.

The 37-shape table pinned in the previous commit passes unchanged; a
counting git shim on PATH holds scanner.New to one exec (two with -c).

Refs: iss-2609281546130900
Assisted-by: Claude:claude-opus-5-5
…it exec

Resolves: iss-2609281546130900
Assisted-by: Claude:claude-opus-5-5
The name_roots pin test hand-listed four plugin surfaces, while the launch
payload also ships .claude-plugin/, LICENSE and .gitignore, so itd-74's first
criterion ("the shipped artefact") was met for the prose surfaces only. The
test now reads the includes through launch.LoadIncludes, the bundler's own
reader, and asserts each is reached by a root or a name root; the three
missing surfaces join name_roots. TestDocsLintHarnessNameGate builds its
fixture roots from the config instead of a second hand list, and the banlist
brief chapter names the widened coverage.

Refs: iss-2609261457358637
Assisted-by: Claude:claude-opus-5-5
… unlogged

Join passed the joining id to withLock, whose contention path looked the
session's role up in its record; a second session's Join has no record yet,
so a Join that met a locked run state logged no backoff, and logBackoff
dropped requireSession's refusal on the floor. Join now takes the lock
through withLockAs with the role it is recording, which places the line when
no record exists; a record that exists still decides the role (a resume).
Any other lookup failure, a session that never joined included, is returned,
so the contention error says the backoff could not be logged. The implement
page and the brief's implement chapter say both.

Refs: iss-2609231206196407
Assisted-by: Claude:claude-opus-5-5
… the slowest race package late

Refs: iss-2609281715083637
Assisted-by: Claude:claude-opus-5-5
…orage

Every durable write in the module now flushes through one call,
fsutil.Flush: the WriteFileAtomic family, CreateExclusiveIn, the
parent-directory syncs and the history-index bootstrap in ahoy. Flush
skips File.Sync (F_FULLFSYNC on macOS, about 8.8 ms a synced write)
only when testing.Testing() reports a test binary AND
ABCD_TEST_SKIP_FLUSH is exactly "1". The Makefile's test and preflight
targets and ci.yml's two test steps set the opt-in; a binary the go
command builds is not a test binary, so the variable does nothing
there, and a shipped abcd flushes whatever its environment says.

The tests pin each half: a truth table over the gate, a durability test
that clears the opt-in and counts two flushes (file and parent) for each
write primitive, a probe under testdata built with go build and run with
the opt-in set that still flushes, and a walk that refuses any direct
Sync, Fsync or F_FULLFSYNC outside Flush. Dropping either half of the
gate on a scratch copy turns the truth table red, and dropping the test
half turns the probe red too. Linking package testing adds 32 bytes to a
stripped release build.

Ruling AR (2026-09-28, the technical facilitator).

Refs: iss-2609281715083637
Assisted-by: Claude:claude-opus-5-5
Two banlist tests lint a fixture tree under the real docs-lint config and
hand-listed its name_roots to make each one resolve, so widening name_roots
to the payload's .claude-plugin/, LICENSE and .gitignore failed them with an
unresolved root. They now take one file per configured root from the config,
as TestDocsLintHarnessNameGate does, so a root added there needs no edit here.

Refs: iss-2609261457358637
Assisted-by: Claude:claude-opus-5-5
The race lane named only ./internal/..., which go test runs in
import-path order, a few packages at a time: on the three-core macOS
runner internal/surface/cli started among the last and ran alone as the
step's tail. The command now names the nine slowest packages under
-race (cli, reading, launch, lifeboat, lint, scanner, capture, ahoy,
site) ahead of the pattern. go test runs a package named twice once, so
the set is unchanged: go list resolves both to the same 78 packages.

The same list goes to all four spellings of the lane (ci.yml's check
job, the Makefile's preflight, release.yml's verify job and the
scaffold template it is rendered from). TestRaceLaneBudgetIsDeclared-
AndFitsItsJob now also holds the three commands to one package list and
resolves it with go list to exactly ./internal/...'s set, with the
pattern last; dropping the pattern or adding a package outside it turns
the test red.

Measured on a scratch snapshot with -p 3 to stand in for the runner:
845s in import-path order (cli started at 546s and ran alone for its
last 171s), 557s slowest-first. On the last green macOS run
(36444242193) the step took 888s and cli, 259s, finished last.

Ruling AR (2026-09-28, the technical facilitator).

Refs: iss-2609281715083637
Assisted-by: Claude:claude-opus-5-5
…owest race packages start first

Resolves: iss-2609281715083637
Assisted-by: Claude:claude-opus-5-5
The ahoy rewrites of .abcd/config.json (the skeleton seed, the four config
values, the version stamp and the attribution opt-in), of abcd's .gitignore
block and of the marker block in CLAUDE.md / AGENTS.md, and the release cut's
writes to CHANGELOG.md, each read a file, changed it in memory and wrote it
back with no lock, so two abcd runs in one working tree could write from the
same stale read and erase each other's change.

Each now reads and writes under fsutil.WithFileLock, the one inter-process
load-modify-write primitive, on a lock file beside the guarded file
(.config.json.lock, .gitignore.lock, .CLAUDE.md.lock, .CHANGELOG.md.lock).
The holder removes the lock file before letting go, which WithFileLock's
inode revalidation makes safe, so a rewrite leaves nothing in the user's
tree. The config-values step prompts first and takes the lock only for the
re-read and the write. The release lock covers the whole ingest, derivation
included, and the undo, so a second cut derives after the first wrote and
is refused as a release in flight instead of overwriting it.

Each lock is a leaf: nothing is taken inside it, and the one caller that
holds other locks (lifeboat embark, ledger then intent then spec) plants its
marker after releasing them.

Intent transitions needed no change: at this base every intent writer
already reads and writes under the intent store's lock (withIntentMintLock,
WithLedgerThenMintLock).

Refs: iss-127
Assisted-by: Claude:claude-opus-5-5
The four families the record names, surveyed at this base: intent
transitions were already guarded by the intent store's lock; ahoy's
config.json, .gitignore-block and marker-block rewrites and the release
cut's CHANGELOG path now take fsutil.WithFileLock (352b21e).

Resolves: iss-127
Assisted-by: Claude:claude-opus-5-5
…17, itd-131

The three intents v0.12.0 ships were audited against the tree at
ceb4b6d (promise vs delivered reality, agents/intent-auditor.md, Role 1)
and each verdict ingested into the intent's Audit Notes through
`abcd intent audit ingest` from re-emitted requests.

- itd-63 (setup wizard explains installs): MET 2, MET_WITH_CONCERNS 2,
  NOT_MET 1. Criterion 4 names a safety gate that is not on main (itd-62
  is a draft); the gh explanation is shown but its install is never
  offered. Both captured.
- itd-2609212137116617 (a new capture or draft is matched against the
  record): MET 5; the one scope condition survived. The ledger's other
  writers (inbox promote, consistency and reading ingest) file without the
  match; captured.
- itd-131 (managed-repo identity gate): MET 4, MET_WITH_CONCERNS 1. The
  runner half of criterion 4 rests on iss-2608210932052003, still open;
  no new record, the open issue is the marker.

Refs: iss-2609281911015826
Refs: iss-2609281911024838
Refs: iss-2609281911024185
Refs: iss-2608210932052003

Assisted-by: Claude:claude-fable-5-1
stepRules planted the empty .abcd/rules.json skeleton through writeRepoJSON,
an atomic rename with no re-check, after the interactive prompt phase.
Detection had set rules.missing before the prompts, so a rules.json written
in that window (a hand-written override) was replaced by the empty-domains
skeleton and the receipt said the rules were written.

The skeleton is now created through fsutil.CreateExclusiveIn behind a new
createRepoJSON (store.go), contained through the same os.Root as
writeRepoJSON. The exclusive create is the re-check and the write in one
act, so no lock is taken and every lock stays a leaf. An existing file is
kept, not written: no receipt line and no refusal, the shape stepSkeleton
uses for a config written meanwhile. The new file's mode stays pinned to
0644 as the atomic writer gave it.

Sweep: writeRepoJSON's only other caller is writeConfig; its one
create-if-absent (stepSkeleton) already re-checks under the config lock.
The banlist scaffolds already create through CreateExclusiveIn.

Refs: iss-2609281931185016
Assisted-by: Claude:claude-opus-5-5
…tten meanwhile

Resolves: iss-2609281931185016
Assisted-by: Claude:claude-opus-5-5
Tests skip the flush to stable storage when opted in, and the slowest
race packages start first.

Assisted-by: Claude:claude-opus-5-5

# Conflicts:
#	Makefile
…anch

The repo-local rewrites ahoy and release make take a file lock, and the
rules skeleton keeps a rules.json written after detection.

Refs: iss-127, iss-2609281931185016
Assisted-by: Claude:claude-opus-5-5
…n branch

Four fidelity-drift records are built, one was already met and five are
deferred as rulings owed to the product thinker.

Conflicts, resolved by hunk: main moved the contribution guide to
.github/ (c250bc7) while this lane widened the name gate's roots and
rebuilt the test fixtures from the config. docs-lint.json keeps main's
.github/CONTRIBUTING.md root and adds the lane's commands, agents, hooks,
.claude-plugin, LICENSE and .gitignore; the banlist brief chapter names
the moved path in the lane's sentence; nameroots_test.go pins the moved
path through the lane's prefix-covering check; public_test.go and
lint_test.go take the lane's config-derived fixture loops and keep
main's provisionDocsLintTrees and links_resolve blocks around them.

Assisted-by: Claude:claude-opus-5-5
…branch

Three shipped intents carry their fidelity audit, and the three minor
divergences it found are captured.

Assisted-by: Claude:claude-opus-5-5
…ased one

The walk that holds every flush to fsutil.Flush matched only a Sync()
call, a called Fsync and the macOS full-flush fcntl. The ciSpeed review
found three spellings it let through: an Fdatasync, the raw
SYS_FSYNC/SYS_FDATASYNC syscall number, and an Fsync taken as a value and
called under another name. None exists in the tree, and a miss would
flush more in tests, never less in a shipped binary, but the walk exists
to keep the gate the only flush. The pattern now names each, and a table
test pins each form and the lines that must stay clear of it.

Refs: iss-2609281715083637
Assisted-by: Claude:claude-opus-5-5
Measured on a clean clone of 5a7d45d (dry-run assemble). Widening
dropped under the one per cent headroom the rule asks for, so its window
moves to the next boundary that leaves it; the other two keep theirs:
- widening 1,327,107 tokens / 5,109,365 bytes: 1,340,000 left 0.97%,
  so 1,350,000 (1.73%)
- entailment 383,721 / 1,477,329: 390,000 kept (1.64%)
- detection 1,336,143 / 5,144,153: 1,350,000 kept (1.04%)
comparative is unchanged.

Refs: iss-2609251455354719
Assisted-by: Claude:claude-opus-5-5
Seven files conflicted and each is resolved by hunk, both sides kept:

- implement/log.go: main's checkFields (drainI's required fields per
  event, the agent_start ceiling) and this branch's backoffFields
  (reason= and minutes=) both run before the lock; withLock takes the
  session (drainDrift) and requireSession's record feeds main's ceiling
  check.
- cli/implement.go help, commands/implement.md and brief 27-implement.md:
  main's required-fields text and table, with the backoff's reason and
  minutes added (a table row, and the lock-contention backoff sentences
  kept inside main's enforced-ceiling paragraph).
- banlist/public_test.go and lint/lint_test.go: this branch's
  config-derived name_roots fixtures, widened to every banned token's
  extra_roots so main's role-ban roots resolve without a second list.
- reading-presets.json: main's figures, re-measured at the tip after.

DECISIONS.md and the release files are main's (this branch adds no
ledger line); `## [Unreleased]` is empty. go generate refreshed
commands.md.

Assisted-by: Claude:claude-opus-5-5
scanner.New reads the caller's identity with one git exec (two when the
parent carries `git -c` configuration) instead of four; the 37 pinned
ProbeIdentity answers are unchanged. Merged cleanly; build, vet, the
scanner and gitutil tests (the pin table and the exec-count test) pass.
Security review SHIP (ruling AR).

Resolves: iss-2609281546130900
Assisted-by: Claude:claude-opus-5-5
Measured on a clean clone of 3a282f5 (dry-run assemble). Widening
1,346,734 tokens left 0.99% under 1,360,000, so it moves to 1,370,000;
entailment 388,426 keeps 400,000 (2.98%); detection 1,355,770 keeps
1,370,000 (1.05%). Comparative is untouched.

Refs: iss-2609251455354719
Assisted-by: Claude:claude-opus-5-5
@REPPL
REPPL enabled auto-merge September 29, 2026 04:16
@REPPL
REPPL added this pull request to the merge queue Sep 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 29, 2026
@REPPL
REPPL added this pull request to the merge queue Sep 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 29, 2026
…claration read refuse

The merge-group macOS leg of PR 744 failed
TestConcurrentConnectsKeepEveryKeyAndBlock: one connect refused
~/.abcd/config.json because another connect renamed its rewrite into place
between the guarded read's vetting lstat and its open.

Refs: iss-2609290518278152
Assisted-by: Claude:claude-opus-5-5
…ts read

ReadDeclaration vets a home-scoped declaration on an Lstat, opens it, and
confirms with os.SameFile that the descriptor is the file it vetted. A
file renamed into place inside that window was refused on sight, so the
ordinary rewrite another abcd process of the same user makes through
WriteFileAtomic made the reader refuse its own ~/.abcd/config.json
(TestConcurrentConnectsKeepEveryKeyAndBlock on the loaded macOS CI leg).

A replacement is now judged from scratch: the Lstat and every guard run
again on whatever the path names, up to declarationAttempts (8) times,
and the bytes returned are always those of a descriptor os.SameFile ties
to an Lstat that passed every guard. A same-owner, owner-only-writable
regular file is read; a symlink, FIFO or directory, a file writable by
group or other, and a file another uid owns are refused by the guard that
judges each, as if they had been there before the first Lstat; and a
replacement still unsettled after the bound is refused with
ErrDeclarationSwapped. Refusing on sight bought nothing the re-vet does
not: whoever can rename a guard-passing file into place after the vetting
can equally do so before it.

A shared lock on the writers' lock file was the other option. It was not
taken: the readers are generic (layered.Load reads every family, rules and
trusted-roots have no writer lock), a read would create a lock file in
~/.abcd, and a hand edit or any writer outside abcd's lock would still
trip the refusal.

ReadGuardedInRoot closes the same window for files inside a checkout and
refused a WriteFileAtomicInRoot rewrite the same way, as ErrNotRegular; it
takes the same bounded re-vet and still answers ErrNotRegular for a
non-regular replacement or one that outlasts the bound.

TestReadDeclarationRefusesAFileSwappedInAfterVetting asserted the refusal
on sight of a guard-passing file; it is replaced by the endless-replacement
and non-benign-replacement detectors in replaced_test.go.

Refs: iss-2609290518278152
Assisted-by: Claude:claude-opus-5-5
… replacement

The fix is b342b2b: ReadDeclaration and ReadGuardedInRoot judge a file
renamed into place after their vetting from scratch, a bounded number of
times, rather than refusing it on sight.

Resolves: iss-2609290518278152
Assisted-by: Claude:claude-opus-5-5
Main carries #743 (gateFix), #742 (drainAhoy) and #746 (integ14).
One conflict, .abcd/config/reading-presets.json (widening's measured
figures): main's figures taken on the conflict hunk only; the windows
are re-measured at the integration tip afterwards. DECISIONS.md and
CHANGELOG.md are main's byte for byte; [Unreleased] is empty.

Assisted-by: Claude:claude-opus-5-5
A guarded declaration read (ReadDeclaration) and an in-root guarded
read (ReadGuardedInRoot) re-vet a file a peer process replaced between
the vetting lstat and the open, up to eight times, instead of refusing
on sight. This closes the oracle race that ejected this branch from the
merge queue (TestConcurrentConnectsKeepEveryKeyAndBlock). Security
review: SHIP.

internal/fsutil/fsutil.go merged cleanly by hunk: the re-vet loop and
its test hooks sit beside this branch's flush skip (ciSpeed, flush.go
and the syncFile path), which neither side's hunks overlap.

Resolves: iss-2609290518278152
Assisted-by: Claude:claude-opus-5-5
Dry-run assemble on a clean clone of 24e7850: detection measures
1,358,267 tokens / 5,229,331 bytes, which left the 1,370,000 window
0.86% headroom, so it moves to 1,380,000. Widening (1,349,231, 1.54%)
and entailment (390,570, 2.41%) keep their windows and figures.

Refs: iss-2609251455354719
Assisted-by: Claude:claude-opus-5-5
@REPPL
REPPL enabled auto-merge September 29, 2026 06:46
@REPPL
REPPL added this pull request to the merge queue Sep 29, 2026
Merged via the queue into main with commit baf6f84 Sep 29, 2026
13 of 14 checks passed
@REPPL
REPPL deleted the integ/land-15 branch September 29, 2026 07:25
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