Skip to content

feat: setup explains missing tools, new captures are matched against the record, and two record-hygiene batches - #731

Merged
REPPL merged 67 commits into
mainfrom
integ/land-7
Sep 28, 2026
Merged

REPPL merged 67 commits into
mainfrom
integ/land-7

Conversation

@REPPL

@REPPL REPPL commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator

Four reviewed lanes land together on one integration branch, with one recalibration of the cold-reading windows at the merged tip.

Record hygiene, first batch of majors. A grounds entry that the site renderer would refuse is refused when it is written. The refusal names the construct and says to put markup in a closed code span or reword it. Reference links and images are refused outright. A reading's raw heading scan runs in linear time, and a document whose heading titles would take more reading than their size allows is refused by name. The release-gate manifest now claims what it pins: the inputs and the scope. A run's failing count is described as one run's observation. The draft intents now qualify every predecessor spec id they cite, and itd-44's "delivered" claim is withdrawn.

Setup explains a missing tool before installing it (itd-63). abcd ahoy explains each missing tool from one registry: what the tool is for, what the install does, and what it sends over the network. It installs the tool only on an explicit yes, either typed at a terminal or relayed as --install-tool <name>. It never installs on --yes, on a piped answer, or under CI. A no keeps the native default and says so. A missing gitleaks or gh is explained in the refusal itself. An install runs in its own process group with a bounded time, and a step that leaves a detached process holding its output is reported as a failed step.

A new capture or draft is matched against the record before it is written (itd-2609212137116617). abcd capture and abcd intent "<text>" compare the new text with the open and resolved issues and the intents. Where the overlap clears match.threshold (0.6 by default, set through the layered configuration), they write duplicates: or refines: links. --json lists the near misses with their scores. A match that cannot run never refuses a filing: the record is written unlinked, and the verb names the reason. record-lint resolves the new links, and it reports every issue record the ledger reader skips, in the reader's own words. A status directory the match cannot read makes the match unread, never an empty candidate set.

Record reading, second batch. Every frontmatter field reader handles a trailing comment and a null the same way. record-lint names a bullet under ## Grounds that the reader drops. A wontfix over a body that leaves a comment or fence open is refused, naming the construct, the line and the way out (close or remove the opener, then re-run), as the 2026-08-31 ruling holds; iss-2608301908270888 is declined on that ruling.

Integration. The release-gate derivation reads CHANGELOG.md out of git within the same 4 MiB cap as every other CHANGELOG read, and lint's read guard now also flags an unbounded file-content read out of git. The record-reading lane's ruling alignment is merged in, and the windows are recalibrated at this tip.

