Skip to content

fix: preflight runs on CI's toolchain, and the audit and intent-lock chain - #735

Merged
REPPL merged 71 commits into
mainfrom
integ/land-10
Sep 28, 2026
Merged

REPPL merged 71 commits into
mainfrom
integ/land-10

Conversation

@REPPL

@REPPL REPPL commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator

Integration branch 10: reviewed lanes land as one PR, with one window recalibration and one preflight. The audit/lock chain is four lanes that built on one another, merged as one merge of its tip. ciFast already landed with integration branch 6 (#730); this branch carries the same commits, so they merge as already merged and its records are not declared again here.

drainGate (preflight gate parity). make preflight runs its Go steps on the toolchain go.mod declares, through one resolver that refuses rather than falling back to the Go on PATH, so a test that passes locally also passes on CI's toolchain. Preflight arms record-lint over the branch's own range, as CI does. docs-lint checks the links of every committed markdown file and arms the persona rule. RS001 and RS005 name a merge-queue collision instead of blaming history from before the branch diverged. The warn-baseline ratchet that remains of the lint-scope record needs a ruling and is deferred past v0.11.0 on the record.

ciFast (the macOS check job under the queue's cap; already on main through #730). The race-instrumented time of the cli package roughly halves with every assertion kept. The 9 MiB history capture and the surface-prose check run in the uninstrumented lane only, on both CI legs. The prose check skips a command path whose words the prose never spells. Record-id filename grammars compile once per family. No workflow, timeout, required job name or ruleset changes.

The audit/lock chain (drainA1, drainA2, drainL, drainSpec). An intent's review block has one bounded reader and one writer. A re-ingest keeps link references and human notes below the block, and markers count only on live lines. The fidelity and consistency requests state the condition identities, the count and the exact verdict or findings shape, and the emit and ingest results say what they did. A stale-policy refusal names the re-emit. The verdict, consistency and reading ingests refuse agent prose that cites a record id that does not resolve. Every writer of ledger, intent and spec records takes its store's lock through one seam, in one order: ledger, then intent, then spec. A waiting writer never holds an earlier lock while it waits for a later one. That covers the verdict ingest, the review emit, related-issue edges, spec links, every repoint, capture migrate, embark, spec close and discard, and plan's rollback.

Done at the tip. Wording from the ciFast review: the race-skipped capture starts no goroutine of ours, which is not the same as one goroutine, and the prose prefilter's blanking replaces bytes with spaces. A finding from the drainSpec review is fixed: spec close and discard planted an empty spec store on a tree without one. It was captured, fixed test-first and resolved here. The cold-reading windows are recalibrated at the tip (see Re-merge).

Operational. Once this lands, a fidelity-audit or consistency request written before it cannot have its verdict ingested. That covers the request files in any checkout's local tier and every OWED marker on main, which at this tip are six: itd-63 (rcp-3c9fb4ba9770), itd-131 (rcp-8a6673e9bc8a), itd-2609150819432059 (rcp-97519c4308ad), itd-76 (rcp-595934bbc552), itd-2609212137116617 (rcp-1887e5f574ef) and itd-2609212130146198 (rcp-68f6cce6a701). Each needs a re-emit before any verdict for it is ingested. The request now carries the verdict shape, so its prompt hash moves. Re-emit the request (abcd intent audit <itd>, or abcd intent consistency for the consistency pass), then run the review again. The ingest refusal says this.

Re-merge. Main after integration branches 6 to 9 (#730 to #733) is merged in. Two files conflicted, both by hunk: an import block in the capture workflow, where both sides' imports are kept, and the reading presets, which are re-measured. docs-lint's link check still names the root CONTRIBUTING.md and SECURITY.md, because both are at the repository root on main. The drainSpec LOW is closed properly: a spec store removed between close's or discard's decision to lock it and the lock itself is no longer re-planted empty, because the lock for those two writers refuses an absent store rather than creating one (tested through a seam that removes the store at the lock, red before the fix). No intent-record writer arrived on main outside the lock seam: main's new code reads intent and spec records and writes none. Windows at the re-merged tip: widening 1,340,000 (measured 1,322,032), entailment 390,000 kept (382,812), detection 1,350,000 (1,331,068).

Reviews: drainGate SHIP (one LOW accepted: its commits from the docs-lint widening to the RS001 resolution are red until the last test commit, and main keeps merge commits). ciFast SHIP (two wording findings, fixed at the tip). drainA1 SHIP (review and re-review). drainA2 SHIP. drainL SHIP, and its fix round verified SHIP. drainSpec SHIP (one LOW, fixed at the tip).

No intent ships here.

Resolves: iss-2609020529185438
Resolves: iss-2609021152026246
Resolves: iss-2609090951287096
Resolves: iss-2609091433422134
Resolves: iss-2609181121301638
Resolves: iss-2609181121305984
Resolves: iss-2609190337545165
Resolves: iss-2609190337598356
Resolves: iss-2609231011136579
Resolves: iss-2609231036448320
Resolves: iss-2609251451432601
Resolves: iss-2609251451434656
Resolves: iss-2609261254247117
Resolves: iss-2609261835118276
Resolves: iss-2609261850045839
Resolves: iss-2609261935343851
Resolves: iss-2609261935407925
Resolves: iss-2609261935407995
Resolves: iss-2609261941039204
Resolves: iss-2609262011046013
Resolves: iss-2609262104070666
Resolves: iss-2609262143209970
Resolves: iss-2609262143265180
Resolves: iss-2609262218059995
Resolves: iss-2609262218306589
Resolves: iss-2609262218309668
Resolves: iss-2609262257227538
Resolves: iss-2609262342345159
Refs: iss-46
Refs: iss-2609262011091645
Refs: iss-2609251455354719

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

The verdict ingest wrote 39 shipped intents back with no final newline
(the replace-in-place path dropped the separator), so each ends on the
last byte of its review block. Every other record writer ends its file
with a newline. Each file gains exactly that one byte; no line changes.

Refs: iss-2609231011136579
Assisted-by: Claude:claude-opus-5-5
The audit's review block is read and written in one place,
readReviewBlocks, and every question the ingest asks of a record goes
through it.

- Liveness: a marker counts only on a live line of the live Audit Notes
  section, through condition.AuditNotes over mdrecord.Mask (the one
  fence-and-comment rule). A marker-shaped line in a fence, a comment
  span or another section is an example, not state, so it can neither
  solicit a verdict nor be reused as the parked receipt.
  appendToAuditNotes finds its section the same way, so the writer never
  appends under a fenced example the reader cannot see, and
  condition.ReadDispositions reads live lines only.
- Extent: every renderer closes its block on a
  `<!-- abcd-review-end receipt=rcp-… -->` line, and the block ends
  there, so prose written below a block survives a replacement and an
  identical re-ingest stays a noop. A block written before the closing
  line existed is read by its known extent: an OWED stub is its marker
  and one sentence, and any other block keeps the rule it always had
  (next marker, heading or end of section) less a trailing run of
  link-reference definitions, which no renderer writes. An identical
  re-ingest over such a block is still a noop, so no tree record is
  rewritten to gain the line.
- Final newline: the record every write returns ends in a newline.
- Citations: a rendered block (verdict, re-ingest or dead letter) whose
  prose cites a record id that names no record is REFUSED, naming the
  id and its line, with nothing written, where the repository's
  record-lint arms prose_citation_resolves over the intent store. Refusal
  over sanitising: the verdict schema cannot mark an id illustrative, and
  termsafe neutralises syntax, never a citation's meaning, so a rewrite
  would be a second sanitiser guessing what the auditor meant. The check
  is the gate's own reading (lint.UnresolvedProseCitationsInRecord, which
  shares the per-line loop with the rule), registered by the front doors
  through intent.SetProseCitationGate because lint's tests import intent;
  an unregistered gate refuses the ingest.

The brief's intent surface row and commands/intent.md state the closing
line, the liveness rule and the citation refusal.

Watched red on a scratch archive of d850f06: TestIngestedRecordEndsIn
ANewline (no-newline stub, dead letter), TestFirstIngestKeepsLinkRefs
BelowTheOwedStub, TestReplacementKeepsLinkRefsBelowALegacyBlock,
TestReingestKeepsAHumanNoteBelowTheBlock, TestOnlyALiveMarkerCounts,
TestAFencedMarkerIsNotASolicitation, TestIngestRefusesAnUnresolvable
Citation, TestAppendLandsInTheLiveAuditNotes,
TestReadDispositionsReadsOnlyLiveBlocks and the CLI's
TestIntentAuditIngestRefusesAnUnresolvableCitation.
TestEveryTreeReviewBlockRoundTrips holds every intent record in the tree
to the byte pattern's reading and to a byte-identical write-back.

Refs: iss-2609231011136579, iss-2609231036448320, iss-2609251451432601
Refs: iss-2609251451434656, iss-2609020529185438
Assisted-by: Claude:claude-opus-5-5
…tion gate

Found sweeping the verdict ingest's siblings: the consistency ingest and
the reading ingest write host-delegated prose into stores
prose_citation_resolves reads, with no check before the write. Left for
its own lane: capture cannot import core/lint, so the fix is the same
front-door seam, and each writer decides whether it refuses the payload
or the one finding.

Refs: iss-2609261835118276
Assisted-by: Claude:claude-opus-5-5
The fix in 5596495 gives the intent audit one live, bounded reader and
writer for the review block: records end in a newline, a verdict citing
an id that names no record is refused before any write, the block's
extent is bounded so trailing link references and prose below it
survive, and only a marker on a live line of Audit Notes is state.

Resolves: iss-2609231011136579
Resolves: iss-2609231036448320
Resolves: iss-2609251451432601
Resolves: iss-2609251451434656
Resolves: iss-2609020529185438
Assisted-by: Claude:claude-opus-5-5
…the PATH toolchain

Refs: iss-2609261850045839

Assisted-by: Claude:claude-opus-5-5
The format gate captured `GOTOOLCHAIN=go<v> go env GOROOT` with stderr
merged into stdout, so on a machine without the declared toolchain cached
the fetch's "go: downloading ..." line became the first line of the
captured root. The executable test on that two-line path failed and the
gate refused, blaming the network, on the run where the fetch had just
succeeded.

The resolution moves out of the Makefile into scripts/pinned-toolchain.sh,
which prints the GOROOT on stdout and nothing else and lets go's own
progress and errors pass through on stderr. It refuses (exit 2) and names
the skew for a fetch that failed, and separately for a root that holds no
go or gofmt, or a go that reports another release. `make fmt-check` and
`make fmt` run the gofmt under the root it prints.

The resolver's branches are driven against a stub go in
TestPinnedToolchainResolver*; the progress case failed on the extracted
merged-stream capture before the streams were separated.

Refs: iss-2609090951287096

Assisted-by: Claude:claude-opus-5-5
…dout alone

Resolves: iss-2609090951287096
Assisted-by: Claude:claude-opus-5-5
make preflight built, vetted and tested on the go on PATH (go1.27.1 on
the machine that found it) while CI builds and tests with the release
go.mod declares (go 1.26.7). On 2026-09-26 pull request 728 passed
preflight and failed CI on a test whose assertion depended on go 1.27's
encoding/json error wording.

preflight now exports GOTOOLCHAIN=go$(GO_TOOLCHAIN_VERSION), read from
go.mod by the same line the format gate uses, to every Go step it makes:
its build, vet, test and race lines and each go run and go test of its
prerequisites. The go on PATH switches to the declared release and puts
its bin first on PATH for what it runs, so a go a test execs is the
declared one too. fmt-check, CI's format gate, joins preflight second,
straight after the load check: its resolver, scripts/pinned-toolchain.sh,
is the one resolver, and it refuses, naming the skew, before any gate
runs on a toolchain that cannot be fetched. That also closes iss-46's
"gofmt missing from make preflight" parity gap: preflight runs every
gate CI's check job runs.

Gates whose behaviour changes: preflight (pinned toolchain for every Go
step; fmt-check as a prerequisite). AGENTS.md, CONTRIBUTING.md, the
install guide, the pre-push hook header and the Makefile comment say so.

TestPreflightRunsTheDeclaredToolchain failed on the missing export and
on fmt-check's absence from the prerequisite list before this change.

Refs: iss-2609261850045839, iss-46

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

Resolves: iss-2609261850045839
Assisted-by: Claude:claude-opus-5-5
make preflight ran record-lint without -agent-diff, so agent_contract's
unbumped-edit check (a changed agent prompt bumps its prompt_version and
adds its agents/CHANGELOG.md entry) was a no-op locally and armed only in
CI, which passes -agent-diff "${BASE_SHA}...HEAD". Pull request 606 passed
three green preflights and was refused in the merge queue for exactly
that check.

The record-lint target now passes -agent-diff origin/main...HEAD, the
merge-base range CI's step passes from its base, and needs origin/main
as lint-issues and lint-decisions already do. Gate whose behaviour
changes: record-lint, standalone and under preflight.

In a scratch clone with an agent prompt edited and its version unbumped,
the old recipe printed no blocker and the new one refuses with
agent_contract. TestPreflightArmsRecordLintAsCIDoes failed on the
unarmed recipe before this change.

Refs: iss-2609021152026246

Assisted-by: Claude:claude-opus-5-5
…I does

Resolves: iss-2609021152026246
Assisted-by: Claude:claude-opus-5-5
… rule

Three of iss-46's lint scope holes, each mechanical:

- links_resolve read .abcd/development and .abcd/work (record-lint) and
  docs/ and README.md (docs-lint), and nothing else: 59 committed markdown
  files, the root prose, the agent prompts, the plugin command pages and
  the READMEs, had relative links no gate checked. docs-lint's
  links_resolve now walks them through extra_roots. They carry no broken
  link today; a planted one in AGENTS.md is refused.
  TestEveryCommittedMarkdownFileHasItsLinksChecked derives its roster
  from git ls-files, so a markdown file added anywhere joins by existing
  or is named in its exemption list with the reason (eval fixtures, the
  templates ahoy writes into other repositories, the symlinked mirrors).
  It failed with 59 files against the old config.
- persona_registry ran over the design record alone. docs-lint now arms
  it against the same roster and severity (TestDocsLintArmsThePersonaRule,
  red against the old config); docs/ carries no unregistered persona
  today, and a planted one is refused. The docs-lint seed withholds it:
  a prepared repository has no persona roster to read.
- CONTRIBUTING.md documented how to activate the committed hooks but not
  that the pre-commit name guard depends on the per-machine banlist, and
  warns and lets the commit through without one.

Gate whose behaviour changes: docs-lint (wider link scope, persona rule),
in preflight and CI alike.

Refs: iss-46

Assisted-by: Claude:claude-opus-5-5
iss-46 named five lint scope holes. Four are closed in this branch: the
link check walks every committed markdown file, docs-lint arms the
persona rule, preflight runs fmt-check and an armed record-lint on CI's
toolchain, and CONTRIBUTING.md documents the hooks' banlist dependency.
The fifth, a warn-baseline ratchet in record-lint with a scope matrix
beyond links, is a design that needs a ruling (which warn rules freeze,
where the baseline lives, how it only shrinks), so the record stays open
with deferred_after: v0.11.0 and that reason.

Refs: iss-46

Assisted-by: Claude:claude-opus-5-5
When a competitor lands a record's resolution while a merge-queue entry
waits, the record is terminal at the entry's base and enters nothing
across the range, so RS001 refuses the entry's trailer, rightly. The
diagnosis probed head..base for the base-side commit that placed the
record, and in the queue the base is an ancestor of the head, so that
walk is always empty and the only message selectable was "already sat in
resolved/ before this branch diverged ... Drop the trailer": history that
never happened, and a remedy aimed at a trailer that was right when it
was written.

A new probe, landed_while_waiting, answers in exactly that shape: when
the base is an ancestor of the head it walks from the declaring commit's
own fork point to the base, and a base-side commit that placed the record
there is named as the competitor's landing, with rebase-and-reconcile as
the remedy. A record terminal before the branch was cut keeps the message
it has, in the queue and out of it. RS005 drew the same stale-branch
split for the intent store and had the same blind spot; it takes the
same probe.

Gate whose behaviour changes: check-issue-resolution.sh commits (RS001,
RS005 refusal text only; every verdict is unchanged). The queue cases in
check-issue-resolution-cases.sh failed against the old probe.

Refs: iss-2609091433422134

Assisted-by: Claude:claude-opus-5-5
Resolves: iss-2609091433422134
Assisted-by: Claude:claude-opus-5-5
TestDocsLintHarnessNameGate loads the real .abcd/docs-lint.json into a
temporary repository, and a configured tree that does not resolve is a
load error. Widening links_resolve's extra_roots and arming
persona_registry in docs-lint made the fixture fail to load for want of
those trees and the persona roster. It now creates each extra root and a
roster from the config it loads, so a new root needs no edit here.

Refs: iss-46

Assisted-by: Claude:claude-opus-5-5
Two banlist tests lint a fixture repository with the real docs-lint
config, and the config's new links_resolve extra roots and persona
roster did not exist there, so the load refused. A helper creates every
tree the config reads beyond its roots and name_roots, read from the
config itself.

Refs: iss-46

Assisted-by: Claude:claude-opus-5-5
The verdict ingest (the review-drainA1 flag), the fidelity-review emit and
the related-issue/link frontmatter writes each rewrite an intent record as a
read-modify-write outside withIntentMintLock.

Refs: iss-2609261935343851
Refs: iss-2609261935407925
Refs: iss-2609261935407995

Assisted-by: Claude:claude-opus-5-5
IngestVerdictBytes resolved the receipt, judged the record and wrote it
outside withIntentMintLock, so two ingests on one intent, or an ingest
beside a condition disposition, each wrote the bytes they read and the
later erased the earlier with both exiting 0. The receipt resolution,
every check on the record and the write(s) now run in one hold
(ingestLocked); reingestVerdict and deadLetter are reached only from
there, so the dead-letter's two writes land in the same hold. A
repository with no intent store is refused without the lock, which would
otherwise create the store in a refusal.

The tests land a whole verb in the window through the lock seam rather
than on the wall clock: a verdict ingested first makes the second a
reported replacement; a malformed verdict never dead-letters over a
verdict that landed; a condition disposition survives the ingest.

Refs: iss-2609261935343851

Assisted-by: Claude:claude-opus-5-5
…ore lock

Resolves: iss-2609261935343851
Assisted-by: Claude:claude-opus-5-5
emitAuditWith, reached from the spec-close ship move, the bundle close,
`intent audit` and the audit drain, read a shipped intent, parked the
OWED stub and wrote the record back outside withIntentMintLock, so a
condition disposition or verdict ingest landing between its read and its
write was erased. The read, the marker judgement and the writes now run
in one hold (emitLocked); every caller reaches it after any hold of its
own is released, so the lock is never taken twice.

Refs: iss-2609261935407925

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

Resolves: iss-2609261935407925
Assisted-by: Claude:claude-opus-5-5
… lock

AddRelatedIssue (the intent half of `capture promote --intent`, any
bucket) and Link (`intent link`) rewrote the intent's frontmatter as a
read-modify-write outside withIntentMintLock, so a hold, a disposition,
an ingest or a second edge landing between the read and the write was
erased. Both now read and write in one hold, and AddRelatedIssue judges
the existing list on the bytes read there rather than on the corpus, so
two edges written to one intent both stand.

Refs: iss-2609261935407995

Assisted-by: Claude:claude-opus-5-5
…tore lock

Resolves: iss-2609261935407995
Assisted-by: Claude:claude-opus-5-5
Left open by the fix2-drainA1 sweep: the fix needs an exported seam for
another package to take the intent store lock, the gap relink.Repoint
already records.

Refs: iss-2609261941039204
Refs: iss-2609261254247117

Assisted-by: Claude:claude-opus-5-5
The consistency request carries no findings shape, the Role 2 sibling of the
fidelity request's missing verdict shape; and the fidelity request's delivered
line still leaves the diff range to the host, the third addendum of the
scope-condition record, which needs a design call on attestation.

Refs: iss-2609262011046013
Refs: iss-2609262011091645
Refs: iss-2609181121301638
Refs: iss-2609181121305984

Assisted-by: Claude:claude-opus-5-5
The verdict ingest returns one struct for every outcome, so a dead-lettered
verdict's JSON carried zero-valued criteria counters beside a conditions count,
which a lane took for a rollup. Its JSON now states what the ingest recorded
(`recorded`: verdict, quarantine or nothing) and carries the acceptance rollup
and the disposition split only beside a recorded verdict, where a zero is a
count. A quarantine keeps the one split it did record, every scope condition
untested, as `conditions_untested`, so the agreement with the record that
iss-2608300927241768 asked for still holds. The Go fields are unchanged, so the
text render and every in-process reader are untouched.

A re-emit on an OWED receipt rewrote the request and reported only
already_owed, which read as nothing happened. The emit result now names the act
beside the state (`request_written`), and `request_path` is the request this
emit wrote: a terminal receipt, whose emit writes nothing, names none rather
than a path to a request it did not write (the same honesty rule from the other
side). The text render says `request rewritten:` on an owed re-emit and `no
request written` on a terminal one.

Refs: iss-2609190337545165
Refs: iss-2609190337598356
Refs: iss-2608300927241768

Assisted-by: Claude:claude-opus-5-5
…d the shape

The request listed the acceptance criteria as "numbered ac-1..ac-K" without
printing K, and never named the scope-condition identities the verdict must
dispose: they reached the auditor only as HTML comments in the record, and a
miscount quarantined a verdict. The request now prints K (the bullet count the
ingest judges against) and carries a Scope Conditions block listing every
condition under its cond- identity with its text, read through ParseClaims, the
reader the ingest's coverage check uses; a conditionless intent is told its
list is empty, and an unstamped condition is listed as one.

It also carried no verdict shape, which lived only in the bundled agent
definition, so a reviewer working from the request learned it from refusals. A
Verdict shape section now states it, rendered by reflection from the verdict
struct the ingest decodes with DisallowUnknownFields: the schema itself, never
a second copy, so a field added to the struct moves the shape and the
prompt_hash with it. The ingest's --verdict-json help names the section. The
dry-run validator the second record's addendum offers as an alternative remedy
is not added: the record names either remedy as sufficient.

Both blocks sit in the hashed prompt body, which stays a pure function of the
receipt and the record, so the ingest recomputes prompt_hash as before. The
auditor definition (0.4.1) names both blocks. The record's third addendum, the
delivered range the host still supplies, is captured apart because it needs an
attestation design call.

Refs: iss-2609181121301638
Refs: iss-2609181121305984
Refs: iss-2609262011091645

Assisted-by: Claude:claude-opus-5-5
The Role 2 sibling of the fidelity request's missing verdict shape: the
consistency request stated the classes and the rubric, while the findings shape
lived only in the agent definition. It now carries a Findings shape section
rendered from the payload struct the consistency ingest decodes, through the
same reflective renderer the fidelity request uses; a finding shows both of its
ends because the ingest requires exactly two. The section sits in the hashed
prompt body, which stays a pure function of the scope and the corpus.

Refs: iss-2609262011046013

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

Resolves: iss-2609190337545165
Assisted-by: Claude:claude-opus-5-5
…ting inside the ledger

A capture transition's repoint waited up to five seconds for the intent
store's lock inside the ledger lock, whose own budget is also five
seconds, so a long intent hold failed a third process's ledger writer
with allocator contention; migrate --apply nested the same way. And an
intent verb's repoint rewrote linking ledger records under the intent
lock alone, erasing a ledger writer that landed between its read and
its write.

intent.WithLedgerThenMintLock is now the one pair acquisition: ledger
first, then the intent lock asked for only briefly, and on contention
the ledger lock is let go and rested before the next attempt, until the
intent lock's budget; fn runs once, with both held. The transition
repoints in a fresh pair after its move's hold is released, as the
intent verbs do, and migrate --apply runs under the pair. The capture
package registers its ledger lock with the intent package from init
(intent cannot import it), and every intent verb's repoint takes it
through the pair. A tree with no ledger takes no ledger lock and grows
none; a ledger with no lock registered refuses the repoint, reported as
the verb's relink error, rather than rewrite it unlocked.

Refs: iss-2609262218059995
Refs: iss-2609262143209970

Assisted-by: Claude:claude-opus-5-5
…dges the plan again

Embark classified every target path, then wrote the set with no lock, so
an intent or an issue created at a planned target between the two was
replaced without a conflict. The write now runs under the target's
ledger lock (only when it creates an issue record, since taking it
plants a ledger) and then its intent store's lock, through
intent.WithLedgerThenMintLock, and every planned write is classified
again under them: a record that landed in the window is a conflict and
refuses the whole write, as any other conflict does. The command page
and the embark brief chapter state it.

Refs: iss-2609262143265180
Refs: iss-2609262218306589

Assisted-by: Claude:claude-opus-5-5
…n crashed attempt

The reading ingest's prose-citation refusal is returned unrecorded, so
the run stays parked for its re-worded ingest, but it skipped the
rollback refuse() performs on every other refusal past the identity
point: records an earlier, interrupted attempt at the same run id left
in the ledger stood until some later commit path swept them, although
a refused run leaves no reading records. It now calls rollbackThisRun
as refuse() does; the rollback writes no outcome, so the re-worded run
is still admitted. The comments that said every refusal from the
identity point on is recorded name the exception, the reading chapter
states the rollback, and the decision log records the departure.

Refs: iss-2609261835118276

Assisted-by: Claude:claude-opus-5-5
…dger to other writers

Resolves: iss-2609262218059995
Assisted-by: Claude:claude-opus-5-5
…the ledger lock

Resolves: iss-2609262143209970
Refs: iss-2609262218309668
Assisted-by: Claude:claude-opus-5-5
… intent lock

Resolves: iss-2609262143265180
Assisted-by: Claude:claude-opus-5-5
…ledger lock

Resolves: iss-2609262218306589
Assisted-by: Claude:claude-opus-5-5
Refs: iss-2609262257227538
Assisted-by: Claude:claude-opus-5-5
…derives from it

acquireFlock doubled its backoff after the sleep while it was under 100ms, so
80ms became 160ms and the waiter's real poll ceiling was 160ms, while the
pair acquisition's ledger rest, written as 150ms, claimed to outlast a 100ms
ceiling. A ledger writer sleeping 160ms could miss the whole window the pair
left the ledger free.

The ceiling is now one exported constant, fsutil.LockPollCeiling (100ms), and
the poll is doubled and held at it, never past it. The pair's rest is
2 x LockPollCeiling, derived rather than restated. Two tests pin the
guarantee: the poll never sleeps past its ceiling and reaches it, and the rest
outlasts the ceiling by at least half again.

Refs: iss-2609262257227538
Assisted-by: Claude:claude-opus-5-5
…ck poll's ceiling

Resolves: iss-2609262257227538
Assisted-by: Claude:claude-opus-5-5
… of three

A link repoint rewrote spec records with no spec lock, and no spec writer
took one: the spec store's only lock was the mint's. spec close renamed a
spec open/ -> closed/ while a repoint that had read it at open/ wrote it back
there, leaving one record in both status folders, and a spec edit landing
between a repoint's read and its write was erased.

The mint's flock on specs/ becomes the spec store's one lock, and every spec
writer takes it: the mint (Create and its siblings), Close (the load and the
rename are one critical section under it), and a new spec.Discard, which the
refused plan and bundle plan use to take back the spec they minted instead of
a bare os.Remove. Writers outside the package — every link repoint (intent's
repointUnderLock, capture's repointMovedIssue), a lifeboat embark and
capture's migrate apply — get it through intent.WithLedgerThenMintLock, which
becomes the three-lock acquisition: ledger, then intent, then spec. It keeps
its retry rule for the third lock: each inner lock is asked for within
pairIntentTry, and on contention for either every held lock is released for
the rest before the next attempt, so no earlier lock is held while a later
one is waited on. The name is kept, not renamed, to leave its five call
sites untouched.

Lock order, stated beside the spec lock, the acquisition and the intent
lock: ledger -> intent -> spec. The spec lock is innermost; plan's mint nests
it inside the intent lock, close, discard and a remainder mint take it
alone, and the spec package imports neither earlier lock's package. None is
reentrant; no chain takes the spec lock twice (the acquisition's callbacks
run relink or embark writes only, and plan's mint and discard run in
sequence).

Tests, each watched red on a scratch copy of the base with the new API
exported but no writer taking it: a repoint racing spec close leaves the spec
in closed/ alone (intent and capture sides); a spec edit racing a repoint is
kept; the three locks are taken ledger -> intent -> spec with a repoint
holding all three; close and discard wait for the lock; the acquisition
leaves the ledger and intent locks free while it waits for the spec lock.

Refs: iss-2609262218309668
Assisted-by: Claude:claude-opus-5-5
…c store's lock

Resolves: iss-2609262218309668
Assisted-by: Claude:claude-opus-5-5
…er the queue cap

Assisted-by: Claude:claude-opus-5-5
The 9 MiB history capture starts no goroutine of ours, but the identity
probe's git calls do start os/exec's pipe-copy goroutines, so "one
goroutine" was not literal; the comment, the skip message and the
resolution text say "no goroutine of ours". The prose prefilter's
blanking replaces bytes with spaces rather than removing them; the
argument holds because a space can spell no word, and the comment says
so. Wording only, from the ciFast review.

Refs: iss-2609261924541555
Assisted-by: Claude:claude-opus-5-5
…; fix/drain-spec-lock)

Assisted-by: Claude:claude-opus-5-5
Measured on a clean no-local clone of 4b715ad with a dry-run assemble
per position; each window is the smallest ten-thousand boundary with at
least one per cent headroom (ceil(tokens * 1.01 / 10000) * 10000):

  widening    1,301,580 tokens -> 1,320,000 (was 1,280,000)
  entailment    376,527 tokens ->   390,000 (was 380,000; 0.9% headroom)
  detection   1,310,616 tokens -> 1,330,000 (was 1,290,000)

The comparative entry is not a tree measurement and is unchanged.

Refs: iss-2609251455354719
Assisted-by: Claude:claude-opus-5-5
spec.Close (and spec.Discard) on a tree with no spec store plant an
empty store before failing, against the rule the store states beside
WithStoreLock. Found by the drainSpec review.

Refs: iss-2609262342345159
Assisted-by: Claude:claude-opus-5-5
Close and Discard took the store lock through withStoreLock, whose
ensureDir creates .abcd/development/specs/ in order to lock it, so on a
tree with no spec store Close planted an empty store before refusing the
id as not found, and Discard planted one to remove nothing. That is the
very thing the store's own rule beside WithStoreLock forbids. Both now
check for the store first, through the same storeExists reading
WithStoreLockWithin uses: Close refuses the id as not found and Discard
has nothing to remove, and neither creates the store. Unlike
WithStoreLockWithin they do not run their body unlocked, because with no
store there is nothing for it to do.

TestAWriterOnATreeWithNoSpecStorePlantsNone was watched RED on a scratch
copy of the previous tip (both subtests: the store was planted) and is
GREEN here.

Refs: iss-2609262342345159
Assisted-by: Claude:claude-opus-5-5
… store

Resolves: iss-2609262342345159
Assisted-by: Claude:claude-opus-5-5
Brings integ6 (#730, ciFast already merged here, so it arrives as
already-merged), integ7 (#731), integ8 (#732) and integ9 (#733) onto
integ/land-10.

Conflicts, by hunk:
- internal/core/capture/workflow.go: an import-block collision only;
  both the chain's intent import and main's issuerecord import are kept.
- .abcd/config/reading-presets.json: the window hunks take this branch's
  figures for now; they are re-measured at the merged tip in a follow-up.

Auto-merged: DECISIONS.md, commands.md, cli.go, alloc.go, reading.go,
create.go, ingest.go, 05-intent.md, 23-reading.md and the command pages.
commands.md and surface.json regenerate with no drift.

docs-lint extra_roots still names the root CONTRIBUTING.md and
SECURITY.md: both are at the repository root on main, so they stay.

Assisted-by: Claude:claude-opus-5-5
Close and Discard checked that the spec store existed and then took its
lock, and the lock creates an absent store. A store removed between the
two was re-planted empty: Close then failed "not found" over a fresh
.abcd/development/specs/, and Discard returned nil over one.

withStoreLock now takes a mode. The mint and the three-lock path keep
createStore; Close and Discard take storeMustExist, in which the open
that takes the lock is the existence check and an absent store is
refused with errStoreAbsent before anything is created. Close maps it to
"not found" and Discard to success, as before, and the separate
pre-check is gone. beforeStoreLock is a test seam that runs as the lock
is entered.

TestAStoreRemovedBeforeTheLockIsNotReplanted removes the store through
that seam. Both subtests were red on a git-archive copy carrying the
seam without the mode ("re-planted the removed spec store") and are
green here.

Refs: iss-2609262342345159
Assisted-by: Claude:claude-opus-5-5
Measured on a clean `git clone --no-local` of b727b96 with a dry-run
assemble per position; window = ceil(tokens * 1.01 / 10000) * 10000.

- widening 1,322,032 tokens / 5,089,824 bytes: 1,320,000 -> 1,340,000
- entailment 382,812 / 1,473,830: 390,000 kept (1.88% headroom)
- detection 1,331,068 / 5,124,612: 1,330,000 -> 1,350,000

comparative is unchanged: it has no tree measurement.

Refs: iss-2609251455354719
Assisted-by: Claude:claude-opus-5-5
Assisted-by: Claude:claude-opus-5-5
@REPPL
REPPL enabled auto-merge September 28, 2026 17:23
@REPPL
REPPL added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit ceb4b6d Sep 28, 2026
13 checks passed
@REPPL
REPPL deleted the integ/land-10 branch September 28, 2026 18:22
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