Skip to content

fix: close 29 records: declarations judged folder by folder, a benign config replace re-judged, home-deletion guard residuals, and a credential-store audit - #748

Merged
REPPL merged 104 commits into
mainfrom
integ/land-17
Sep 29, 2026
Merged

REPPL merged 104 commits into
mainfrom
integ/land-17

Conversation

@REPPL

@REPPL REPPL commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

This lands eight reviewed lanes as one change. Together they close 29 open records, re-defer 42 others out loud against v0.11.1 (each naming the ruling, planning or lane it waits on), and record one fidelity audit. Four of the fixes are on a security path: a declaration file in ~/.abcd was trusted even when the folder holding it could be changed by another account; the status-line setting was read through a second look that judged nothing; the secret scan missed a token glued onto the word before it; and the shell guard let several spellings of the home or the root past its recursive-delete check. The rest remove two spurious plugin agents, close a symlink read in the abcd reading status render, state the guard's tool reach on every surface that describes it, keep config paths out of .git, and bring records, gates and documents back in line with the tree.

fsVet (security). Every folder level below home on the way to a declaration is now judged on the descriptor opened for it: a folder every account can write, or one owned by an account that is neither you nor root, is refused as DeclarationDirectoryExposed, naming the folder and the chmod o-w that repairs it. Absence is still decided first, home itself is still not judged, and every caller renders the refusal beside its symlinked-folder case. abcd's own writers make ~/.abcd at 0755 or 0700, so neither case arises from an install. statusline.ReadSettingsFile reads ~/.abcd/statusline.json through the same canonical read, so a file swapped in between two looks is no longer read on the judgement made about the file it replaced. One question stays open for the product thinker: whether a group-writable ~/.abcd (0775, which the documented mkdir -p ~/.abcd makes under a user-private-group umask of 002) should be refused too.

drainObs (observations). The installed plugin stops listing abcd:README and abcd:CHANGELOG as agents: the agent directory's readme and prompt-version log move to .abcd/development/agents/. The bare abcd reading status render refuses a local tier that is a symlink out of the checkout instead of listing names from outside it. prose_citation_resolves reads the whole durable record; the docs-lint harness rules catch a host named through its plugin directory or an environment variable; one strip-then-decode reader, frontmatter.UnquoteScalar, replaces three private copies; and the include table, the .abcd/README.md index, .gitattributes, one principle and the brief's meta chapter now say what the tree does. Two records are deferred for a product ruling (the persona role check and a spec step that waits on another intent), and one new capture records that the agent contract's default log path still sits inside agents/.

triageMajorA (major findings, first half). The guard's tool reach is stated as a standing limit on the guard brief chapter, the plugin page and the guard hook help (and so the generated CLI reference): the hook manifest hands the guard the shell tool and the question tool and nothing else, and a call through any other tool never reaches it. A new test pins the clause on all four surfaces. The matcher is not widened. Eight lapsed major findings are re-deferred, each naming what it owes.

triageMajorB (major findings, second half). The smoke lane runs the record-writing verbs (capture, capture resolve, decide) through the built binary against a scratch repository and asserts what lands on disk. The product thinker's ruling on third-party interface guidance and redaction purposes is written into a new principle, guidance-carries-its-evidence-and-its-purpose.md, with one decision-log line saying why it lives there. Twenty-three lapsed major findings are re-deferred; five are promoted into draft intents and stay open until those intents ship, as the ledger's promotion rule requires.

drainEcho4 (security, secret scan). The scanner's secret patterns only started a match at a word boundary, so a key written notes_<token>, or a token with a letter on each side, went unrecognised: a refusal naming such a key or path repeated the token, and the launch scan, the memory writer's page-name check and the store-before-commit redactors missed it. Every scan now also runs a glued-token sweep that retries the boundary-anchored hard-fail patterns without the boundary in one linear pass, over the raw line and its percent-decoded and JSON-unescaped copies. A refusal seals a glued token byte for byte and keeps the rest readable; a memory page whose name carries one is refused; and a pattern the sweep cannot build marks the scanner degraded, so every redactor and the launch scan refuse rather than run a narrower sweep. Over the repository's 4,605 tracked text files the sweep adds no finding and 7 to 10 per cent to the scan's time.

audits12 (fidelity audit). The fidelity audit of the shipped credential-store intent (itd-2609221017023290) is recorded on the intent: one criterion met and four met with concerns, none unmet. Two places where the record says more than the code does are captured as minor drift and stay open: the setup takes the credential home as a flag rather than asking for it, and only the abcd home's write is refused inside a working tree, where the intent and its ADR still say every write. The audit also narrows one condition: the keychain home is exercised only through the test binary's fake, which is already an open, deferred record. No code changed.

drainRest (drain of fifteen records). Six are fixed and resolved: every repo-relative path in the positioning, docs-lint and record-lint configs refuses a path inside .git, through the shared check the site manifest already used; abcd <itd-N> and abcd <spc-N> say that the spec close is what ships the intent; AGENTS.md states the scan-before-mutating rule by blast radius; itd-5's pre-flight steps agree with its own amendment; two closed specs stop stating tree facts that were false; and the ingest page carries the two author-name conventions. The bootstrap hook's network refusals now name the proxy and CA-bundle variables the lockdown ignores (a single-quoted constant, never expanded; the lockdown is unchanged), and that record stays open on its deferral, because whether to offer a way through the lockdown is a product decision. Eight more are deferred past v0.11.1, each with the ruling it owes on the record.