Re-merge. Main (#728, #729, #730) is merged in by hunk, six conflicts, with the command reference regenerating with no drift. One mechanism now handles a ledger status directory that exists and cannot be read: it is a fault naming the directory, exit 1, on the board, the list, the transitions and the mint, and the filing-time match reports it as an unread record set; the match lane's skipped-roster form contradicted that on the same input and is folded into it, with its record's resolution note, the capture page and the capture chapter saying so. The match lane's intent heading pattern is renamed beside the consistency pass's. Setup's scanner-hint summary entry is removed, since the explain-then-install step records no such write and the entry's "abcd never runs an installer for you" is no longer true. Every ledger-verb input refusal still exits 2: a grounds text the site renderer refuses and a wontfix over a locked body are pinned there by a test. The windows are recalibrated at the re-merged tip: widening 1,277,632 tokens (window 1,300,000), entailment 379,456 (390,000 kept), detection 1,286,668 (1,300,000).

Reviews:

  • Record hygiene, first batch: review SHIP (one nitpick, the "Quote" capitalisation the renderer's move introduced; fixed on this branch).
  • Setup explains installs: review FIX FIRST (two medium, two low), all fixed on the lane; second review SHIP.
  • Match before write: review FIX FIRST, three follow-ups fixed on the lane; second review SHIP. Its minor (capture's test-only parser wrappers) is fixed on this branch. Its record_schema leg for skipped records is folded into main's reader-parity leg here, so one mechanism reports each skipped record.
  • Record reading, second batch: reviewed SHIP with one product-thinker flag, answered on the lane (the wontfix refusal restored) and merged here.

Delivers: itd-63
Delivers: itd-2609212137116617
Resolves: iss-2608301646046226
Resolves: iss-2608301421382564
Resolves: iss-2608231409595789
Resolves: iss-239
Refs: iss-2608301646046226
Refs: iss-2608301421382564
Refs: iss-2608231409595789
Refs: iss-239
Refs: iss-2609261536147903
Resolves: iss-2609261447331434
Resolves: iss-2609261604485703
Resolves: iss-2609261604498137
Resolves: iss-2609261604492132
Resolves: iss-2609261604499090
Refs: iss-2609261447331434
Refs: iss-2609261447395216
Refs: iss-2609261604485703
Refs: iss-2609261604498137
Refs: iss-2609261604492132
Refs: iss-2609261604499090
Resolves: iss-2609261631120364
Resolves: iss-2609261631134401
Resolves: iss-2609261631132673
Refs: iss-2609261631120364
Refs: iss-2609261631134401
Refs: iss-2609261631132673
Resolves: iss-2608241347321759
Resolves: iss-2608301747001641
Refs: iss-2608241347321759
Refs: iss-2608301747001641
Refs: iss-2609012039210402
Refs: iss-2608301908270888
Refs: iss-2608310912217521
Resolves: iss-2609261726015043
Refs: iss-2609261726015043
Refs: iss-2609251455354719
Refs: iss-2609261241121312
Refs: iss-2609260552251398

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

ahoy offers trufflehog as an optional dependency and asks a scan.deep
question, but nothing in abcd runs trufflehog or reads scan.deep. Both
were confirmed while finding the tool checks itd-63 reroutes.

Refs: iss-2609261447331434
Refs: iss-2609261447395216

Assisted-by: Claude:claude-opus-5-5
internal/core/tools is the mode itd-63 names: a curated registry of the
tools abcd knows (gitleaks, gh), Explain(name, capability) for the
plain-language explanation a verb gives when one is missing, and
Install, which runs the registry's fixed argv only after the caller's
Confirm returns yes and reports the verify result.

Install is a trust boundary: an unknown tool is refused, CI never
installs and is not asked, a nil confirmation is a no, the program must
resolve on PATH outside the repository the verb ran from, and the step
runs without a shell, with stdin closed, in its own process group killed
through its handle on timeout. A no or a failure ends with the
capability's standing ("continuing on the native secret scanner").

A tool the registry does not know gets a generic explanation, no step,
and the capture that records the gap in abcd's own ledger.

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

internal/core/record/match scores how much of a new text another record
already holds, weighted by how rare each shared term is across the
candidates, and proposes `duplicates` (high both ways) or `refines` (high
one way: the new text is the narrower) above a threshold, listing the best
below it as near misses with their scores. It declares itself a lexical
heuristic on every outcome, never proposes a reversal or a supersession
(the itd-84 discipline keeps those advisory and human), and files a text
below a declared minimum of terms without comparing it.

The spec (spc-2609212141417782) names the embark ranking's overlap function
as the one to move here. That ranking is unbuilt (scope 7 of the reflect
spec, still open), so there is no copy to move: this is the primitive that
ranking calls when it lands. It is a subpackage of record rather than
record itself because record imports capture and intent, which both call
it; a leaf is what avoids the cycle.

The 0.6 default was read off this repository's ledger: each of the 449
open issues matched against the other 1,660 issues and intents, 14 cleared
it, most of them a real double or a narrower follow-up.

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

LoadConfig resolves the two keys through internal/core/layered, the one
layered configuration reader (.abcd/config.json, then ~/.abcd/config.json,
then the bundled default), claiming the `match` namespace so a misspelt
key is refused, and refusing a threshold outside (0, 1] or a field outside
the closed set (issue.body, intent.title, intent.press_release) naming the
file it came from. The reader already named match.threshold as a consumer
it was built for; this is the first reader of the shared config family.

Assisted-by: Claude:claude-opus-5-5
The two refusals abcd gives on main for a missing tool now carry the
tool registry's explanation through tools.MissingError, which keeps the
cause for errors.Is and appends what the tool is, whether the capability
requires it, what works without it, the exact install step and what that
install does.

- history: a repository that armed gitleaks and has no binary still
  refuses to store a transcript (fail-closed), and the refusal names the
  way back to the native scanner (enabled: false). A binary that exists
  but is refused is not a missing tool and gets no install step.
- ahoy remote and site setup: a missing gh is explained under the
  capability GitHubSettings, including that the install does not sign
  anyone in.

This is itd-63's missing-scanner path: the pluggable safety gate the
intent names as the first consumer (itd-62) is a draft, and its
always-block-on-a-missing-scanner path does not exist on main; the
gitleaks opt-in in the history store is the one fail-closed
missing-scanner path there is.

Assisted-by: Claude:claude-opus-5-5
ahoy's dependency gap and its install step now run through the
explain-then-install mode (itd-63) instead of printing a bare
"brew install gitleaks".

- detect: the gitleaks gap carries the registry explanation (Gap.Tool,
  in --json) and is required where the repository armed gitleaks in
  .abcd/config/gitleaks.json, optional over the native scanner
  otherwise.
- install: the category approval reaches the step; each missing tool is
  then put to the front door's confirmation. A yes runs the registry's
  step and reports it as a change with its verify result; a no, a
  failure or an unasked caller is a note carrying the explanation and
  "continuing on the native secret scanner".
- CLI: the confirmation asks only at a terminal (default no), or takes
  --install-tool <name>, the answer a host's question tool relays; a
  piped answer, --yes and CI never install. --install-tool refuses a
  name ahoy install does not check for, naming the ones it does.
- The trufflehog gap is gone: nothing in abcd runs trufflehog, so the
  mode would have installed a program that does nothing for the person.

The command page, the ahoy brief chapter, the sentence, the generated
surface, the package map and ACKNOWLEDGEMENTS (Homebrew) move with it.

Refs: iss-2609261447331434

Assisted-by: Claude:claude-opus-5-5
`ahoy install --install-tool <name>` is the answer a host relays from
its own question tool, so it has to reach the install step with stdin
closed. It now approves the dependency category without asking its
question (ApproveDependency); every other category is asked or
pre-answered as before. End-to-end tests drive both halves through the
CLI with no package manager on PATH, so no test can ever install.

Assisted-by: Claude:claude-opus-5-5
The named case answered through a terminal that also typed y, so a
confirmation that ignored --install-tool still passed. It now answers
off a terminal with empty input, where only the flag can be the yes;
watched failing with the named branch disabled on a scratch copy.

Assisted-by: Claude:claude-opus-5-5
Resolves: iss-2609261447331434
Assisted-by: Claude:claude-opus-5-5
… before it is written

`capture` and the quoted-text `intent` create now take a match request:
under the writer's existing lock (the ledger lock; the intent mint lock),
the new text is compared with every open and resolved issue's body and
every intent's title and press release, as far as match.fields names
them, through the canonical overlap primitive. Each candidate above the
threshold is written onto the new record as `duplicates:` or `refines:`
(an inline id list, at most three links), and the result carries the
outcome: the matches, the near misses below the threshold with their
scores, or why nothing was compared (a text below the declared minimum of
terms, or a candidate set that could not be read).

The match never refuses and never drops a write: a failure to gather the
candidates, or a link the schema would refuse, files the record unlinked
and says so on the outcome. Removing a link leaves an ordinary record.

The two link keys are KNOWN issue properties (issueschema), id lists
validated like every other, read back onto Issue, and record_schema
resolves their targets like any other cross-reference. The intent create
takes its candidates through a Matcher the ledger fills, because the
ledger reads intents and the intent store does not read the ledger.
Callers with their own matching (the inbox drain, promote) pass none.

Assisted-by: Claude:claude-opus-5-5
The explain-then-install mode, its registry and its callers are on the
branch. The close note records the two places the tree differs from the
record the spec was written from: the itd-62 safety gate is a draft, so
the history store's armed-gitleaks refusal is the missing-scanner path
routed; and the guard and the launch have no tool checks to reroute.

Delivers: itd-63
Assisted-by: Claude:claude-opus-5-5
Assisted-by: Claude:claude-opus-5-5
Both verbs resolve match.threshold and match.fields through the layered
reader and hand them to core, which runs the match under the writer's
lock. The text render prints each link written ("matched iss-N —
duplicates (score …): link written; confirm it by leaving the link, or
remove the line"), a match past the link cap as listed-not-linked, the
count of near misses, or why nothing was compared; --json carries the
whole outcome, near misses and their scores included.

A configuration the reader refuses (a misspelt key, a threshold out of
range) never refuses the filing: the record is filed unlinked and the
outcome names the refused key, because a finding lost to a config typo
is the failure the match exists to prevent. No new flag.

Assisted-by: Claude:claude-opus-5-5
…he itd-84 rung

The capture and intent command pages, their brief chapters and the
configuration chapter describe what ships: the match against every open
and resolved issue and every intent, the duplicates/refines links, the
near misses in --json, the never-refuse rule, and the match.threshold /
match.fields keys the layered reader resolves from .abcd/config.json and
~/.abcd/config.json. The process explanation tells a reader what capture
now decides for them.

itd-84 marks its capture-time candidate pass delivered by
itd-2609212137116617 (criterion 5), naming what stays undelivered; a test
holds the mark. The reflect spec's embark ranking points at the canonical
primitive rather than a score of its own in the lifeboat package.

Assisted-by: Claude:claude-opus-5-5
…me match

The match weights each shared term by how rare it is across the records
compared (Spärck Jones, 1972), and runs at filing without refusing, the
advisory shape of Linear's similar-issue detection the itd-84 discipline
already cites.

Assisted-by: Claude:claude-opus-5-5
A new capture or quoted-text draft is matched against the record before
it is written: the canonical term-overlap primitive, the typed
duplicates/refines links, the never-refuse rule, match.threshold and
match.fields through the layered reader, near misses in --json, and the
itd-84 capture-time candidate rung marked delivered (the close repointed
its link to shipped/). Impact additive, as the intent declares.

Delivers: itd-2609212137116617
Assisted-by: Claude:claude-opus-5-5
…livery claim

The draft intents cited specs of the retired predecessor store as though they
were live, and since the live ordinals now run to spc-70 every one of those
ids resolves to an unrelated live spec: itd-44 (spc-48, spc-56, spc-12),
itd-51 (spc-33, spc-34, spc-37), itd-57 (spc-48, spc-58, spc-60, spc-62),
itd-30 (spc-3), itd-55 and itd-62 (spc-12) and itd-59 (spc-15). Each citation
now carries the "(predecessor store)" qualifier the specs charter sets.

The record claimed a delivery the tree cannot back. intents/README.md said the
capture-time classifier has a fourth `decision` verdict, itd-34 said the case
"landed as itd-44", and itd-44 said it "is delivered under" a thin adoption
owned by spc-56. No capture-time classifier verdict exists in the binary and
itd-44 is a draft with no live spec; all three now say so, matching the
brief's own intent chapter, and itd-44 carries a dated delivery-state note.

The specs charter's namespace section put the live ceiling at spc-42 and
called spc-43 to spc-83 predecessor-only, which stopped being true when the
live store minted up to spc-70; it now names the true ceiling, the colliding
ids, and says the mint's ordinal range is spc-2 to spc-70. spc-75 in itd-61
had already been removed; itd-70 and itd-69 cite it, and it lies above the
ceiling with its baseline entry.

Refs: iss-239

Assisted-by: Claude:claude-opus-5-5
…r spec ids unqualified

The iss-239 sweep qualified the draft corpus; planned intents and disciplines
carry the same class and need a per-site reading, so it is filed rather than
folded in.

Refs: iss-2609261536147903, iss-239

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

Resolves: iss-239
Refs: iss-2609261536147903
Assisted-by: Claude:claude-opus-5-5
…rable findings

manifest.json called itself the reproducibility anchor and said two honest runs
of the same tier "mean the same thing". It pins the inputs — doc list,
directions, checker count, prompt — and that holds; it cannot pin the findings,
because the checkers are LLM agents. Three full-tier runs against one manifest
returned 125, 126 and 147 findings, and the two whose brief was byte-identical
differed by 21.

Every site that repeated the claim now states what the manifest fixes (the
scope two runs of one tier examine) and what it does not (the findings, their
count and their classes): the manifest's own comment, the release-gate README,
the detector's header, the currency test's doc comment and the phase-8 goal.
The README gains the runbook sentence the record asked for: a receipt's
`failing` count is an observation from one run, not a metric to compare across
releases or to scope an intent by.

A record correction, not a mechanism change: the gate's refusals are unchanged.
The manifest's hash changes with its comment, and no committed receipt echoes
the old hash, so no receipt is invalidated.

Refs: iss-2608231409595789

Assisted-by: Claude:claude-opus-5-5
…ts, not reproducible findings

Resolves: iss-2608231409595789
Assisted-by: Claude:claude-opus-5-5
A raw heading opener with no hard bound had its title read over the whole
remainder, and every opener walked the remainder for its bound, so a run of
`<p role="heading">x` with no close took 13.5 s at 4 000 openers and 57 s at
8 000 — hours at the 4 MiB size cap. The linearity test covered only openers
that close at once, so its name over-claimed.

Three changes, each watched failing first:

- The bounds are indexed once per reading and each opener finds its own by
  binary search. The index lists exactly the matches the walk visited, since
  no bound match straddles the `>` ending an opener; the one exception, a
  masked reading that blanks that `>`, is handed back to the walk. Element
  names are keyed by case-fold class, the equivalence strings.EqualFold
  decides. A differential test holds the index to the walk.
- Rendering the titles is charged to a read budget of eight times the
  document plus 1 MiB. Openers sharing one far bound are each bounded, so
  nothing refused them, and reading their overlapping titles is quadratic
  however the bound is found; such a document is refused by name. A cap that
  truncated a long title would let padding walk an excluded heading past it,
  and the committed corpus reads under one times its length in titles.
- Line numbers are counted incrementally. Counting from the top for every
  opener took 11 s on closed openers at the size cap.

The unbounded-opener refusal is asked before the budget one, so every
refusal message a document drew before is the one it draws now. The readings
charter names the new refusal.

Refs: iss-2608301421382564

Assisted-by: Claude:claude-opus-5-5
Resolves: iss-2608301421382564
Assisted-by: Claude:claude-opus-5-5
The unbounded wait behind a re-grouped descendant, the unsanitised
changed: line, the untrue install Effects text, and the second CI
detector, each filed before its fix.

Refs: iss-2609261604485703
Refs: iss-2609261604498137
Refs: iss-2609261604492132
Refs: iss-2609261604499090
Assisted-by: Claude:claude-opus-5-5
Grounds text that cleared the substance floor could still be refused by
`abcd site build`: an unclosed code span, a remote image, a raw <div>, an
undefined reference link, a footnote-style reference. The entry is append-only
and no verb removes it, so one unbalanced backtick in a sentence naming a flag
landed the record and turned site-render — a preflight gate, a CI step and a
release gate — red until somebody hand-edited a committed record.

grounds.New and grounds.NewDerived now ask the site's renderer itself whether
the bullet they are about to hand a writer renders, and refuse it with nothing
written when it does not. There is no list of the renderer's rules here to
drift from it: the predicate, mdrender.RefusalIn, runs the renderer over the
block. It answers strictly where a page would supply context — every
reference link and every image is refused — so it is never looser than the
page the entry lands on. The derived path is included because a wontfix's
`declined:` entry is as append-only as a supplied one.

The renderer moves out of core/site into a new leaf, core/mdrender, because
core/site imports the record families that record grounds and they could not
import it back. core/site keeps its Renderer (it carries the site's interface
strings) and delegates, and keeps its names for the helpers its pages use, so
no caller outside the two packages changes. Two UnsupportedError literals in
core/site are keyed, as vet requires of an imported struct.

The 645 grounds entries committed in the tree all pass the predicate, so no
existing entry is stranded. The condition-disposition writer was checked as a
sibling and is not one: its cleaner already defuses every construct above.

The capture and intent command pages, the capture brief chapter and the
package map say so.

Refs: iss-2608301646046226

Assisted-by: Claude:claude-opus-5-5
Resolves: iss-2608301646046226
Assisted-by: Claude:claude-opus-5-5
runArgv killed the step's process group on timeout and then waited for
cmd.Wait, which waits for the output copier. A descendant that moves
into another group or session (setsid, setpgid) survives the group kill,
and while it holds the stdout/stderr pipe that wait never returned, so
the 15-minute bound was not a bound. cmd.WaitDelay now ends the wait for
output 10 seconds after the step exits or is killed, and a step that
exits cleanly while leaving such a process behind is reported as failed,
saying so.

The comment no longer claims the kill reaches everything the step
started; it names the re-group limit, as do the ahoy brief chapter and
the command page. TestRunArgvBoundHoldsAgainstAReGroupedDescendant runs
the test binary itself as the step (no installer, no shell), forking a
Setpgid holder that keeps the pipe; with the WaitDelay line removed on a
scratch copy both subtests hung past the 8s watchdog.

Refs: iss-2609261604485703
Assisted-by: Claude:claude-opus-5-5
… a re-grouped descendant

Resolves: iss-2609261604485703
Assisted-by: Claude:claude-opus-5-5
… the result

itd-63 put external output on the ahoy changed: line: the verify
program's first line (and a failed step's output tail) entered
Result.Output raw, flowed through Summary() into the ahoy result's
changes, and the CLI prints changes without termsafe. The one place a
program's output becomes result text is now outputLines, which trims and
sanitises each line; tail and firstLine both read through it. Sanitising
there rather than at one print covers every render of the result: the
changed: and note: lines and --json, which escapes no C1 control or bidi
override.

Swept the other prints of a tools result: Missing (history, ahoy
remote), Explain and Lines (the ahoy gap, the terminal question) carry
registry text and fixed causes only, and the note: print is already
sanitised. TestInstallSanitisesTheProgramsOutput failed on all three
paths (verified, failed step, failed verify) before the change.

Refs: iss-2609261604498137
Assisted-by: Claude:claude-opus-5-5
… before it enters the result

Resolves: iss-2609261604498137
Assisted-by: Claude:claude-opus-5-5
The lapsed v0.10.0 deferral fields go with the resolution: the record's
question (trim the comment, or document the form unsupported) is
answered by the scanner already trimming it and the two readers that
did not now doing so.

Resolves: iss-2608241347321759
Assisted-by: Claude:claude-opus-5-5
A malformed `grounds:` frontmatter value was a value the schema gate
could judge; a malformed bullet under `## Grounds` reads as prose, so
the reader drops it and no gate says so. core/grounds now exposes
MalformedIn, the other half of the one walk ParseSection takes (the
bullets that parse, and the ones that do not), with each bullet's file
line; record_schema reports every dropped bullet on an issue or intent
record at the rule's severity. The grammar alone is judged, never the
substance floor, which the ledger's reader does not apply either; a
prose paragraph under the heading is not a bullet and stays silent.

This is a gate finding while the 2026-09-09 ruling keeps grounds
refusals parked, and it does not reopen that ruling: the parked
refusals are the ones that stop a person in the triage flow for a
sentence the gate cannot judge, and this one stops no verb (the verbs
never write a malformed bullet) and judges a grammar a machine can.

The tree carried one such bullet, a hand-written `- rejected:` on
iss-2609012039210402, which is the record's case exactly; it becomes
`- declined:`, the vocabulary's word for an alternative not taken. The
capture brief chapter states the check.

Refs: iss-2608301747001641, iss-2609012039210402
Assisted-by: Claude:claude-opus-5-5
The lapsed v0.10.0 deferral fields go with the resolution.

Resolves: iss-2608301747001641
Assisted-by: Claude:claude-opus-5-5
…flicts

Four majors resolved on the lane: grounds text the site renderer refuses
is refused at write (the renderer moves to internal/core/mdrender, with
core/site's markdown.go a delegating shim), the raw heading scan is
linear, the release-gate manifest claims pinned inputs, and the drafts
qualify their predecessor spec ids.

Semantic conflict fixed here: the lane rewords the release-gate
manifest's _comment, so the manifest's sha256 moves to 38770ed8..., and
main's TestReceiptExampleManifestHashIsTheCommittedManifests (landed
with integ2 after the lane's base) pins receipt.example.json to the
committed manifest's hash. The example now carries the new hash.

Assisted-by: Claude:claude-opus-5-5
… into "Quote"

The extraction of the site renderer into internal/core/mdrender renamed
the quote helper, and the rename caught three doc comments where
"quote" is prose (a quote inside a quote; no quote to break out of).
The review of the drainM1 lane named it as a nitpick.

Assisted-by: Claude:claude-opus-5-5
Ships itd-63 (spc-2609211955339422 closed): ahoy explains each missing
tool from one registry (internal/core/tools) and installs it only on an
explicit yes; a missing gitleaks or gh is explained in the refusal
itself. The CI detector moves to the leaf internal/cienv. Regenerating
commands.md and surface.json produced no drift.

Assisted-by: Claude:claude-opus-5-5
An unclosed `<!--` or fence in an issue body masks every line below it,
so no appended grounds entry can read back. Resolve and promote record
grounds only when given them, so without --grounds they already acted;
wontfix derives a `declined:` entry from its reason and so refused on
every attempt, leaving a hand edit as the only way to decline the
record.

The derived entry is a copy of the reason the record carries in
wontfix_reason whether or not the entry lands. So a wontfix with no
--grounds now moves the record without it and returns
GroundsNotWritten, which the CLI prints as a stderr warning naming the
construct, its body line and the opener's text. The append's unclosed
refusal becomes a typed grounds.UnclosedBodyError (same message) so the
ledger tells this cause from the others without matching text. Grounds
the operator supplies are still refused on every route, naming the same
construct and line: the guard the 2026-08-30 entry calls correct stays
correct, and nothing invisible is ever written.

Not done, on the 2026-08-31 ruling: validating bodies at capture time,
and a repair verb. The brief chapter and the command page state the
exception.

Refs: iss-2608301908270888
Assisted-by: Claude:claude-opus-5-5
The lapsed v0.10.0 deferral fields go with the resolution.

Resolves: iss-2608301908270888
Assisted-by: Claude:claude-opus-5-5
The v0.10.0 grant lapsed at the v0.11.0 cut. The record is not
mechanical: one grammar through grounds.New for every --grounds would
change what a person types on released verbs, and the disposition verb
carries its own state vocabulary that the grounds tokens do not map
onto. The reason now names the three choices owed to the product
thinker, and a third grammar found in the sweep (intent condition
--grounds: free text held to the floor, no token).

Refs: iss-2608310912217521
Assisted-by: Claude:claude-opus-5-5
…e conflicts, one semantic

Ships itd-2609212137116617 (spc-2609212141417782 closed): a new capture
or intent draft is matched against the record before it is written, and
duplicates/refines links are proposed. The issue reader's judgement moves
to the leaf internal/core/issuerecord.

Conflicts, by hunk:
- brief 05-internals/03-configuration.md, the machine config.json item:
  main's text (the provider adapter's machine layer, its one write, the
  credential store's once-per-name rule) combined with the lane's claim
  that the match keys are read from it beneath the repo scope.
- internal/core/capture/validate.go: main's ReadRefusal kept, now asking
  issuerecord.Parse/ValidateStrict/ValidateInvariants directly; main's
  acceptedValues copy dropped (the lane's wrapper over
  issuerecord.AcceptedValues is the one left).
- internal/core/lint/schema.go scanRecordStores: main's setext-underline
  leg kept.

Semantic conflict fixed here: main and the lane each gave record_schema a
leg that reports an issue the ledger reader skips (main's reader-parity
leg over the capture.ReadRefusal seam, 35600e9; the lane's
issuerecord.Judge leg, its F4). Both armed, one skipped record would be
reported twice in two phrasings. Main's leg is kept, since the front
doors and their tests pin its seam and it names a defect once; the
lane's leg, its ledgerReader store flag and readerSkipsPhrase are
removed, and its schema_test.go count changes reverted. ReadRefusal asks
issuerecord, so the gate and the reader still reach one verdict from one
derivation. The lane's regression test
(TestRecordSchemaReportsEveryRecordTheLedgerReaderSkips) is kept and
asserts the reader's words through the parity leg; all three of its
cases pass. internal/README.md says how lint reaches the judgement.

Also fixed here: the configuration chapter said the match keys are "the
one block" read through the layered reader; main's provider adapter reads
the oracle keys through it too.

Refs: iss-2609261631132673
Assisted-by: Claude:claude-opus-5-5
… test-only wrappers

The match lane moved the issue reader's parse into internal/core/issuerecord
and left capture's parseFrontmatterBlock and parseScalarOrList as one-line
wrappers with no non-test caller, kept only so the existing tests compiled.
The tests now call issuerecord.ParseBlock and issuerecord.ParseScalarOrList
directly, and the wrappers are gone; two comments that named them follow.
The review of the lane named it as a minor.

Assisted-by: Claude:claude-opus-5-5
…git without a bound

Refs: iss-2609261726015043
Assisted-by: Claude:claude-opus-5-5
…t the read guard see git reads

changelogAt read CHANGELOG.md out of a revision with an unbounded
`git cat-file blob`, so the release-gate derivation held any size of
blob in memory and parsed it, while every other CHANGELOG read in lint
and core/changelog holds to 4 MiB. It now reads through
gitutil.RunCapped at maxChangelogBytes, which refuses a blob past the
cap rather than truncating it; ReleasedVersion fails closed on one.

TestLintReadsNothingUnguarded did not see this read because its
scanner names filesystem opens and reads only. It now also names an
unbounded gitutil.Run asked for a file's content (`cat-file blob`,
`show`); the bounded runners pass, and TestUnguardedReadsSeesEverySpelling
plants both shapes. The sweep found no other unbounded record read in
lint: readBucket's first read, which the match lane's review noted as a
bare os.ReadFile on the lane's base, already goes through
fsutil.ReadGuarded at this tip (b48fd58).

Watched RED on a scratch copy of the tip without this change:
TestLintReadsNothingUnguarded (named releasegate_derive.go:237) and
TestReleasedVersion_RefusesAnOversizedChangelog (read "1.0.0").

Refs: iss-2609261726015043
Assisted-by: Claude:claude-opus-5-5
…ad out of git is bounded

Resolves: iss-2609261726015043
Assisted-by: Claude:claude-opus-5-5
…gration branch: four conflicts

Conflicts, by hunk:
- brief 05-internals/06-lint.md, the record-currency bullet: both
  additions kept (the ledger reader's verdict through issuerecord, and
  main's principle typed claims and inherited scope conditions).
- release-gate/receipt.example.json manifestHash: the merged
  manifest.json carries drainM1's _comment and main's rosters and
  counts, so the example carries its sha256, ffe0d2ab...
- internal/core/capture/capture.go error block: the match lane's
  re-exported ErrPathUnsafe (issuerecord's value) and main's new
  ErrNotCharacterised, both kept.
- internal/core/history/history.go imports: main's sessionkind and
  itd63's tools, both kept.

Regenerating commands.md and surface.json produced no drift. Build, vet
and the overlapping packages' tests pass.

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

Three records fixed on the lane (comments and nulls read one way in every
field reader; record_schema names a grounds bullet the reader drops;
wontfix acts on a record whose body leaves an opener open) and one
re-deferred out loud. The lane's review is not yet done.

Conflict, by hunk, internal/core/lint/schema.go: the schemaRecord struct
and its literal carry both main's content (the reader-parity leg's input)
and the lane's malformedGrounds; in checkRecordSchema the lane's
grounds-bullet leg runs after main's reader-parity and body-render legs,
beside the body leg, since a dropped bullet is a body defect and not a
reason the reader skips the record, so it neither silences nor is
silenced by the parity leg.

Regenerating commands.md and surface.json produced no drift. Build, vet,
the lane's packages' tests, record-lint and lint-issues pass.

Assisted-by: Claude:claude-opus-5-5
Reverts the behaviour cee142d introduced. That commit let a wontfix with
no --grounds move a record whose body leaves an HTML comment or a fence
open, dropping its derived `declined:` entry and warning on stderr. It
went against the product thinker's 2026-08-31 rulings in
.abcd/work/DECISIONS.md: ruling 3 holds that the guard is correct and
accepts the hand edit as the repair, and ruling 1 requires a
terminal-folder record to carry a grounds entry. So the wontfix refuses
again, with or without supplied grounds, and nothing moves.

Removed with the skip: transitionGrounds and its derived flag,
TransitionResult.GroundsNotWritten (JSON grounds_not_written), the CLI's
stderr WARNING, and grounds.UnclosedBodyError. Nothing else used them.
appendGrounds wraps the cause with %v again.

Kept, and improved: the refusal names the construct, its body line and
the opener's text, and now also names the exit: close or remove the
opener in a text editor, then re-run. The brief chapter
(04-surfaces/06-capture.md) and commands/capture.md say the same: a
wontfix always gets a bullet, a locked body is refused on every route
that would append one, and the hand edit is the repair. Resolve and
promote given no grounds append nothing and act; the promote case,
missing from cee142d's test, is covered now.

Tests: TestALockedBodyRefusesWhatWouldWriteGroundsAndNothingElse (both
locks; wontfix derived and supplied refuse and leave the record in
open/; resolve and promote without grounds act) and
TestCaptureWontfixRefusesALockedBody (CLI: refusal names HTML comment,
body line 2, the editor exit; the record stays in open/). Both were
watched red against 1babb04 on a scratch copy.

Refs: iss-2608301908270888
Assisted-by: Claude:claude-opus-5-5
Measured on a clean clone of b27d5aa with a dry-run assembly per
position; window = ceil(tokens * 1.01 / 10000) * 10000, an existing
window kept only with at least 1% headroom:
- widening 1,192,369 tokens / 4,590,624 bytes: 1,210,000 (was
  1,200,000, 0.64% headroom)
- entailment 365,852 / 1,408,533: 370,000 kept (1.13% headroom)
- detection 1,201,405 / 4,625,412: 1,220,000 (was 1,200,000, exceeded)
Comparative is unchanged.

Refs: iss-2609251455354719
Assisted-by: Claude:claude-opus-5-5
The record moves from resolved/ to wontfix/. Its resolution claimed
that no triage verb locks out a record whose body leaves an opener
open, which rested on cee142d's derived-skip path. 5af3f87 removed
that path on the product thinker's 2026-08-31 ruling 3 (the guard is
correct; the hand edit is the accepted repair). The record is declined
on that ruling, with reason and a `declined:` grounds entry.

This supersedes the `Resolves: iss-2608301908270888` trailer of
8b3a7ad. That commit's resolution does not stand: the record is
declined, not fixed. The resolved state never reached main, so no
record sits in two status folders.

How the move was made: `capture wontfix` refuses a record outside
open/ (ErrTransitionConflict). So the record was first put back in
open/ as main holds it, by hand, dropping the lapsed v0.10.0 deferral
fields the resolve had dropped too. Then `go run ./cmd/abcd capture
wontfix` moved it, writing wontfix_reason and the grounds entry. One
hand edit after the verb made the reason exact: main's refusal already
named the construct, body line and opener text, and this branch's net
improvement is the stated exit. The pursued: entry of the superseded
resolve is not carried, because it was never on main.

Refs: iss-2608301908270888
Assisted-by: Claude:claude-opus-5-5
…the integration branch

The merge dropped workflow.go's errors import (fix2-drainR1 removed its own use of it while match F1's unreadable-directory check still needs it); restored in this merge commit.

Assisted-by: Claude:claude-opus-5-5
Six conflicts, all resolved by hunk: the intent chapter's command table
(main's owed-review status row beside the match lane's create row), the
reading presets (main's figures, re-measured after), the release-gate
receipt example (manifestHash reset to the sha256 of the merged
manifest), the capture error block (ErrRequestRefused beside the
issuerecord re-exports), checkOneStatusPerID and ahoy's
stepDependencies. The command reference and surface.json regenerate
with no drift.

Semantic fixes in this commit:
- One mechanism for an unreadable ledger status directory. drain1's
  readStatusDir faults, naming the directory, on every reader (list,
  status, the transitions, the mint); match's scanStatusDir had turned
  the same case into a read-layer skip on list and status. The two
  contradict on one input, so the loud one survives: scanStatusDir now
  lists through readStatusDir, scanLedger and openIDSet return the fault,
  and unreadableDirSkip is gone. The filing-time match still reports an
  unread record set rather than refusing the filing. The match lane's
  tests keep what they protect (the directory is named, never counted as
  empty, the match links nothing) and now assert the fault form; the
  unread-match test reaches the match through the intent create's
  candidate set, because a capture over an unreadable resolved/ is
  refused at the mint first. The match record's resolution note, the
  capture page and the capture chapter say so.
- The match lane's intent/match.go declared h1Re beside itd-48's in
  consistency.go with a different pattern; it is renamed titleH1Re.
- ahoy: itd-63's explain-then-install step replaced the listed install
  command main kinded as the scanner hint, and its tests pin that a no
  records no write. The scanner-hint write kind therefore has no writer
  and its summary text ("abcd never runs an installer for you") is no
  longer true, so the kind is removed; the fallback test now asserts an
  unkinded write borrows no kind's explanation at all.
- Every input refusal still exits 2: TestCaptureGroundsAndBodyRefusalsExit2
  pins the site-renderer grounds refusal and the locked-body wontfix
  refusal to exit 2, and the unreadable-directory surface test now also
  covers capture list and the bare board at exit 1.

Refs: iss-2609261241121312
Refs: iss-2609261631120364
Refs: iss-2609260552251398

Assisted-by: Claude:claude-opus-5-5
Measured on a clean clone of 00a2506 (dry-run assemble per position):
widening 1,277,632 tokens / 4,918,884 bytes, window 1,280,000 -> 1,300,000
(0.18% headroom); entailment 379,456 / 1,460,909, window 390,000 kept
(2.78% headroom); detection 1,286,668 / 4,953,672, window 1,290,000 ->
1,300,000 (0.26% headroom). Comparative has no qualifying widening run
at this tip and is unchanged.

Refs: iss-2609251455354719

Assisted-by: Claude:claude-opus-5-5
@REPPL
REPPL enabled auto-merge September 28, 2026 10:17
…n tests

toolFreePath cleared CI but not GITHUB_ACTIONS, the other variable
cienv.Runner reads, so on a GitHub runner the install took the CI refusal
and the two end-to-end confirmation tests failed on both CI legs while
passing locally. Watched red with GITHUB_ACTIONS=true set, green after.

Assisted-by: Claude:claude-opus-5-5
@REPPL
REPPL added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit 18da361 Sep 28, 2026
13 checks passed
@REPPL
REPPL deleted the integ/land-7 branch September 28, 2026 11:33
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