Skip to content

v0.12.0 integration 24b2: guard trim, repo guard lessons, provider route warnings, settled blockers, filing match on every route, update notice, release targets, drain rule, gh offer - #760

Merged
REPPL merged 78 commits into
mainfrom
integ/land-24b2
Sep 30, 2026

Conversation

@REPPL

@REPPL REPPL commented Sep 30, 2026

Copy link
Copy Markdown
Collaborator

This lands nine reviewed branches as one change, the second half of Integration 24b for v0.12.0 (still breaking, still unreleased). For a person using abcd: the shell guard refuses more ways of deleting the root or the home directory, and the rules an agent reads before shell work now teach the repository's own guard entries; a repository route to a paid provider is skipped with a warning everywhere it is read instead of breaking every command; an intent waiting on a blocker that was replaced by an accepted decision, or reclassified as a discipline, can start; every route that files a record, a cold reading's findings included, links a repeat to the record it repeats; an update of the abcd binary is announced once, by whatever swapped it; a release cut moves every target it passes to next and the board shows each intent's target; the drain reads the drained repository's own rule for what a machine may fix alone; and a missing gh is explained and offered for install on a yes typed at a terminal.

The guard reads the shape of a trim (drainTrim, fix/guard-trim-to-root). The shell guard let rm -rf ${X%${X#?}} through, which deletes the root: a trim whose pattern can take the rest of the value leaves only the leading /. The guard now reads the shape of a trim's or replacement's pattern and decides from it what the expansion can print whatever the value holds, so the everyday trims (${DIR%/}, ${f%.txt}, ${p##*/}, "$HOME/${d##*/}") keep passing. Five review rounds added more readings, each as bash reads it: ANSI-C and locale strings in a default's word, a line that names IFS, a replacement's pattern read where both bash 3.2 and bash 5 end it, indirect, positional and special parameters, a parameter that can print nothing, and an assignment target that holds an expansion. A reading the guard cannot finish refuses.

The rules teach the repository's own guard entries (teachRepoGuard, feat/guard-teach-repo-entries). Every rules load rebuilds the SHELL domain from the registry the guard enforces in the repository, the bundled entries merged with .abcd/guard.json, through the generator the bundled entries use (ruling CK1). A lesson in the repository's own words is marked (repo). A guard.json the guard refuses is refused here too, loudly on stderr, and SHELL teaches the registry the guard falls back to.

Configuration warnings on every door (connectWarn, fix/ahoy-connect-config-warnings, carrying rulingsC). Three technical-facilitator rulings of 2026-09-29: the transcript store's refusal of a records directory another account owns names the owning uid and no longer reports a refused write on a read (CB1); the bare board's "next up" runs the same peers check abcd build next runs (CC1); and a repository's route to a provider that holds a key is skipped with one warning while the machine's own route to that name applies and every other route keeps working, a denylisted route still refused (CD2). abcd ahoy connect, abcd ahoy credential bare and by name, and the bare abcd ahoy board (a new optional gap, oracle_api.route_skipped) now name each skipped route.

A settled blocker releases the intent waiting on it (cfSettled, fix/blocker-settled-adr-discipline, carrying recRulingsDR). The decision log records the eighteen rulings the person gave on 2026-09-30. The build's blocked check follows a superseded blocker to an ADR successor and settles the edge when that decision is accepted (CF1), and counts a blocker in disciplines/ as settled (CF2); any other decision status, a missing status, or a decision this checkout does not hold still refuses naming it. The board's "next up" reads the same check. The capture page says an agent offers a state-of-the-art research pass before a person writes the real fix for a machine-filed record.

Every filing route checks for repeats (filingMatch2 + filingReading, feat/filing-duplicate-every-route). A report promoted from the inbox, a finding the consistency pass files, and a finding a cold reading returns each get a duplicates: or refines: link when they repeat an existing record in other words, and promoting an accepted reading item into an intent draft checks again. The reading route follows ruling DQ2b and reverses itd-180's warm-work-only rule, recorded as adr-2609300821558671. One scorer and one link writer serve every route.

An update is announced once, by whatever swapped it (installerMeta2, fix/installer-previous-tag). The plugin bootstrap records the release it replaced as previous_tag and its success notice opens with "abcd updated from X to Y"; abcd update opens its receipt with the same line (rulings CJ1 and CJ1b). The session-start version comparison is removed. A swap made by a hook whose output nobody sees is shown once at the next session start, claimed per release with one exclusive create, so sessions starting together show it exactly once.

A cut moves the targets it passes (targetNext, feat/target-release-next). When a release goes out without an intent that was meant to be in it, the cut rewrites that intent's target_release to next in the same write as the changelog roll, under the intent store's lock and undone if the cut fails; the release notes name every move, and launch ship prints and returns them. Each status row carries its target, shown in the brackets of Now and Next on the text board and on the site's Status page. This finishes itd-2609212103572513.

The drain reads the drained repository's own rule (drainOwnRule, feat/drain-own-rule). abcd drain reads which issues a machine may fix alone from an accepted decision record in the drained repository carrying four drain_ fields, and refuses, naming how to add it, when there is none or it is partial, malformed or ambiguous (ruling BX2). Loosening a floor is named on every dry run and start (ruling H11). Records whose remedy waits on a ruling, or whose deferral is live at the current release tag, are always handed back. abcd ahoy install offers to write abcd's strict baseline, only to a person at a terminal and only on a yes.

gh is explained, then offered (ghOffer, feat/ahoy-offer-gh). When the GitHub CLI is missing, abcd ahoy remote apply and abcd site setup explain what it is and why they need it, then offer to install it (ruling DQ3). The install runs only on a yes typed at a terminal; --yes, a piped answer and a run with no terminal each decline, and the refusal shows the command to run by hand.

Integration. Conflicts were resolved hunk by hunk, keeping both sides. Three semantic conflicts were fixed in their merge commits: connectWarn's denylisted-route test relied on the bundled anthropic/* denylist that main retired (ruling H9), so it now writes that entry in the machine's oracle.denylist; cfSettled's and targetNext's status-block tests call Read with the peer reader connectWarn added; and targetNext's two tests use main's lane field Stage. A fourth, found by the preflight: drainOwnRule's stated-ADR writer searched the skeleton for its closing delimiter, which main's delimiter gate refuses, so the skeleton and the stated record are now built from the same two pieces. Follow-ups: a dated DECISIONS.md line corrects the unseen-update claim's name to cache/update-shown-<new>; itd-2609211116005482 decision 9 carries a dated amendment citing CF1 and CF2; a skipped test pins that a decision id two files claim is settled first-wins at this head, to be lifted when lane adrIdUnique lands; a guard test comment that named a real home directory is fixed and its capture resolved. The reading windows are recalibrated at the tip (widening 1,460,000, detection 1,470,000, entailment kept at 420,000), and the reach-audit baseline drops intent.WithMintLock, which targetNext now reaches.

Delivers: itd-2609212103572513
Resolves: iss-2609281911024185
Resolves: iss-2609281911024838
Resolves: iss-2609290426544292
Resolves: iss-2609291942520919
Resolves: iss-2609300057304812
Resolves: iss-2609300057311045
Resolves: iss-2609300057318410
Resolves: iss-2609300057326778
Resolves: iss-2609300057462186
Resolves: iss-2609300057467536
Resolves: iss-2609300651115290
Resolves: iss-2609300651122268
Resolves: iss-2609300651127327
Resolves: iss-2609300651133651
Resolves: iss-2609300711394491
Resolves: iss-2609300711394709
Resolves: iss-2609300711402515
Resolves: iss-2609300726419415
Resolves: iss-2609300726507446
Resolves: iss-2609300751191426
Resolves: iss-2609300756163382
Resolves: iss-2609300805090515
Resolves: iss-2609300812525892
Resolves: iss-2609300939590291
Resolves: iss-2609301046113575
Refs: iss-2609291731336469, iss-2609292320015665, iss-2609300009506126, iss-2609300009581165

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

…s root

rm-rf-root-or-home compared each operand with one written spelling, the
variable, so `rm -rf ${DIR:-$HOME}` and `rm -rf ${DIR:-/}` allowed though
they delete the home or the root with DIR unset. A word's written spelling
(segment.spelled, varSite.texts) is now the set of texts it can print:

- a default or an assignment (`-`, `:-`, `=`, `:=`) is the variable and
  every text its word prints, read through spellWord, also at the first
  operator after a subscript's `]` as bash 3.2 reads it (`${X[0]]-$HOME}`);
- an alternative is its word's texts and the empty text, since it prints
  nothing when the variable is unset (`${X:+x}$HOME` is `$HOME` too);
- a substring is the variable and the `/` a path begins with
  (`${PWD:0:1}`), and a replacement whose pattern is only `*` is the
  variable and its string (`${X/*/$HOME}`);
- an error message (`${DIR:?$HOME}`) stays the variable alone.

A word is every combination of its sites' texts. The spelling follows a
word into nested expansions 8 deep (spellWordDepth, was 3) and holds at
most 16 texts (maxSpellings); past either bound the site is spellCapped,
which the arg_values compare reads as naming every value (writtenMatches),
so the bound refuses rather than passes. A string handed to a shell is
written out once per text (spelledViews, at most 16 views) and every
pairing is kept (spellPayload), so `sh -c "rm -rf ${DIR:-$HOME}"` blocks
as the bare line does. The spelling's work is tallied and held linear.

The adversarial corpus gains the refused forms and their safe look-alikes
(`${DIR:-./build}`, `"${TMPDIR:-/tmp}/x"`, `${DIR:?}`, `${DIR/#\~/$HOME}`).
The record gains its remedy line.

Refs: iss-2609290426544292
Assisted-by: Claude:claude-opus-5-5
A suffix trim whose pattern is unknown text or begins with a glob can
leave only the leading slash of an absolute path (`${X%${X#?}}` and
`${X%%[!/]*}` print `/` with X=/a/b on bash 3.2), and rm-rf-root-or-home
reads the trim as its variable alone. Confirmed while fixing the default
word; left for its own change because reading every trim as the root
refuses the everyday `${DIR%/}` and `${f%.*}`, so the rule needs the
pattern's shape. The record carries its remedy.

Refs: iss-2609292320015665, iss-2609290426544292
Assisted-by: Claude:claude-opus-5-5
…ome or root

Resolves: iss-2609290426544292
Assisted-by: Claude:claude-opus-5-5
…trim

Two siblings of the trim that leaves only the root, confirmed on bash 3.2,
/bin/sh and dash while fixing it: an expansion that prints nothing whatever
the value is (`${X%%*}/` is the root), and a replacement whose pattern can
take the whole value (`${X/\/*/$HOME}` is the home).

Refs: iss-2609300009506126
Refs: iss-2609300009581165
Refs: iss-2609292320015665

Assisted-by: Claude:claude-opus-5-5
rm-rf-root-or-home read a trim as its variable alone, so a pattern that
can take the rest of the value passed: with X=/a/b, bash 3.2, /bin/sh and
dash print `/` for `${X%${X#?}}` and `${X%%[!/]*}`, and `rm -rf` of either
allowed. readPattern now records a trim's or a replacement's pattern shape
in one left-to-right pass (its first and last element, the same past any
run of `*`, and whether it can take any length), and the spelling adds
what that shape lets the expansion print whatever the value holds:

- a suffix trim whose pattern can take any length and whose first element
  past its `*`s is a glob or unknown text (`$Y`, `${...}`, `$(...)`, a
  backtick), and a prefix trim whose last such element is, can leave only
  the root, and nothing (`${X%${X#?}}`, `${T##*[!/]}` with T=/tmp/x/);
- a longest trim whose pattern can match the whole path prints nothing, so
  the text beside it is the word (`${X%%*}/` is the root);
- a replacement whose pattern can match the whole path prints its string
  (`${X/?*/$HOME}` is the home), and one that can match all of it after
  the leading `/` prints `/` and its string (`${X/${X#?}}` is the root);
- a substring can also print nothing, and bash 3.2 prints nothing for a
  trim, a replacement or a substring after a scalar's subscript
  (`${X[0]%zzz}/` is the root).

The everyday forms keep their verdicts: `${DIR%/}`, `${f%.txt}`,
`${p##*/}`, `${p%/*}`, `${p#$HOME/}`, `${X%?}`, `${X/foo/$HOME}`,
`${DIR/#\~/$HOME}` and `${name//[^a-z]/}`. `${DIR%$HOME}`, pinned as
allowed when a trim read as its variable alone, now blocks: its pattern is
unknown text at the end it trims from; the pin keeps its intent as
`${DIR#$HOME/}`. Every existing corpus line keeps its verdict (1070 lines,
dumped through the binary before and after); the corpus gains 15 block and
14 quiet lines.

Refs: iss-2609292320015665
Refs: iss-2609300009506126
Refs: iss-2609300009581165

Assisted-by: Claude:claude-opus-5-5
The trim that leaves only the root, the expansion that prints nothing
whatever the value holds, and the replacement that takes the whole value
are read by their pattern's shape (d82049f).

Resolves: iss-2609292320015665
Resolves: iss-2609300009506126
Resolves: iss-2609300009581165

Assisted-by: Claude:claude-opus-5-5
Criterion 3 of itd-2609212103572513, as the product thinker ruled it on
2026-09-29 (BS1): when a release goes out without an intent targeted at
it, the target becomes `next`, whatever the following release is
numbered, never a version number.

- launch.MissedTargets picks the targets a cut passes: `next` (it named
  the release being cut) and a tag at or below the derived version. A tag
  above the cut is still ahead and stays; an illegal value is left for
  the record lint.
- The ingest (the write that rolls the changelog) rewrites each such
  record's `target_release` to `next` under the intent store's lock,
  first among the cut's writes, and restores it on the cut's undo. A
  record whose target changed since the cut read it stops the cut.
- The dated section names the move in one line under its notice, ahead
  of the first change-type heading (changelog.TargetMoveNote), so
  delivery_state does not judge it. The site's release stamp (releaseOf)
  passes over that line, because it names intents the release did not
  ship.
- `launch ship` prints one `moved:` line per intent; the JSON carries
  `moved_targets`.

Found at the base: the cut's writes are release.Ingest (launch.Ship has
no production caller), so the move lives there, and `next` was already
admitted by the verb and the lint (AC1 built earlier); a lint test pins
it.

Assisted-by: Claude:claude-opus-5-5
Criterion 4 of itd-2609212103572513: given the status block, when a
targeted intent is listed, then its row shows the target.

- statusblock.Row carries `target_release` for a planned intent, in Now,
  Next and Later alike; a draft carrying one by hand shows none, since
  the cut reads targets off planned intents alone.
- The text board adds `target <value>` in a Now or Next row's brackets,
  after its lane state or `next up`. Later stays a count (ruling BV1),
  so a Later row's target is in --json and on the site.
- The site's Status page adds the target after what places a row, under
  a new `status.target` label in ui.json (site-src and the setup source),
  following the block's own labels.

Assisted-by: Claude:claude-opus-5-5
…uling BS1

The product thinker ruled on 2026-09-29 (BS1) that a target the cut
passes becomes `next`, whatever the following release is numbered. The
intent's criterion 3 and its in-scope line said "rewritten to the next
version"; both are worded to the ruling, and decision 4 records it with
its reason (the version a cut derives is the one it cuts, so a number
would name a release already out). Spec scope 3 is worded the same way.

Every criterion is met at this head: 1 and 2 by the earlier target lane
(intent target, plan --target, the record_schema leg, the preview and
cut lists), 3 and 4 by the two commits before this one. Closing
spc-2609212138243443 (impact additive, as the intent declares) ships the
intent; its fidelity review is owed (receipt rcp-47e25ab4498e).

Delivers: itd-2609212103572513
Assisted-by: Claude:claude-opus-5-5
…me every loosened floor, hand back what waits on a person

The drain no longer applies a rule compiled into the binary. It reads the
drained repository's own decision record and refuses a repository without
one, as the product thinker ruled on 2026-09-29.

BX2, verbatim: "the PROJECT MUST HOLD the eligibility decision in its own
record (e.g. added at setup); drain refuses there until it does".
H11, verbatim: "MAY LOOSEN abcd's floors (a project may let drain take
major/critical and security issues). NOTE for the lane: make a loosened
floor loud (drain --dry-run and the drain start name every floor the
project loosened), and keep abcd's own repository at the stricter default."

