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
Conversation
…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
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
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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
nextand 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 missingghis 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
SHELLdomain 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). Aguard.jsonthe guard refuses is refused here too, loudly on stderr, andSHELLteaches 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 nextruns (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 credentialbare and by name, and the bareabcd ahoyboard (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 indisciplines/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:orrefines: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_tagand its success notice opens with "abcd updated from X to Y";abcd updateopens 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_releasetonextin 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, andlaunch shipprints 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 drainreads which issues a machine may fix alone from an accepted decision record in the drained repository carrying fourdrain_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 installoffers to write abcd's strict baseline, only to a person at a terminal and only on a yes.ghis explained, then offered (ghOffer, feat/ahoy-offer-gh). When the GitHub CLI is missing,abcd ahoy remote applyandabcd site setupexplain 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'soracle.denylist; cfSettled's and targetNext's status-block tests callReadwith the peer reader connectWarn added; and targetNext's two tests use main's lane fieldStage. 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 tocache/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 dropsintent.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