guardResid (security, shell guard). The guard promises to block a recursive delete of the home or the root, and several spellings got past it: a backslash-newline inside a variable's name, a variable inside a brace group or a sequence expression, a ${HOME...} expansion whose operator can leave the value as it is, an alternative after a subscript as bash 3.2 reads it, an alternative's word split on whitespace, a root or home path with redundant separators (//*, /./*, $HOME//), and a path that climbs with .. from a named directory (/tmp/../* is the root, ~/../* holds the home). Each is now read the way bash reads it, so it blocks. A here-document whose body a substitution on the same line postpones is read where all three shells read it, and one that a substitution opens and never reads refuses the line. The stated over-blocks are rare and listed in the guard chapter; the residuals (a default's own word, an alternative nested more than three deep, ${PWD:0:1}, and three .. shapes the text cannot decide) are named in 17-guard.md and commands/guard.md and deferred past v0.11.1. Evidence: 249, 40, 144 and 129 new or extended subtests, each block pin watched fail at the base; verdict diffs over 127,805 and 150,497 inputs (every tightening under the home-or-root entry; the only loosenings are lines every shell reads as a document's text); two 3-minute fuzz runs with no panic.

race17 (concurrent-config race, on the integration branch). The Linux check leg of this pull request failed TestConcurrentConnectsKeepEveryKeyAndBlock: one ahoy connect refused ~/.abcd/config.json as "replaced between its vetting and its read" because another connect had just rewritten it. The earlier fix for this race (iss-2609290518278152) had gone into the path-based declaration read, which no reader of a home declaration calls any more; the descriptor-based read they all use still refused a replacement on sight. That read now judges a file renamed into place after its check from scratch, up to eight times, and reads it only if it passes every check again (a regular file, owned by you, writable by nobody else, and inside the reader's own mode rule). A symlink, FIFO, folder, group-writable, foreign-owned or too-open replacement is still refused by the check it fails, and a file that keeps changing is still refused as swapped. Evidence: eight new tests and subtests, seven of them watched fail before the fix (the symlink case was refused either way, and still is).

Reviews: fsVet SHIP (security review). drainObs SHIP. triageMajorA SHIP. triageMajorB SHIP. drainEcho4 SHIP (its fix round then closed the review's one low finding and one note). drainRest SHIP. guardResid SHIP (security verification). audits12 is records only.

Integration. The first four lanes merged in order with no conflict. Main (#747) was then merged in: internal/core/rules/root.go and internal/core/history/location.go conflicted, because fsVet added its new refusal to a switch that main had replaced with one shared function, fsutil.HomeDeclarationNames. Main's call is kept in both files, and the new refusal joins the symlinked-folder arm of that shared function, which merged without a textual conflict but would otherwise have fallen to its default wording; a new test holds it, watched fail without the arm. drainEcho4, audits12, drainRest and guardResid then merged with no conflict. Build, vet and each lane's touched tests passed after every merge, under the local toolchain and the one go.mod declares, and the generated CLI reference and surface appendix were regenerated at the merged tip and match it. The decision log carries main's entries first, then this branch's two 2026-09-29 entries at the end: triageMajorB's, and one recording the user's ruling that an autonomous run keeps at most five sub-agents alive, superseding the ceiling of four. The reading windows were measured again at the merged tip: widening (1,358,172 tokens) and detection (1,367,208) had fallen under 1% headroom and move to 1,380,000 and 1,390,000; entailment (392,765) keeps 400,000.

One record this build does not settle: iss-2609281134544802 (a variable's carried value is not read by the shell guard, major) stays in open/ with its deferral against v0.11.0, which lapsed when v0.11.1 was cut. guardResid did not carry it; renewing the deferral belongs to a later lane, and until that lands the next release cut refuses on it.

Resolves: iss-110
Resolves: iss-2608221254566264
Resolves: iss-2608231120121681
Resolves: iss-2608271711539855
Resolves: iss-2608271804497247
Resolves: iss-2608271804499169
Resolves: iss-2608311039531552
Resolves: iss-2608311949421873
Resolves: iss-2609012043432648
Resolves: iss-2609091955574760
Resolves: iss-2609100506256173
Resolves: iss-2609240646538011
Resolves: iss-2609290656480443
Resolves: iss-2609290656491358
Resolves: iss-2609290541525428
Resolves: iss-2609290551363398
Resolves: iss-2609290743362554
Resolves: iss-2608291814578333
Resolves: iss-2609100508566033
Resolves: iss-2608230957104179
Resolves: iss-2608261437042674
Resolves: iss-2608310912206749
Resolves: iss-2608210923438110
Resolves: iss-2609290419119456
Resolves: iss-2609290521415701
Resolves: iss-2609290625381759
Resolves: iss-2609290625482831
Resolves: iss-2609290745243990
Resolves: iss-2609291157309818
Refs: iss-2609290703091174
Refs: iss-371
Refs: iss-2609260932372448
Refs: iss-2609290630234596
Refs: iss-2609250834251447
Refs: iss-2609231050273096
Refs: iss-2609091717146700
Refs: iss-2609100506269348
Refs: iss-2608290820473197
Refs: iss-2608231607594913
Refs: iss-2608220150157503
Refs: iss-2608260941298050
Refs: iss-124
Refs: iss-193
Refs: iss-209
Refs: iss-211
Refs: iss-213
Refs: iss-2608210932052003
Refs: iss-2608210934566224
Refs: iss-2608230847432285
Refs: iss-2608230847432286
Refs: iss-2608231000561060
Refs: iss-2608241612007530
Refs: iss-2608250844259345
Refs: iss-2608290822140563
Refs: iss-2608290956522870
Refs: iss-2609012313465609
Refs: iss-2609020716570699
Refs: iss-2609091256264547
Refs: iss-2609091956001547
Refs: iss-2609100505146979
Refs: iss-2609100507439414
Refs: iss-2609100519122086
Refs: iss-2609211105023379
Refs: iss-92
Refs: iss-160
Refs: iss-216
Refs: iss-2608291814562032
Refs: iss-2608210934566220
Refs: iss-2608220750029985
Refs: iss-2608282026177429
Refs: iss-2609012111162089
Refs: iss-2609201954342967
Refs: iss-2609212142568782
Refs: iss-2609252055532027
Refs: iss-2609262011091645
Refs: iss-2609290426544292
Refs: iss-2609290825240166
Refs: iss-2609290825319136
Refs: iss-2609281654467661
Refs: iss-2609281134544802
Refs: iss-2609251455354719
Refs: iss-2609290518278152

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

…rd allows

A backslash-newline inside the variable's name, a variable inside a brace
expansion, and a parameter expansion of HOME with an operator each reach a
recursive delete of the home that rm-rf-root-or-home allows.

Refs: iss-2609290419119456
Assisted-by: Claude:claude-opus-5-5
…rators

rm-rf-root-or-home compares a delete's target with the words it names as the
line wrote its variables. Three spellings of the home reached that compare as
no word it names, and allowed:

- a backslash-newline inside the name (`rm -rf $HO\<newline>ME`): bash drops
  it before it reads the name, and simpleParamEnd now runs the name on across
  it, so the spelling is `$HOME`, not `${HO}ME`;
- a variable inside a brace group (`{$HOME,x}`, `$HOME/{.*,}`, `$HO{ME,}`):
  bash expands the group before it reads the variable, and each word the
  expander makes now keeps its variables' sites (bword.s), so it is spelled
  as bash reads it, a bare name running on into the group's unquoted text;
- a parameter expansion with an operator that can leave the value as it is
  (`${HOME%/}`, `${HOME:-x}`, `${HOME#}`, `${HOME/x/x}`, a substring, a case
  change, a subscript), and an alternative whose word is one of these
  (`${X:+$HOME}`): spellParameter spells each as `${NAME}`.

Judgement taken: "can be the home" decides, so an expansion whose pattern
might not match blocks, and a suffix trim that leaves the path above the home
(`${HOME%/*}`) blocks too. Stated over-blocks, all rare: `${HOME#/}` (a
relative path), `$HO''{ME,}`, and `sh -c "rm -rf $HO{ME,}"`, where the outer
shell has already expanded `$HO`. Named as residuals in 17-guard.md and
commands/guard.md: a default's own word (`${DIR:-$HOME}`), an alternative
nested more than three deep, and `${PWD:0:1}` (the root, read as `$PWD`).

Verdict diff over 12,595 pre-existing inputs (every guard test literal, both
corpora and every bundled fixture, bare, in sh -c "..." and in bash -c '...'),
base 8cd7f88 against this tree: 6 differences, all tightening, all the two
residual pins this change flips (`{$HOME,x}`, `${HOME:-/}`).

Refs: iss-2609290419119456
Assisted-by: Claude:claude-opus-5-5
rm -rf ${DIR:-$HOME} deletes the home when DIR is unset and allows: a word's
written spelling holds one text, and a default can print two. Named in
17-guard.md's residuals and deferred past v0.11.1 with what is owed.

Refs: iss-2609290426544292
Assisted-by: Claude:claude-opus-5-5
…spellings

resolved_by is ad44726, which spells a backslash-newline inside a name, a
brace group's words and a value-keeping parameter expansion as bash reads them.

Resolves: iss-2609290419119456
Assisted-by: Claude:claude-opus-5-5
The operator switch in spellParameterAt compared a byte with the tilde, which
the fence-run detector (TestNoFenceRunReaderOutsideMdrecord) reads as a
markdown fence reader outside mdrecord. The operators are one string now, and
the reading is unchanged.

Refs: iss-2609290419119456
Assisted-by: Claude:claude-opus-5-5
… pending here-document

cat <<E <(x, cat <<E $(x and cat <<E with an open backtick, each followed by
a newline and E, panic flushSegment with an index out of range: the newline
inside the substitution resets docOwners while the enclosing command's
curDocs still indexes it. The hook fails closed on the panic's exit 2, and
abcd guard check prints a stack trace.

Refs: iss-2609290521415701
Assisted-by: Claude:claude-opus-5-5
…ative's word

Three spellings inside the mechanisms iss-2609290419119456 reads still
deleted the home and allowed (review-guardResid):

- A subscript was cut at its first `]`, so `${HOME[x[0]]}`, `${HOME[0]]}`
  and `${HOME[a]]}` kept their raw text. It is read to its matching `]` now,
  and whatever follows it but an alternative spells as the variable: the
  bash 3.2 of macOS (its /bin/sh and /bin/bash) prints the value past
  `${HOME[0]]}`, `${HOME[0]x}` and `${HOME[0]@q}`. A subscript whose `]`
  never comes spells as the variable too, on the side of the block.
- A sequence expression's terms carried no wordStruct flag, so a bare name
  did not run on into them: `$HO{M..M}E`, `$HOM{E..E}`, `$H{O..O}ME` and
  `$HO{M..N}E` spelled `${HO}ME`. A term's letters, digits and underscores
  are unquoted name bytes now; no other byte is flagged, so `[` from
  `{Z..a}` is never a glob.
- An alternative's word was spelled only where it was one expansion. An
  alternative prints its word or nothing, one text, so the word is spelled
  as written through its own expansions: `${X:+/}` is `/`, `${X:+~}` is `~`,
  `${X:+$HOME/*}` is `$HOME/*`, and `${X:+"$HOME"/}` is `$HOME/`. Only a
  simple name is braced where name bytes follow it, so a word spelled that
  way is never rewritten. `${X:+x}` still allows (its pin is unchanged).

Over-blocks, stated: `${HOME[0]@q}` blocks although bash 5 quotes the value,
and a quoted `~` or a backslash inside an alternative's word spells as the
unquoted text. homeresiduals_test.go pins each spelling bare, in bash -c and
(where the outer shell leaves it) in sh -c, plus six allow pins, and three
new cost shapes stay linear under the unchanged bar.

Refs: iss-2609290419119456
Assisted-by: Claude:claude-opus-5-5
cat <<E <(x, cat <<E $(x and cat <<E with an open backtick, each followed by
a newline and E, panicked with an index out of range. The newline inside the
substitution reads the pending bodies and clears docOwners, while the
suspended command that opened them kept their indices in its saved curDocs;
resumed at the end of the input, flushing it indexed the cleared slice.
Reading the bodies now clears that record in every frame a substitution
suspends, since the documents it names are read. The line is read as its
sibling without the document is: `rm -rf / <<E $(x` blocks, `cat <<E $(x`
allows as `cat $(x` does.

Refs: iss-2609290521415701
Assisted-by: Claude:claude-opus-5-5
…ment in an open substitution

resolved_by is 627a73a, which clears the suspended command's record of the
documents whose bodies were read inside the substitution.

Resolves: iss-2609290521415701
Assisted-by: Claude:claude-opus-5-5
The resolution named the three mechanisms without the parts 1de705c adds
inside them: a name running on into a sequence's letters, a subscript read
to its matching bracket, and an alternative read as its word as written. It
now states each, and the pin count is 193.

Refs: iss-2609290419119456
Assisted-by: Claude:claude-opus-5-5
…ed token

The drainEcho3 security review found that scanner.RedactRefusal leaves a
token raw when it sits right after an underscore or a letter: the
scanner's patterns anchor on a leading word boundary.

Refs: iss-2609290541525428

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

spellAlternative (1de705c) steps an alternative's shell word and stops at
a backtick, which opens a command substitution and leaves the word
unspelled. That is shell grammar, not a markdown code span, so the file is
named with its one scan and the reason, as tokenize.go is.

Refs: iss-2609290419119456
Assisted-by: Claude:claude-opus-5-5
Every bundled secret pattern opens on a leading \b, and '_', a letter and
a digit are word characters, so a token glued behind one (a key spelled
notes_<token>, a path spelled x<token>y) was never matched and came back
from a refusal raw, through every RedactRefusal caller.

The primitive now adds a glued sweep: the hard_fail secret patterns that
open on \b are recompiled without it and run once through the scanner's
own adjacency machinery, so every start position a suffix sweep would
try is tried in one linear pass. Its findings are deduplicated with
ScanText's and sealed by Redact, so a bounded token keeps the
fingerprint it had. The primitive still fails closed: on a degraded
scanner, on a sweep it cannot build, and now on a secret span that
survives Redact. ScanText itself is unchanged.

Refs: iss-2609290541525428

Assisted-by: Claude:claude-opus-5-5
The drainEcho3 security review's INFO: the launch scan and the memory
writer's page-name bar share RedactRefusal's blind spot, since ScanText's
secret patterns open on a leading word boundary. The record carries the
measurement taken before deciding to fix it in the scanner.

Refs: iss-2609290551363398

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

The glued sweep moves from RedactRefusal into scanText, so the launch
scan, the memory writer's page-name bar and every store-before-commit
redactor find a token right behind a letter, a digit or an underscore,
at its own byte span. The sweep is built once per scan (gluedSweep),
runs the hard_fail secret patterns that open on \b without that anchor
through scanAllPatterns, and applies each pattern's Skip and SkipAt; a
span the bounded pass already holds is deduplicated. RedactRefusal now
reads ScanText alone and fails closed when the sweep cannot be built
whole.

Measured before deciding: over the repository's 4605 tracked text files
the sweep adds 0 findings to the 13 hard_fail secret findings, the launch
dry-run's count stays 1, and it costs 7 to 10 per cent of the bounded
scan's time; the package's linear-cost guard still passes.

Refs: iss-2609290551363398

Assisted-by: Claude:claude-opus-5-5
…d token

Resolves: iss-2609290541525428

Assisted-by: Claude:claude-opus-5-5
Resolves: iss-2609290551363398

Assisted-by: Claude:claude-opus-5-5
…status render

The bare `abcd reading` render listed the assembly parking area and the
ingest stage with a plain os.ReadDir on a joined path. A clone that commits
`.abcd/.work.local`, or either listed directory, as a symlink out of the
checkout had the render echo run-id-shaped names from the far end into
`staged_runs` and `orphaned_ingests`. The sweep that deletes from the same
stage already lists it through the root and refuses a linked directory, so
the read side now does the same: both listings go through the one os.Root
the render already opened for its commit-marker probes, via readDirIn, and
a listing that would leave the checkout refuses the render.

Refs: iss-2609012043432648
Assisted-by: Claude:claude-opus-5-5
…ing it

frontmatter.Unquote takes a scalar's INNER text, so every reader holding a
raw, possibly double-quoted value strips the pair first. The ledger's reader
(issuerecord), record-lint's schema gate and the cold-reading definition
locator each kept a private copy of that strip, and a caller that forgot it
refused well-formed records with a message comparing a value against itself.

frontmatter.UnquoteScalar is the strip beside the decoder: a value that opens
and closes with a double quote comes back decoded with quoted=true, anything
else unchanged with quoted=false, so the ledger reader that reads a bare value
as a number branches on the flag instead of re-testing the quotes. The three
call sites and the ledger reader's quotedScalar predicate move onto it; it
trims nothing, as none of them relied on it trimming. The changelog gate's
scalar is left alone: it strips either quote kind on purpose and says why.

Refs: iss-2608311039531552
Assisted-by: Claude:claude-opus-5-5
The include table's two Match forms compared case differently with no
stated reason — an extension with strings.EqualFold, an exact basename with
== — so `.MD` matched while `makefile` did not match `Makefile`, and whoever
added a fourth form had no rule to follow.

The rule is now written on Row.Match and restated where matches applies it:
a form that names a KIND of file folds case (an extension), a form that
names one FILE matches the spelling the repository commits (a basename; a
row wanting another spelling lists it), and a form that follows a tool's
own rule keeps that rule (MatchSuffix, the Go toolchain's lowercase
_test.go). A new form states which of the three it is. No compare changes,
so nothing the assembler admits moves and the assembler version stands;
TestTheMatchFormsFollowTheOneCaseRule pins each form to the stated rule.

Refs: iss-2608311949421873
Assisted-by: Claude:claude-opus-5-5
A record id written in prose is checked by prose_citation_resolves, but the
rule read the ten record stores alone, so an id in the brief, a principle,
the roadmap, a plan or a research note was judged by no gate. The rule now
honours extra_roots — a directory or one file, held inside the repository
and refused when absent, exactly as links_resolve holds its own — and reads
each for prose the way it reads a store; the write-path check a verb makes
before it files text agrees. The shipped config names .abcd/development
whole, so a file a store already covers is read once.

The first run over the widened set found one unresolvable id, spc-82, cited
by the retired predecessor store's own triage note; it joins the baseline
beside its never-minted siblings spc-74..spc-83. The lint chapter says what
the rule now reads.

This widens the existing record-lint gate rather than the site export's
reference extraction the record proposed: record-lint already owns prose
citations through the one resolver, so the export stays on typed edges and
no second resolver is added.

Refs: iss-2608271804497247
Assisted-by: Claude:claude-opus-5-5
The binary reads two per-repository files under .abcd/config/ that this
checkout does not carry — pii.json, the redaction scanner's pattern
override, and scripts-closure.json, the pinned scripts/ closure the launch
payload applies — and no document named either. The .abcd/README.md index
now lists both as optional, with what each does when absent and where its
schema is stated, beside the config/ members it already listed plus the two
it had missed (artefact.json, reading-presets.json). The root table also
gains prose-citations-baseline.json, the one tracked root file it did not
index.

Refs: iss-2608271804499169
Assisted-by: Claude:claude-opus-5-5
…env var

harness/claude-code matched the product name only as a phrase, so the
install page named the host twice with no finding: a link to the plugin's
manifest directory and the host's per-plugin data variable. The pattern now
also matches that dotted directory and an upper-case environment variable
prefixed with the host's name, staying quiet on a documentation host inside
a URL and on a lowercase identifier. harness/codex and harness/gemini
already caught a path (a dot is a word boundary) but not an environment
variable, where an underscore joins the name to the rest; both gain the
same arm.

Watched RED first (TestDocsLintHarnessNameGateReachesPathsAndEnvVars, all
five cases, against the old config), and watched the widened rule fire on
both install page sites. The page is then rephrased in generic terms rather
than given a per-line allow marker: whether install pages are a sanctioned
place to name a host is the ruling iss-216 still owes, and this change does
not pre-empt it.

Refs: iss-2608271711539855
Refs: iss-216
Assisted-by: Claude:claude-opus-5-5
.gitattributes gives CHANGELOG.md and .abcd/work/DECISIONS.md the union
merge driver, and nothing told an author it stops at the local clone. The
forge computes a pull request's mergeability and the merge queue's merge
without it, so two open pull requests that each append to one of those files
conflict there as soon as the first merges, while a local merge of the same
two is clean — every records pull request of autonomous run A went dirty
that way. The attributes file and the one-writer-per-file principle, the
convention that names the attribute as a remedy, now say so, and point at
the remedy that removes the conflict on the forge too: one file per entry,
already accepted as adr-2609151138420062. The changelog comment also stops
claiming that pull requests append to [Unreleased], which record-lint now
refuses.

Refs: iss-2609240646538011
Assisted-by: Claude:claude-opus-5-5
The configuration chapter lists disembark.maxAgentTokens under its staged
keys, which no shipped code reads, but the meta chapter still cited it as a
budget in force. It now names it as the staged key it is.

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

A here-document a substitution opens and never reads stays pending after
the close in the guard's reading, while bash 3.2 and /bin/sh run the lines
it would cover. And a root or home operand written with repeated slashes,
a /./ segment or a /../ segment under the root is compared as an exact word
and allows. Both are present at main.

Refs: iss-2609290625381759
Refs: iss-2609290625482831
Assisted-by: Claude:claude-opus-5-5
bash 3.2, bash 5 and /bin/sh read no pending document's body at a newline
inside a substitution that opened after it: the lines run inside the
substitution, and the body begins on the line after it closes. 627a73a
read the bodies there instead, so `cat <<E $(x`, `rm -rf ~`, `E`, `)`
allowed where its sibling without the document blocks. The pending
documents are now saved with the substitution's frame, a newline inside
reads only the documents the substitution opened, and the close restores
the rest.

A document the substitution opens and never reads is pending after the
close in bash 5 and dropped in bash 3.2 and /bin/sh, which run the lines
it would cover, so the line blocks fail-closed as an unterminated
document. heredoc_test.go's arithmetic pin moves from allow to block.

Refs: iss-2609290521415701
Refs: iss-2609290625381759
Assisted-by: Claude:claude-opus-5-5
… loader's root

A harness registers every markdown file at the top of agents/ as an agent,
with no frontmatter requirement and no name exemption, so agents/README.md
and agents/CHANGELOG.md were listed as abcd:README and abcd:CHANGELOG agents
on every installed surface — workers nothing can dispatch, and names a real
agent could not take. The loader is the host's, so the fix is where the
files live, as it was for commands/README.md (iss-160).

Both move, unchanged in substance, to .abcd/development/agents/: the
operator statement of the prompt contract and the itd-5 prompt-version log,
durable record rather than plugin payload. record-lint's agent_contract
reads the log from its configured `changelog` path, which follows it; the
prompt-quality chapter, the surfaces chapter, itd-5's rule text, the
Makefile note and the code comments that named the old paths follow too,
and the development index gains the folder. Dated plans, research notes and
closed records keep the paths they were written against.

TestPluginAgentSurfaceRegistersOnlyAgents is the detector, the agent half
of the iss-160 command-surface test: every markdown file at the top of
agents/ must open a frontmatter block naming itself after its file. Watched
RED on the two files before the move and GREEN after.

Refs: iss-110
Refs: iss-160
Assisted-by: Claude:claude-opus-5-5
bash 3.2, the /bin/sh and /bin/bash of macOS, steps over any text after a
subscript's closing bracket to the first operator byte, and a `+` or `:+`
there is an alternative: `${X[0]]:+$HOME}`, `${PATH[0]]:+$HOME}`,
`${X[0]x:+$HOME}` and `${X[0]]]:+$HOME}` print the home. Only a leading
`+` or `:+` was read, so those allowed. A `-`, `=`, `?`, `%`, `#`, `/`, a
lone `:` or a backslash first still spells as the variable
(`${X[0]]-$HOME}` prints X's value).

Refs: iss-2609290419119456
Assisted-by: Claude:claude-opus-5-5
Both are open and minor, so the release cut does not require a deferral,
but each needs a ruling no implementer can take, and saying so on the record
is what keeps it from reading as forgotten. The persona-role check fails
existing quotes whichever way it is built, so what happens to them is a
roster decision; the spec-step marker changes the build loop's rule, as the
record itself says. Each carries deferred_after v0.11.1 and the ruling it
waits on.

Refs: iss-371
Refs: iss-2609260932372448
Assisted-by: Claude:claude-opus-5-5
The subscriptOperators comment and the `${X[0]]-$HOME}` allow pin said the
form prints X's value. That holds only with X set: with X unset, bash 3.2
and /bin/sh print the word for `${X[0]]-$HOME}`, `${X[0]]:-$HOME}`,
`${X[0]]=$HOME}`, `${X[0]]:=$HOME}` and `${X[0]]-/}` (checked with echo;
bash 5 refuses each as a bad substitution). That is the default's-word
class the open record defers, so the comment, the pin and 17-guard.md's
residuals now say so, and the record carries the subscript forms as
evidence. No behaviour changes.

Refs: iss-2609290426544292
Assisted-by: Claude:claude-opus-5-5
…ory reaches the root or the home

Refs: iss-2609290745243990
Assisted-by: Claude:claude-opus-5-5
…aded

ScanText carried no signal when the glued sweep was incomplete: a
configured secret pattern whose leading \b carries a quantifier has no
boundary-free form, the sweep ran without it, and only RedactRefusal read
the sweep's completeness. New now folds the gap into the scanner's
degraded state, with a reason naming each pattern it could not build, so
Unavailable says it and every write-time redactor, the launch scan
(ScanBundle) and RedactRefusal fail closed on it, the way they already do
on a bad override regex. RedactRefusal drops its own rebuild of the sweep
and reads Unavailable alone. A configured pattern with a plain leading \b
leaves the scanner available.

Refs: iss-2609290743362554

Assisted-by: Claude:claude-opus-5-5
… or the home

cleanSeparators read a `..` only directly under the root, so `rm -rf
~/../*` (the home's parent globbed, which holds the home), `~/../../*`,
`$HOME/../../*`, `/tmp/../*` and `/etc/../*` allowed, bare and in `sh -c`.

foldParents folds each `..` into the segment before it where the target
begins at the root or the home, as the path reads lexically. At the root a
`..` stays at the root. Past the home it climbs to a directory that holds
the home, so the path is the home where each segment it then descends
through is `*` (`~/../*` and `~/..` read as `~`, `~/../*/*` as `~/*`), and
`~/../x` stays a sibling. The kernel reads `..` otherwise only after a
symlink; the lexical reading is the one that blocks. A trailing `..` is
folded too, though rm refuses it. Nothing is folded across a segment that
holds a variable or a substitution, whose directory count is not known.

17-guard.md and commands/guard.md say so and name the residuals: a `..`
after a symlink or after a variable's segment, and `~/../?*` beside `/?*`.

Refs: iss-2609290745243990
Assisted-by: Claude:claude-opus-5-5
…ory reaches the root or the home

Resolves: iss-2609290745243990
Assisted-by: Claude:claude-opus-5-5
…ed views

Resolves: iss-2609290743362554

Assisted-by: Claude:claude-opus-5-5
A `~/x/…/../…/*` and a `/tmp/a/../…*` operand grow with the input as the
other home spellings do; workPerByteBar stays 24.

Refs: iss-2609290745243990
Assisted-by: Claude:claude-opus-5-5
termsafe's TestNoSecondCodeSpanPairer refuses a backtick scan outside
termsafe. literalSegment needs none: a substitution, backtick or `$(…)`,
is spelled as a mark below 0x20, and a backtick left in the text is a
quoted name byte. `~/`x`/../*` still blocks under the vanish reading.

Refs: iss-2609290745243990
Assisted-by: Claude:claude-opus-5-5
…ts verb

The sentence added with the next-move change quoted the spec close verb's
spelling in the chapter's hand-written prose, where shape is not stated
(itd-147 ac-5, held by TestSurfaceChapterProseStatesNoShape); the verb's
spelling lives in the generated appendix. The sentence now says the move
is closing the spec.

Refs: iss-2609100508566033
Assisted-by: Claude:claude-opus-5-5
Refs: iss-2609290656480443, iss-2609290656491358, iss-2609290703091174

Assisted-by: Claude:claude-opus-5-5
Refs: iss-110, iss-2608221254566264, iss-2608271711539855, iss-2608271804497247, iss-2608271804499169, iss-2608311039531552, iss-2608311949421873, iss-2609012043432648, iss-2609240646538011, iss-2609260932372448, iss-2609290630234596, iss-371

Assisted-by: Claude:claude-opus-5-5
Refs: iss-2608220150157503, iss-2608231607594913, iss-2608260941298050, iss-2608290820473197, iss-2609091717146700, iss-2609091955574760, iss-2609100506269348, iss-2609231050273096, iss-2609250834251447

Assisted-by: Claude:claude-opus-5-5
Refs: iss-124, iss-193, iss-209, iss-211, iss-213, iss-2608210932052003, iss-2608210934566224, iss-2608230847432285, iss-2608230847432286, iss-2608231000561060, iss-2608231120121681, iss-2608241612007530, iss-2608250844259345, iss-2608290822140563, iss-2608290956522870, iss-2609012313465609, iss-2609020716570699, iss-2609091256264547, iss-2609091956001547, iss-2609100505146979, iss-2609100506256173, iss-2609100507439414, iss-2609100519122086, iss-2609211105023379, iss-92

Assisted-by: Claude:claude-opus-5-5
Fidelity review of the credential store intent (receipt rcp-ebf7d171b544),
re-emitted at the integration tip and ingested from the audits12 worktree:
five criteria, MET 1 and MET_WITH_CONCERNS 4, none NOT_MET; the one scope
condition narrowed, because the keychain home has only run against the test
binary's fake (iss-2609281654467661 is the owed real round trip).

Two divergences of substance are captured, not fixed:

- ac-3: the record says the write path runs the scanner and refuses a
  tracked-path write; the store scans the index alone and refuses only the
  abcd home's value write inside a working tree (ruled at review in
  278e266), and the record does not say so.
- ac-2: the spec and the press release say the CLI asks for the home; the
  CLI takes it as a --home flag and only the plugin page asks.

The unwired provider call (APIConfig.Call has no production caller, so the
route receipt's provider_call is never produced) is not captured: call.go
declares it as spc-2609251028149555's scope.

Refs: itd-2609221017023290
Refs: iss-2609290825240166
Refs: iss-2609290825319136
Refs: iss-2609281654467661
Assisted-by: Claude:claude-fable-5-1
main carries integ16 (drainBugs, drainSec, drainUx, drainDrift2,
drainEcho..drainEcho3, drainDrift3, drainDebt).

Conflicts, resolved by hunk:
- internal/core/rules/root.go and internal/core/history/location.go: this
  branch (fsVet) added DeclarationDirectoryExposed to the refusal switch
  beside DeclarationBehindSymlink, while main (drainDebt) replaced that
  switch with a call to fsutil.HomeDeclarationNames. Main's call is taken
  in both files.

Semantic collision, fixed in this merge:
- internal/fsutil/home.go auto-merged textually, but HomeDeclarationNames'
  switch did not know fsVet's new refusal, so an exposed ~/.abcd fell to
  the default arm and read "it could not be read (...)" around the
  refusal's own clause. DeclarationDirectoryExposed joins the
  DeclarationBehindSymlink arm, which carries the clause as written.
  New test TestHomeDeclarationNamesNamesTheExposedDirectory
  (internal/fsutil/home_scope_mode_test.go) holds it; watched red on a
  scratch copy without the arm, green with it.

DECISIONS.md: triageMajorB's 2026-09-29 entry was auto-merged above
main's last line; it is moved to EOF so the diff from main is +1/-0.
commands.md and surface.json regenerated; release files are main's and
## [Unreleased] is empty.

Assisted-by: Claude:claude-opus-5-5
The secret scanner and the refusal redactor find a token glued behind a
word character, the glued sweep also runs over the decoded views, and a
sweep that cannot be built whole reports degraded. Stacked on the echo
chain main now carries, so only its own ten commits are new. Merged
cleanly; build, vet and the scanner, memory, history, capture and
implement/loop tests pass; go generate produced no drift.

Refs: iss-2609290541525428, iss-2609290551363398, iss-2609290743362554
Assisted-by: Claude:claude-opus-5-5
Records only: the fidelity audit of itd-2609221017023290 (receipt
rcp-ebf7d171b544; MET 1, MET_WITH_CONCERNS 4, NOT_MET 0) lands in the
intent's Audit Notes, and two drift captures (ac-3, ac-2) enter open/,
where they stay. Merged cleanly; no Go changed.

Refs: itd-2609221017023290
Refs: iss-2609290825240166, iss-2609290825319136, iss-2609281654467661
Assisted-by: Claude:claude-opus-5-5
Drains fifteen open records: six fixed and resolved, nine re-deferred
past v0.11.1 with the ruling each owes. Every config-named repo path
refuses the git directory, the record dispatcher says the spec close
ships the intent, AGENTS.md states scan-before-mutating by blast radius,
and the bootstrap hook's network refusal names the environment it
ignores.

Conflict, resolved by hunk:
- itd-5 prompt-quality-additions, the pre-flight steps: this lane
  rewrote steps 2-3 (the calibration corpus is the gate, length is no
  tiebreak) and still named agents/CHANGELOG.md in step 4, while this
  branch (drainObs, d5091da) had already renamed step 4's target to
  "the prompt-version log" when it moved that file. Kept the lane's
  steps 2-3 and this branch's step 4.

Build, vet and the lint, positioning, record and cli tests pass;
go generate produced no drift.

Refs: iss-2608291814578333, iss-2609100508566033, iss-2608230957104179
Refs: iss-2608261437042674, iss-2608310912206749, iss-2608210923438110
Refs: iss-2608291814562032, iss-2608210934566220, iss-2608220750029985
Refs: iss-2608282026177429, iss-2609012111162089, iss-2609201954342967
Refs: iss-2609212142568782, iss-2609252055532027, iss-2609262011091645
Assisted-by: Claude:claude-opus-5-5
The shell guard reads more spellings of the home and the root the way
bash does, so a recursive delete through them blocks: a backslash-newline
inside the name, a brace group's words, an expansion that can leave the
value as it is, an alternative's word, redundant separators, and a ".."
folded into the directory before it. A pending here-document waits out a
substitution opened on its line. Merged cleanly beside triageMajorA's and
main's guard prose in commands/guard.md and 17-guard.md; build, vet and
the guard and termsafe tests pass; go generate produced no drift.

iss-2609281134544802 is neither resolved nor re-deferred by this lane:
it stays in open/ with its lapsed deferred_after v0.11.0. Its renewal is
lane fix5-guardGlob's, which is not in this build.

Refs: iss-2609290419119456, iss-2609290521415701, iss-2609290625381759
Refs: iss-2609290625482831, iss-2609290745243990, iss-2609290426544292
Refs: iss-2609281134544802
Assisted-by: Claude:claude-opus-5-5
The user ruled directly to the run A orchestrator at 06:53Z, "use up to
five sub-agents from now on". The entry supersedes the four-agent
ceiling of 2026-09-24; Fable reviews and audits stay one at a time.

Assisted-by: Claude:claude-opus-5-5
Dry-run assemble on a clean clone of 8fdf0d0: widening measures
1,358,172 tokens / 5,228,966 bytes, which left the 1,370,000 window
0.87% headroom, so it moves to 1,380,000; detection measures
1,367,208 tokens / 5,263,754 bytes, 0.93% under 1,380,000, so it moves
to 1,390,000. Entailment (392,765, 1.84%) keeps its window and figures.

Refs: iss-2609251455354719
Assisted-by: Claude:claude-opus-5-5
@REPPL
REPPL enabled auto-merge September 29, 2026 11:17
… refuses a concurrent rewrite

The Linux check leg of PR 748 failed TestConcurrentConnectsKeepEveryKeyAndBlock
with ErrDeclarationSwapped from ~/.abcd/config.json: the re-vetting fix for
iss-2609290518278152 lives in the path-based ReadDeclaration, while every
home-scoped reader goes through the descriptor-based readDeclarationIn,
which still refuses a replacement on sight.

Refs: iss-2609291157309818, iss-2609290518278152
Assisted-by: Claude:claude-opus-5-5
… and its read

ReadHomeDeclaration (and ReadHomeDeclarationDenying) judge the leaf on a
root-relative Lstat, open it relative to the same directory descriptor, and
confirm with os.SameFile that the descriptor is the file judged. A file
renamed into place inside that window was refused on sight with
ErrDeclarationSwapped, so the ordinary rewrite a concurrent abcd makes
through WriteFileAtomic under the writers' lock made an unlocked reader
(layered.Load via oracle.LoadAPI) refuse its own ~/.abcd/config.json.

iss-2609290518278152 fixed this in b342b2b, but in ReadDeclaration, the
path read; the integration merge that landed it (24e7850) kept
a07ad67's descriptor read, readDeclarationIn, which every home-scoped
reader calls and which had no retry. The fix never reached a production
reader.

readDeclarationIn now loops over one vetting and read
(readDeclarationInOnce) up to declarationAttempts (8) times, re-running on
ErrDeclarationSwapped only. Every attempt runs every guard from scratch:
the Lstat (regular file), CallersAlone (no group/other write, owned by this
uid), the O_NOFOLLOW open, the descriptor's regular-file and SameFile check,
the descriptor's uid, and the caller's deny mask on the descriptor's mode.
The bytes returned are always those of a descriptor os.SameFile ties to an
Lstat that passed every guard. A replacement that fails a guard is refused
by that guard; one still unsettled after the bound is refused as a swap.
The directory walk (openHomeScope) is unchanged: a directory level replaced
while it is opened is still refused.

Refs: iss-2609291157309818
Refs: iss-2609290518278152
Assisted-by: Claude:claude-opus-5-5
…dges a benign replacement

The fix is ecd41fd: readDeclarationIn, behind every home-scoped
declaration read, judges a leaf renamed into place after its vetting from
scratch, a bounded number of times, rather than refusing it on sight, so the
re-vetting iss-2609290518278152 intended now reaches the readers that use it.

Resolves: iss-2609291157309818
Refs: iss-2609290518278152
Assisted-by: Claude:claude-opus-5-5
@REPPL REPPL changed the title fix: close 28 records: declarations judged folder by folder, home-deletion guard residuals, and a credential-store audit fix: close 29 records: declarations judged folder by folder, a benign config replace re-judged, home-deletion guard residuals, and a credential-store audit Sep 29, 2026
@REPPL
REPPL added this pull request to the merge queue Sep 29, 2026
Merged via the queue into main with commit e792a23 Sep 29, 2026
13 of 14 checks passed
@REPPL
REPPL deleted the integ/land-17 branch September 29, 2026 14:20
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