The record: the one accepted ADR in .abcd/development/decisions/adrs/ whose
frontmatter carries drain_categories (inline list, a subset of the fixable
set), drain_severities (inline list of severities), drain_security
(handback | take) and drain_remedy (required, its only value: the remedy is
the brief a lane works from). The new leaf package core/drainrule reads it
inside an os.Root at the checkout (a store symlinked out of the tree is
refused), measures it against abcd's bundled strict baseline and names
each loosened floor ("severity major", "severity critical", "security").
No record, only a proposed or superseded one, two accepted ones, or a
missing, misspelt, repeated or mis-valued field all refuse (exit 2,
nothing written) with no fallback to the baseline or to a looser rule.
Widening the categories is refused as a decision by kind, which H11 does
not name. abcd's own adr-2609291342092738 now carries the baseline as the
four fields, and TestAbcdsOwnDrainRuleIsTheStrictBaseline fails if it
loosens anything.

Loud: the dry run prints a LOOSENED block (or one line saying nothing is
loosened), both output modes warn on stderr, --json carries `loosened`
and the record's `rule`, and the start's refusal names every floor.

The waiting records (50 of 54 dry-run-eligible records on the remedies
branch wait on a ruling): BOTH shapes are handed back, each its own rule,
whatever the repository's record says. A remedy opening "Waits on"
(case-folded) is `waits-on-ruling`, because taking it would make the
ruling it waits on; a record whose deferred_after names the current
anchor tag is `deferred`, because a person carried it past this release.
They are asked after the category and severity hand-backs, the ruling
before the deferral since it names the decision owed. The release tags are
read only when an open record carries a deferral, and a failure to read
them refuses the plan rather than letting a live deferral through.

The brief chapter states the trust boundary: the record is a
repository-authored file deciding what an unattended agent may do, so a
contributor's pull request can loosen it; it is committed history
reviewed like code, a loosening is loud on every run, and abcd's own
repository keeps the baseline under a test.

Partial of itd-82; its spec stays open (the host judgement, the lane, the
hand-back writes and the pace are still owed).

Assisted-by: Claude:claude-opus-5-5
… on the person's yes

Ruling BX2 (2026-09-29), verbatim: "the PROJECT MUST HOLD the eligibility
decision in its own record (e.g. added at setup); drain refuses there
until it does". Setup is where it is added.

`ahoy install` raises an optional repository gap, drain_rule.offered, in
its own category, drain-rule, asked after oracle-routing, while the
repository's decision store holds no accepted record carrying the drain
fields. A record that states the rule badly is not offered a second one;
the drain names what is wrong with the one it has. The offer states
abcd's strict baseline in one question, and a yes mints it through the
decision store's own seam (new decide.CreateStated: same id, date and
filename allocator as `abcd decide`, written accepted, since the person's
yes is the decision) with the four drain_ fields and the four sections.
It re-reads the store before writing, so a record that appeared while
the question was open is never doubled.

Consistent with the routing offers: --yes approves the category but never
writes the record, because it decides what an unattended agent may change
in the repository, and reports it under optional_skipped with that reason;
a decline writes and records nothing, so the next install offers again;
the offer only ever writes the baseline, and loosening is an edit a person
makes to the record. Piped answer streams gain one question, drain-rule,
between oracle-routing and user-state; commands/ahoy.md names the order.

Assisted-by: Claude:claude-opus-5-5
…ing and the waiting hand-backs

One entry at the end of the log for rulings BX2 and H11 of 2026-09-29 as
built by lane drainOwnRule: the record's four fields and where the drain
reads them, every refusal without a fallback, how a loosened floor is
named, the setup offer, and the choice of BOTH waiting hand-backs (a
remedy opening "Waits on", and a deferral live at the current anchor
tag) with why each is a person's and why the ruling is asked first. The
spec of itd-82 stays open.

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

The security review of the default-word and trim lanes found four ways
rm -rf still reaches the root past rm-rf-root-or-home: a default word
written as an ANSI-C or locale string, an unquoted default word split on
an assigned IFS, a quoted / in a replacement pattern on bash 5, and an
indirect parameter with a default. The fix lane's sweep confirmed two more
outside those four readings: $! read as a number though it is empty until
a background job runs, and a run of / written before the home.

Refs: iss-2609300057304812
Refs: iss-2609300057318410
Refs: iss-2609300057311045
Refs: iss-2609300057326778
Refs: iss-2609300057467536
Refs: iss-2609300057462186
Assisted-by: Claude:claude-opus-5-5
…nd every parameter

Four readings of a ${...} expansion let rm -rf reach the root or the home
past rm-rf-root-or-home (review-guardSet). Each is now read as bash reads
it, and a reading the guard cannot finish refuses:

- spellWord decodes an ANSI-C string (readAnsiCQuote) and reads a locale
  string as the double-quoted string it holds, so ${X:-$'/'},
  ${X:-$'\x2f'} and ${X:-$"/"} are /. $! is its number or nothing
  (${X:-$!/} is /), and any other $ that opens nothing is the $ it is
  instead of an unread word.
- A line that names IFS in any word at any payload layer reads an unquoted
  default's or alternative's word, a trim's or replacement's texts, and an
  unquoted $HOME or $PWD as capped (capIFSSplits, ifsSplits): the fields
  rest on an IFS the guard does not model. Capping is the smaller correct
  change beside reading the assignment, which would need every way IFS can
  be set (export, read, declare, eval layers) and which splitAfterIFS
  already declines to model. A variable of unknown value is not capped, so
  while IFS= read -r d; do rm -rf $d; done stays allowed. The prefix form
  IFS=x rm -rf ${U:-x/x}, which bash does not apply to its own words, now
  refuses too.
- replacementTexts reads the pattern where bash 3.2 ends it and where bash 5
  does, past a quoted /, and unions the texts (${X/"/"*/$HOME}).
  readPattern reads $" as the double-quoted string it opens, so
  ${X%%$""*}/ is /.
- spellParameterAt reads the parameter as bash does (paramNameEnd): after
  an indirection's !, a positional digit run, or one special byte, so
  ${!X:-/}, ${1:-/}, ${@:-/}, ${!:-/} and ${#:+/} read their words. An
  indirection's value past an operator is capped; alone or as a name list
  it keeps its text.

Also found in the sweep and fixed here: a run of / before $HOME or $PWD
names that directory (absoluteName), so rm -rf /$HOME blocks. 17-guard.md's
mechanism paragraph says "any substring" and names the new readings.

The corpus gives the same verdict on all 1099 lines at the base and here.

Refs: iss-2609300057304812
Refs: iss-2609300057318410
Refs: iss-2609300057311045
Refs: iss-2609300057326778
Refs: iss-2609300057462186
Assisted-by: Claude:claude-opus-5-5
…bling

Each record enters resolved/ with impact fix and resolved_by the fix
commit. iss-2609300057467536 ($!/ read as a number) stays open: the
spelling it needs lives in the tokenizer, beside the kill readings of $!.

Resolves: iss-2609300057304812
Resolves: iss-2609300057318410
Resolves: iss-2609300057311045
Resolves: iss-2609300057326778
Resolves: iss-2609300057462186
Refs: iss-2609300057467536
Assisted-by: Claude:claude-opus-5-5
…e it

`rm -rf $!/` was allowed: the tokenizer kept a top-level `$!` as the
number it prints and spelled the word as the literal `$!/`, but before any
job runs in the background `$!` prints nothing, and bash 3.2, /bin/sh,
dash and bash 5.3 all hand rm `/`.

The written spelling (segment.spelled, read only by the arg_values
compare) now holds the empty text beside each parameter that can print
nothing at the top of a fresh shell (emptyable): `$!`, `$@`, `$*`, a
positional one, `$_` (after `x=` or `true ""`) and `$-` (dash), braced or
not, quoted or not, at the top level, in a default's word and as the value
every operator reads. `$!` keeps its text in the token (addBang: a site
over the two bytes, varSite.width), so `kill $!`, `wait $!`, `echo $!`
and `rm -f "$tmp.$!"` read exactly as before; a payload pairing leaves a
word with no variable's mark alone. The empty reading splits into no
field, so a line naming IFS counts these sites as it did (varSite.empty).

Sibling found in the sweep and fixed here: a trim's or a replacement's
pattern read `$!` as literal text, so `rm -rf ${PWD%%$!*}/` (`/` in every
shell) was allowed; `$!` there is now text of any length, as `$Y` is.

Corpus: the 1099 repo-mined and adversarial lines give the same verdict at
5b51dbc and here.

Refs: iss-2609300057467536
Assisted-by: Claude:claude-opus-5-5
The guard reads `rm -rf $!/`, its special and positional siblings and
`$!` in a trim pattern as the root (fixed in 5e3fec2). Impact fix.

Resolves: iss-2609300057467536
Assisted-by: Claude:claude-opus-5-5
The re-verification of the guard rounds found one bypass, two over-blocks
and a pre-existing class: an IFS named through an expansion (a declaration,
a read or printf -v target, an eval'd assignment, arithmetic), the empty
value kept under a colon default, a positional slice read as a substring,
and an expansion that prints nothing beside the root (an empty default
word, a subscript, a case change or a transform).

Refs: iss-2609300651115290
Refs: iss-2609300651127327
Refs: iss-2609300651122268
Refs: iss-2609300651133651
Assisted-by: Claude:claude-opus-5-5
namesIFS matched the literal text IFS, so a name built by an expansion
(export ${I}FS=x, read -r ${I}FS, printf -v ${I}FS x, eval "I${F:-F}S=x")
set IFS unseen, and an unquoted default word split into the root. A word
whose assigned name holds an expansion's mark now counts after a
declaration, read, mapfile, getopts or let, as a printf -v or wait -p
target, or as an assignment-shaped word; the tokenizer also raises
segment.arithmeticAssigns for an arithmetic body that names IFS or
assigns through an expansion (: $((IFS=1))), which it steps over.
splitAfterIFS reads a naming through the same function. The brief's
residual drops declare $(echo I)FS=x, which is now read.

Refs: iss-2609300651115290
Assisted-by: Claude:claude-opus-5-5
A colon default, assignment or error message treats an empty parameter
as unset, so ${1:-dist} with no argument prints dist, never nothing. The
empty text an emptyable parameter adds is dropped under :-, := and :?
and kept under -, = and ?, where a set but empty parameter prints it.
An indirection keeps reading as every value past either form.

Refs: iss-2609300651127327
Assisted-by: Claude:claude-opus-5-5
An empty default word (${X:-}, ${X-}) now prints the empty text, and a
subscript alone, a case change and an @ transform read as the value or
nothing, so ${X:-}/, ${A[0]}/, ${X^}/ and ${X@P}/ read as the root, as
${1:-}/ already did. ifsSplits drops the empty text of every site, since
it splits into no field under any IFS; that replaces varSite.empty and
emptiedParameter, which dropped it for an emptyable parameter only.

Refs: iss-2609300651133651
Assisted-by: Claude:claude-opus-5-5
${@:2}, ${*:2} and ${1:2} print what the positional parameters hold, as
"$2" does, so they read as the parameter's value and nothing rather than
as a variable's substring, which adds the root.

Refs: iss-2609300651122268
Assisted-by: Claude:claude-opus-5-5
The IFS named through an expansion (3c68b00), the empty value kept
under a colon default (8b6ddeb), the expansion that prints nothing
beside the root (97f1d7a) and the positional slice read as a
substring (9e5fb2e), each with impact fix.

Resolves: iss-2609300651115290
Resolves: iss-2609300651127327
Resolves: iss-2609300651133651
Resolves: iss-2609300651122268
Assisted-by: Claude:claude-opus-5-5
… hand back deferrals when no tag is known

Three findings of the drainOwnRule review, on the path that decides what
an unattended drain may take.

The rule's reader narrowed frontmatter.Duplicates to drain_ keys, so a
record stating `status: accepted` then `status: superseded` loaded on
the line scanner's first-wins reading while every YAML reader calls it
superseded. Any candidate record (one carrying a drain_ key) that states
any top-level key twice now refuses as malformed, whatever its status
reads as. A record whose frontmatter id disagrees with the id its file
name gives it refuses too, since every surface names the rule by it.
Each record is read through fsutil.ReadGuardedInRoot under
issueschema.RecordReadLimit, so a record that is a link (even one
resolving inside the checkout) or past the cap refuses under a new
sentinel, drainrule.ErrUnreadable, and `drain --dry-run` exits 2 on
every refusal of the rule as the bare verb does.

With no release tag in the checkout (a --depth 1 --no-tags clone, the
unattended drain's likely checkout), every deferred_after read as
lapsed. The anchor is now marked unknown and every record carrying a
deferral is handed back as `deferred`, the reason and the dry run naming
the missing tags and `git fetch --tags`; a failed tag read still refuses.
Handing back was chosen over refusing the plan: the rest of the dry run
stays readable, and nothing carrying a deferral is taken.

"Waits on" is matched as words: followed by a blank, a colon or the end,
so "Waits on: ruling H4." is handed back and "Waits onward" is not.

Sweep: the issue records the drain reads go through issuerecord's capped
reader and its parser refuses a duplicate key (the record lands as
unreadable), so the drain's other repository-authored input already
holds both properties.

Refs: iss-2609300711394709, iss-2609300711402515
Assisted-by: Claude:claude-opus-5-5
…swer stream keeps its order

The drain-rule category question and the offer were inserted mid-order,
and the stdin prompter reads one line per question, so a scripted
`ahoy install` handed the answer meant for a later question to the drain
rule, could write an accepted record the script never asked for, and
left the last question reading EOF.

The offer now follows the itd-131 precedent of the git identity
question: it is asked only when the prompter is at a terminal. Off one,
resolveApproval neither asks the drain-rule category nor counts it
declined, stepDrainRule does not run, the already-up-to-date check does
not wait on it, and the result lists drain_rule.offered under
optional_skipped, the text naming the terminal as the way to be asked.
A terminal gate was chosen over a named --drain-rule flag: the record
decides what an unattended agent may do, which a scripted answer is not a
person's yes to, and a flag would hide the offer from the person at a
terminal it is for.

Refs: iss-2609300711394491
Assisted-by: Claude:claude-opus-5-5
…wn anchor and the stricter rule reader

One entry appended after the drainOwnRule entry, correcting three of its
points after review: the drain-rule offer is asked only at a terminal
(and why that over a --drain-rule flag), a checkout with no release tag
marks the anchor unknown and hands back every record carrying a
deferral, and the rule's reader refuses any duplicated key, an id its
file name contradicts, a linked record and one past the size cap.

Refs: iss-2609300711394491, iss-2609300711394709, iss-2609300711402515
Assisted-by: Claude:claude-opus-5-5
…'s rule reader and tagless anchor

Both fixed in 788f0a5: a checkout with no release tag hands back every
record carrying a deferral, and the rule's reader refuses any duplicated
key, a contradicted id, a linked record and one past the size cap.

Resolves: iss-2609300711394709
Resolves: iss-2609300711402515
Assisted-by: Claude:claude-opus-5-5
…nly at a terminal

Fixed in 6683d62: off a terminal the drain-rule category and offer are
neither asked nor counted declined, so a piped answer stream keeps its
order, and drain_rule.offered is listed under optional_skipped.

Resolves: iss-2609300711394491
Assisted-by: Claude:claude-opus-5-5
The colon forms of @ and * read as a single empty parameter does, a
regression of the round-3 colon reading, and an IFS assigned through a
name built from an expansion in a context the per-context reading never
reached ($[ ], a subscript, an integer attribute, an increment, a
substring offset, a value arithmetic evaluates).

Refs: iss-2609300726419415
Refs: iss-2609300726507446
Refs: iss-2609300651127327
Refs: iss-2609300651115290
Assisted-by: Claude:claude-opus-5-5
${@:-w}, ${*:-w}, ${@:=w} and ${@:?} test whether there are any
positional parameters, not whether a joined value is empty: after
set -- "" "" they print the two empty parameters, so ${@:-x}/ is / and
$HOME${*:?} is the home in bash 3.2, /bin/sh, dash and bash 5.3. The
colon reading that drops the empty value now applies to a single
positional or special parameter only; ${1:-dist}/ stays allowed.

Refs: iss-2609300726419415
Assisted-by: Claude:claude-opus-5-5
…ly recurrence rule

The person ruled on 2026-09-30 that a reading finding's likely repeats
are matched mechanically: stored as a link on the finding, shown at
storing time, and checked again at the promote step. That reverses
itd-180's ruling that spotting a recurrence is the researcher's warm
work and never a mechanical join.

- adr-2609300821558671 records the ruling verbatim, with a typed
  `reverses` link to itd-180 (frontmatter and the Typed links line).
- itd-180 states the two halves of the recurrence link in the present
  tense, citing the ADR: the stored duplicates:/refines: proposal, and
  the researcher's `recurs` citation as its confirmed form. No
  disposition state means "already covered".
- One DECISIONS.md line, appended.

Refs: iss-2609281911024185
Assisted-by: Claude:claude-opus-5-5
… mints

Ruling DQ2b (adr-2609300821558671) finishes the filing-time match on the
third route of iss-2609281911024185, the reading ingest, in all three
ways the person chose. The one canonical match is extended, not copied.

- (a) A reading record may carry `duplicates:` and `refines:` naming an
  iss-N, itd-N or rdi-N (issueschema.ReadingKnown gains exactly those two
  keys; the writer's validator checks the ids). `reading ingest` runs
  capture's match on each item under the ledger lock, with the same
  threshold, link cap and configuration. It compares the finding's own
  words (pattern and body, never the envelope every item of a run
  shares) with the open and resolved issues, the intents and every
  earlier reading item, and never with an item the same ingest files.
- (b) The ingest shows each record's match: `matches` in --json, and
  renderMatch under each record id in the plain rendering.
- (c) `capture promote <rdi-N>` matches the draft it mints on the item's
  finding (intent.Matcher.Text), against the set a capture is compared
  with, and links the draft as a capture is linked; PromoteResult.Match
  carries it and the verb prints it.

capture/match.go splits matchAndLink into rankExcept and linkMatches, so
the issue and reading families share one scorer and one link writer and
differ only in the validator. Earlier reading items are candidates on
the reading route only: a capture's candidate set is unchanged.

Docs: brief 04-surfaces/06-capture and 23-reading, commands/capture.md
and commands/reading.md, the --recurs flag help and the ingest help
(go generate).

Refs: iss-2609281911024185
Assisted-by: Claude:claude-opus-5-5
…very route

All three routes are done: inbox promote and the consistency pass
(1029ff5), and the reading ingest with the promote of a reading item
(b796ac7), per ruling DQ2b.

Resolves: iss-2609281911024185
Assisted-by: Claude:claude-opus-5-5
The rulings CJ1 and CJ1b: the installer records the release it replaced
and says "abcd updated from X to Y" once, when it finishes, and the
session check stops reporting the transition.

- hooks/bootstrap.sh records previous_tag in the binary-meta it writes at
  the cache and per-root swaps, and opens its one-line success notice
  with the update line when the release changed. A first install, a
  cache copy at the same release and the fast path print no such line.
- update.UpdatedFormat is the one wording; `abcd update` opens its
  receipt with it, and a test holds the script's printf literal to it.
- The session-start setup_version comparison (ahoy.VersionTransition)
  is removed; ahoy's own version.upgrade gap stays.
- The single exception: the salvage runs in the UserPromptSubmit,
  PreToolUse and PreCompact hooks discard the bootstrap's output, so
  they pass --unseen, the swap records transition_unseen=yes, and the
  next session start shows the line once and writes cache/update-shown
  in the data dir (dataDirHazard guard, release-tag shape check), its
  only write.
- itd-111 criterion 6 is amended, with a DECISIONS entry citing CJ1b.

Refs: iss-2609291942520919
Assisted-by: Claude:claude-opus-5-5
…t the swap

Resolved with the rulings CJ1 and CJ1b applied in the previous commit;
its deferral past v0.11.1 is dropped with it.

Resolves: iss-2609291942520919
Assisted-by: Claude:claude-opus-5-5
A reading record's duplicates: and refines: may name an earlier reading
item (ruling DQ2b, adr-2609300821558671). record_schema's cross-reference
check read only adr, itd, iss and spc handles, so a link naming an rdi-N
the ledger does not hold passed unread. The handle pattern now covers
rdi, so the gate resolves every id such a link names, as the ADR says.
The gate's output over this repository is unchanged.

Refs: iss-2609281911024185
Assisted-by: Claude:claude-opus-5-5
The session check read its shown marker and then wrote it with a
create-temp-and-rename, so sessions starting together each saw no marker
and each showed the update line (16 of 16 in a probe). Once is now one
exclusive create per release: cache/update-shown-<tag> in the plugin data
directory, opened O_WRONLY|O_CREATE|O_EXCL at 0600 after the dataDirHazard
and ReleaseTagShape checks. An existing claim path (a link included) or any
create error shows nothing, so the line never repeats. Claims for earlier
releases are left in place: removing one would be a second write, and each
is an empty file.

With no data directory the bootstrap runs per-root and records the swap in
the plugin root's .binary-meta; the check now reads that record and claims
.update-shown-<tag> in the root with the same function and the same single
write, behind the same shape check, so a per-root unseen swap is shown once
rather than never.

The brief's update chapter and commands/update.md name both claim files.

Refs: iss-2609300939590291
Refs: iss-2609291942520919
Assisted-by: Claude:claude-opus-5-5
…e per release

Resolves: iss-2609300939590291
Assisted-by: Claude:claude-opus-5-5
Assisted-by: Claude:claude-opus-5-5
Conflicts, resolved hunk by hunk:
- itd-2609081951381895 Decisions: item 8 takes the branch's CD2 wording
  (a repository route to a keyed provider is skipped with a diagnostic);
  item 9 keeps main's 2026-09-30 H9 amendment (no bundled denylist).
- statusblock_test.go: main's Lane.Stage field with the branch's third
  Read argument (the peer reader), in all four calls.
- oracle/config.go merged without a textual conflict: CD2's skipKeyedRoute
  and main's cd1 typed-route settings merge both stand.

Semantic conflict fixed here: TestADenylistedKeyedRepositoryRouteIsStillRefused
relied on the bundled anthropic/* denylist, which main removed (H9,
adr-2609300107513982); the test now writes that entry in the machine's
oracle.denylist and expects the refusal to name oracle.denylist.

Assisted-by: Claude:claude-opus-5-5
Conflict, resolved by hunk: commands/abcd.md keeps connectWarn's
peers-check sentence (the head passes over an intent another checkout
holds) and takes cfSettled's "an unsettled blocker".

Semantic conflict fixed here: cfSettled's new statusblock test calls
Read(root, nil); connectWarn added the peer reader as Read's third
argument, so the call is Read(root, nil, nil).

Assisted-by: Claude:claude-opus-5-5
…eading, 205b2ad)

Conflict, resolved by hunk: the ADR index keeps both new rows, main's
adr-2609300107513982 first and the branch's adr-2609300821558671 after
it, in id order.

Assisted-by: Claude:claude-opus-5-5
Conflicts, resolved by hunk:
- brief 04-surfaces/08-abcd.md: the JSON row list takes targetNext's
  target_release clause with main's lane key `stage`; connectWarn's
  peers-read sentence below it stands.
- statusblock_test.go: both new tests stand (connectWarn's
  TestTheHeadPassesOverAnIntentAPeerHolds, targetNext's
  TestARowShowsItsTarget).

Semantic conflicts fixed here: targetNext's two tests wrote the lane's
Step field and a two-argument Read; main renamed the field Stage and
connectWarn added the peer reader, so TestARowShowsItsTarget calls
Read(..., nil) with Stage and TestBoardRowShowsItsTarget (cli) writes
Stage.

Assisted-by: Claude:claude-opus-5-5
Conflict, resolved by hunk: brief 02-constraints/03-invariants.md takes
the branch's rewrite of invariant 19 (the drain reads the drained
repository's own rule) and keeps main's invariant 20 (the dependency
re-authoring bound) after it.

Assisted-by: Claude:claude-opus-5-5
Assisted-by: Claude:claude-opus-5-5
The installerMeta2 entry names the session start's single write
`cache/update-shown`; the file its fix round built is
`cache/update-shown-<new>` (per-root `.update-shown-<new>`). A dated
correction line is appended at the end; the entry itself is untouched
(DA002).

Refs: iss-2609291942520919
Assisted-by: Claude:claude-opus-5-5
…nd CF2

Decision 9 still read that a supersession chain ending at a decision
(adr-N) blocks. The product thinker's rulings CF1 and CF2 of 2026-09-30
settle a chain ending at an accepted ADR, and a blocker sitting in
disciplines/, as lane cfSettled built them in followBlocker. A dated
amendment says so in the item; the ruled text before it stands.

Refs: iss-2609300751191426
Assisted-by: Claude:claude-opus-5-5
…nique

A supersession chain ending at an ADR whose id two files in the store
claim (one accepted, one proposed) should refuse, not settle on the
first file the scan reads. At this head it settles first-wins:
recordid.LookupOne keeps the first file in scan order, so the accepted
copy settles the edge. Watched without the skip: the test fails with
OK=true and "itd-27 → adr-37 (accepted)". It is skipped with a comment
naming lane adrIdUnique (fix/lint-adr-id-unique f6cd7b2), which makes
the duplicate a refusal and lands later.

Assisted-by: Claude:claude-opus-5-5
The comment on TestDefaultWordsSplitOnANamedIFSTheWrittenCompareReads
gave a developer account's absolute home path as its example HOME, and
abcd lint's privacy-hygiene rule refuses the tree over it. The comment
now says what the example needs, a macOS home whose account name holds
a v, without the path. Captured in the same commit.

Refs: iss-2609301046113575
Assisted-by: Claude:claude-opus-5-5
…no home path

Resolves: iss-2609301046113575
Assisted-by: Claude:claude-opus-5-5
Measured on a clean clone of a586d62 (the 24b-2 merges and the
follow-ups): widening 1,441,769 estimated tokens, window 1,440,000 ->
1,460,000; detection 1,450,805, 1,450,000 -> 1,470,000; entailment
411,876, window 420,000 kept (1.97% headroom, over the 1% rule), its
measurement updated.

Refs: iss-2609251455354719
Assisted-by: Claude:claude-opus-5-5
internal/core/release/ingest.go (targetNext) now calls
intent.WithMintLock from outside its package, so its line leaves the
unreached baseline: 192 unreached, 192 baselined.

Assisted-by: Claude:claude-opus-5-5
…imiter search

Semantic conflict between drainOwnRule and main: drainOwnRule's
renderStated found the skeleton's closing delimiter with a private
strings.Index on "---\n\n" and spelled a third delimiter literal, and
main's TestNoPrivateDelimiterCompare (frontmatter) pins decide.go at two
writer literals. The skeleton and the stated record are now built from
the same two pieces, renderKeys (the opening delimiter and the nine keys
at a given status) and renderCloseAndTitle (the closing delimiter and the
H1), so nothing searches for a delimiter and the file spells two.
Watched: TestNoPrivateDelimiterCompare failed in the integration
preflight (4 literals, allowlist 2) and passes now; the decide, drainrule
and ahoy drain-rule tests pass.

Assisted-by: Claude:claude-opus-5-5
@REPPL
REPPL enabled auto-merge September 30, 2026 11:25
@REPPL
REPPL added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit c31e906 Sep 30, 2026
14 checks passed
@REPPL
REPPL deleted the integ/land-24b2 branch September 30, 2026 12:36
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