Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion .abcd/development/brief/04-surfaces/17-guard.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,10 +285,26 @@ than one worktree, a stash or pop that does not name its entry is warned about,
because the stash stack is shared across worktrees. Where the reading is a
guess, over-blocking is the direction the guard takes.

A recursive delete is read by what it deletes. Of the filesystem root or the
home directory (`/`, `/*`, `~`, `$HOME`, `${HOME}`, each also with a trailing
`/` or `/*`, and the home's dotfiles `~/.*`, `$HOME/.*`, `${HOME}/.*`) it is a
block wherever it stands, with or without `-f`. Of the directory the shell is
in or the one above it (`*`, `*/`, `.`, `..`, `./*`, `./*/`, `../*`, `.*`,
`./.*`, and `$PWD` or `${PWD}`, each also with `/*`) it is a warn, graded like
`git clean`, because that directory is usually the repository and emptying a
build directory the same way is ordinary work. Chained after a `cd` any
recursive forced delete blocks, as above. The target is compared as written,
before the shell expands it, so `$HOME` and `$PWD` are seen as those words
although no other parameter expansion is.

What an allow still does not see is a hazard that never reaches command position
at all: a word that is wholly a command substitution or a variable standing
where a flag would be, which is read as an operand because that is how a commit
message or a branch name is spelled every day; one behind a wrapper flag the per-wrapper
message or a branch name is spelled every day; a delete target printed whole by a
substitution (`rm -rf $(echo /)`), which is read by its known text because that
is how an everyday delete names what it removes (`rm -rf $(find . -name
'*.pyc')`); a target spelled any other way than the words above (`rm -rf
"$DIR"/*` with `DIR` unset, `rm -rf /?*`); one behind a wrapper flag the per-wrapper
table does not name; a REST
path an entry names by its root segment when the host serves that API under a
prefix; an IFS the shell already holds when the line starts, or gains during the line
Expand Down
6 changes: 5 additions & 1 deletion .abcd/development/brief/04-surfaces/21-update.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,11 @@ origin, the tag, the asset and its digest, the target path (redacted to `~`), an
the ownership proof that allowed the swap. It carries `env_ignored` when proxy or
CA overrides were scrubbed. A refusal receipt is deliberately thinner: a refusal
raised before any fetch carries the target path and a block naming shape, detail
and remedy, and nothing else, because there is no release it could name.
and remedy, and nothing else, because there is no release it could name. In the
JSON form a refusal of either kind is one document on stdout: the receipt,
carrying the three fields of the global refusal envelope (`"abcd": "error"`,
`error`, `exit_code`) beside its own, so a reader expecting one document gets
the refusal whole rather than a receipt followed by a second envelope.

An old version number is only derivable when a release manifest dated the file it
replaced. A file swapped under either local proof has no published release naming
Expand Down
2 changes: 1 addition & 1 deletion .abcd/development/release/surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -2048,7 +2048,7 @@
"hidden": false,
"group": "checks",
"block": "people",
"sentence": "Check this repository against the conventions, every target included: Writes nothing; refuses with exit 2 on an error finding and exit 1 on warnings alone.",
"sentence": "Check this repository against the conventions, every target but outbound: Writes nothing; refuses with exit 2 on an error finding and exit 1 on warnings alone.",
"flags": [
{
"name": "root",
Expand Down
2 changes: 2 additions & 0 deletions .abcd/work/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2573,6 +2573,8 @@ together (the script's header says why there is no escape hatch).
- 2026-09-26 — The build loop's worktree store is keyed on the FULL root sha: a lane lives at `~/.abcd/worktrees/<root-sha>/<run-id>-<lane-id>` with the 40-hex root commit, the form the history, transcript and voyage stores use and the one the store's draft (itd-2609091014076309) specifies. The lanes of autonomous run A made by hand under the abbreviated key (`~/.abcd/worktrees/488a0aa9/<name>/`) are the pre-verb convention, not a second form of the store: the loop never reads or adopts a lane under that key, and those worktrees are retired with `git worktree remove` like any other (implementer of fix round fix2-loop2, autonomous run A, on item 4 of the loop2 review; spc-2609202134338445 piece 6).
- 2026-09-28 — Rulings Z, AR and the cancel policy, given by the user as technical facilitator at 15:10:37Z (autonomous run A, recorded by lane cap45 for orchestrator abcd-a8). (Z) The macOS leg of ci.yml's `check` job and the main ruleset's merge-queue `check_response_timeout_minutes` both rise from 30 to 45 minutes, because a 30-minute cap cancelled passing macOS runs (#728, #730 twice, #733; iss-2609281514435020). This amends the standing rule that never raises the check job's timeout or edits the ruleset, for exactly this one change: 45 minutes, those two settings; every other timeout, every required-check name or split and every other ruleset field stays as it is, and the ubuntu leg keeps 30. The workflow and the `.abcd/work/rulesets/main-protection.json` mirror change through a reviewed pull request; the live ruleset is changed with `gh api` by the run's orchestrator after that pull request merges, and until then the live queue still fails a group at 30 minutes. (AR) All three speed-ups of iss-2609261924541555 are built: a test-only switch that skips the disk flush in the atomic write code and the slowest -race package first in the race step (lane ciSpeed), and the scanner's per-identity git calls folded into one (lane scanFold), a trust path that is security-reviewed before it lands. (Cancel policy) The rerun-once rule stands: a check cancelled at the cap is rerun once, and a second cancellation for the same reason stops that pull request and opens a speed lane, never another raise of the timeout; and a step on the macOS leg warns, in the log and the step summary, once the check has run past 35 minutes, without ever failing the job, so a speed lane opens before any cancellation.
- 2026-09-28 — Release v0.11.1 is cut by autonomous run A, and the run's agenda line is: approve the publish step. Under ruling A2 of the product thinker's run A interview (2026-09-23 07:52Z: the run approves the release environment itself once every gate is green) and the product thinker's releases ruling of 2026-09-25T08:04:52Z ("cut additional releases if that makes sense, but bundle multiple intents for it"), the run approves the `release` environment's deployment of v0.11.1 only after the merge queue, the verify job and every other gate on the tagged commit report green, and stops with a handover instead if any does not. The cut: v0.11.1, impact additive (no breaking record since v0.11.0; the run had been calling it v0.12.0 until `launch ship` derived the version), 354 records since v0.11.0: eighteen shipped intents, all additive, and 336 resolved or declined issues (228 fixes, eighteen additive, 90 internal and outside the changelog); the release guard and the findings guard passed with no waiver. Content commit 8a6c83d5, on top of 9ead1d1bf, which the docs-currency gate's findings required. Both semantic gates ran at tier full. The docs-currency-reviewer (Fable 5.1) read the first roll 7c7f5525 and found four minor findings (two stale terminology rows, a README sample status line no state renders, and the root command sentence missing three dispatched record families), all fixed in 9ead1d1bf. The brief-surface cross-check (44 pinned checkers, Opus 5.5, at most four alive) found 127; an independent classification (Fable 5.1) found 126 real at the content commit: the two user-facing and three behaviour findings are captured as four records (iss-2609282105240689, iss-2609282105242542, iss-2609282105241960, iss-2609282105240081), the one major among them, the guard registry passing a bare `rm -rf /` (iss-2609282105242542), deferred out loud past v0.11.0 because it already shipped in v0.11.0 and a registry change needs its own tests and review, all four to be fixed in the first lane after the tag; the design-record drift goes to the systematic brief pass iss-2609091956001547. Landed before the cut on the cutting session's ruling: #736 (integration branch 11); integration branches 12, 13 and 14, reviewed and ready, hold until the tag and fall into the next release.
- 2026-09-28 — guard registry, the rm targets (iss-2609282105242542, autonomous run A lane gateFix): the chapter's headline promised the catch of "an `rm -rf` with an unlucky glob" while the one rm entry fired only behind a `cd` chain, so `rm -rf /` and `rm -rf ~` were an allow. Two bundled entries close it through one new additive Pattern field, `arg_values` (some operand is exactly one of the listed words, by its known text; empty and dash-led values are load-time rejections like an empty prefix). `rm-rf-root-or-home` is a BLOCKER on `/`, `/*`, `~`, `$HOME`, `${HOME}` and their `/` and `/*` forms; `rm-rf-working-directory` is a WARN on `*`, `.`, `..`, `./`, `../`, `./*`, `../*` and `.*`, graded like `git clean` because emptying a build directory the same way is ordinary work and a blocker there would teach sessions to route around the guard. Both require the recursive flag alone, not `-f`: for an agent, whose stdin is not a terminal, rm does not prompt, so `-f` changes nothing about what is destroyed (the cd-chain entry keeps its `-f` requirement; changing a blocker's pattern is a separate act). This reverses the incidental stance that a bare `rm -rf *` is not a hazard (the lab finding iss-2609012040019014 and two tests that used it as their ordinary-work probe): those tests keep their point with `rm -rf ./build` as the probe, and a host workdir is still never read as a `cd`. A third operand residual is recorded beside the two unknown.go names: a target a substitution prints WHOLE (`rm -rf $(echo /)`) is read by its known text, as the `+` refspec prefix is, because `rm -rf $(find . -name '*.pyc')` is how an everyday delete names its targets and reading the word as every target would block it as a delete of `/`; glued text still counts (`"$(true)"/` is `/`). Parameter expansions stay unread except as the literal words the entry names (`rm -rf "$DIR"/*` with `DIR` unset is not seen). The per-byte work bar of the guard's cost tests moves from 20 to 24: the operand walk reads every token once per entry, and sixteen entries put the costliest asserted shape at about 20.
- 2026-09-28 — `abcd banlist list` unfiltered is byte-identical to bare `abcd banlist` (iss-2609282105240081, the v0.11.1 gate's x-115), the plain unfiltered `<verb> list` the naming chapter forbids; DEFERRED out loud past v0.11.1 rather than fixed by lane gateFix of autonomous run A. The chapter forbids the shape and prescribes no remedy, and every remedy changes a shipped verb: an unfiltered `list` that refuses and points at bare `banlist` (the `capture list` precedent, which refuses without a status flag) breaks a script calling it; retiring `list` moves its layer filter onto bare `banlist`; naming it in the bare-invocation exception paragraph keeps the shape the rule forbids. Which one is the product thinker's call, so the record carries `deferred_after: "v0.11.1"` and the reason, and waits for that ruling.
- 2026-09-27 — iss-2609100506263330 takes the record's option (b): with no verified release artefact in the persistent plugin data directory, `ahoy install` writes no PATH entry and names the install one-liner as the command to run first, rather than writing a symlink into the plugin root that the next plugin update strands (lane implementer drainH, autonomous run A, orchestrator abcd-51). Option (a), fetching the artefact inside install, is not taken: install's documented meaning is local configuration, and adr-38 lets the network answer only a verb whose documented meaning is the fetch; the one fetch-and-verify primitive (`abcd update`'s) also imports ahoy, so reaching it from install would need a second downloader or an import inversion. A symlink into the plugin root that an earlier release wrote is left where it stands on a cold cache, raised as a required `symlink.legacy` gap whatever the cache holds, and recorded so the hooks accept it meanwhile. A dangling link `~/.abcd/path-entry` names (read through the same owned, not-group-or-other-writable guard the hook shims apply) is classified abcd's own and repaired by install or removed by uninstall with its record; an unrecorded dangling link keeps iss-2609100506256636's ruling — no provenance claimed, left untouched by detection and by any run with nothing to write in its place, cleared only when install writes the verified copy there.
- 2026-09-28 — iss-2609280932480608 is fixed rather than deferred (ruling by orchestrator abcd-9f, autonomous run A; lane fix2-drainH): iss-2609100506256636's rule, danglingness not provenance, applies past the one entry install acts on, so a gap-driven install removes every abcd-owned dangling `PATH` entry other than its target, with its record, and names each in a note. The removal waits for the target to be a working entry of abcd's own after the step, not merely for the run to have something to write: a cold-cache run that adopts the one-liner's copy writes nothing and still leaves `abcd` answering, while a run that leaves nothing working at the target keeps the dangling entry and names the command to run first. An unowned dangling link is never removed by this step. A dangling link runs nothing — the shell skips it — so the shadow note and the dangling gap no longer say it is what runs or that it shadows later entries. Correction to the 2026-09-27 entry above on iss-2609100506263330: an unrecorded dangling link is not cleared only when install writes the verified copy there, as that entry says, but whenever install writes an entry of its own there — the verified copy, or the `--dev` shim when the plugin binary it rebuilds beside exists; `clearDanglingEntry` clears it ahead of either write, and a run with nothing to write in its place still leaves it untouched.
- 2026-09-27 — The kill-by-search reading follows a search into and out of shell strings, and keeps one over-block, recorded so it is not mistaken for a defect (lane drainG2, autonomous run A, on iss-2609262259360005 and the review of the first reading). Every command of a string that `xargs` runs is read as handed xargs's input, so `pgrep make | xargs sh -c 'kill 4242'` blocks as `kill-by-search` though its kill names a pid: `xargs -I{}` replaces the input into any part of the string, and telling a command that reads `"$@"` or `$1` from one that does not would be a text match on the string, which the feed mechanism does not make. The same holds for the standard input a shell passes to the commands of its string (`pgrep make | sh -c 'xargs kill'`). The accepted over-block of the first reading, that a lower-case signal name (`pkill -term -g <pgid>`) blocked as `pkill-by-owner` because its letters read as the `-t` and `-u` selectors, is removed rather than recorded: `pkill`'s first `-NAME` word naming a signal is read as its signal, in any case and with or without `SIG`, as procps-ng and BSD pkill read it, which is also what lets `-U` and `-G` be read attached (`pkill -Ubob`) without taking `-HUP`, `-USR1` or `-SIGTERM` for them. Only the first such word is the signal, so `pkill -9 -term -g <pgid>` still blocks: both implementations hand the second word to their option parser as `-t erm`. `killall` is not read this way, because psmisc killall reads a signal name only when it begins with a capital letter and parses `killall -term` as `killall -t erm`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ found_during: "v0.11.1 release gate crosscheck (autonomous run A)"
origin: researcher-authored
production_mode: hand-written
found_at: "internal/surface/cli"
deferred_after: "v0.11.1"
deferral_reason: "The naming chapter forbids the shape but prescribes no remedy, and each remedy changes a shipped verb: making an unfiltered banlist list refuse and point at bare banlist (the capture list precedent) breaks a script that calls it, retiring list moves its layer filter onto bare banlist, and recording it as an exception keeps the shape; which one is the product thinker's choice, so the record waits for that ruling rather than a lane picking one"
---

`abcd banlist list` with no flags prints output byte-identical to bare `abcd banlist`, the alias shape the naming chapter (02-constraints/04-naming.md) forbids, and no exception lists it. Found by the v0.11.1 crosscheck (x-115); sub-verb registered at v0.11.0.

## Deferral 2026-09-28

Deferred past v0.11.1: The naming chapter forbids the shape but prescribes no remedy, and each remedy changes a shipped verb: making an unfiltered banlist list refuse and point at bare banlist (the capture list precedent) breaks a script that calls it, retiring list moves its layer filter onto bare banlist, and recording it as an exception keeps the shape; which one is the product thinker's choice, so the record waits for that ruling rather than a lane picking one.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ found_during: "v0.11.1 release gate crosscheck (autonomous run A)"
origin: researcher-authored
production_mode: hand-written
found_at: "commands/lint.md"
resolution: "The bare lint sentence reads 'every target but outbound' in the manifest, commands/lint.md, docs/reference/cli/commands.md and surface.json; proved by TestBareLintSentenceNamesWhatTheBareRunLeavesOut, watched failing on a scratch archive first."
impact: fix
resolved_by:
commit: "e3160ebf78b6bd3c9b7a075916a033259c331b57"
---

The bare `abcd lint` help (commands/lint.md frontmatter, abcd --help, docs/reference/cli/commands.md) says it checks the conventions with every target included, but repolint.DefaultRules excludes the outbound target, so a reader believes the bare run checks outbound text when it does not. Found by the v0.11.1 crosscheck (x-045); same text at v0.11.0.

## Grounds

- pursued: a reader of the bare lint help learns the outbound target is not part of the bare run, matching repolint.DefaultRules and the lint chapter; help text still claiming every target, or a DefaultRules that gains an outbound rule without the sentence changing, would show it wrong
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ found_during: "v0.11.1 release gate crosscheck (autonomous run A)"
origin: researcher-authored
production_mode: hand-written
found_at: "internal/surface/cli/update.go"
resolution: "update --json writes one document on a refusal: the receipt carrying the refusal envelope's abcd, error and exit_code fields, with no empty origin; proved by TestUpdateJSONRefusalIsOneDocument, watched failing on a scratch archive first."
impact: fix
resolved_by:
commit: "d11429b11fe08cf9c7ebf0df3d0e0caba3c81e8f"
---

`abcd update --json` on the absent refusal prints the dispatch-refusal receipt (with an empty origin key, update.Report's Origin has no omitempty) and then the root error envelope: two JSON documents on stdout where a machine reader expects one. Found by the v0.11.1 crosscheck (x-051); unchanged since v0.11.0.

## Grounds

- pursued: a machine reader decoding stdout of a refused update --json finds exactly one JSON value naming the refusal's shape, remedy and exit code; a second value on stdout, or a document missing abcd:error or the refusal block, would show it wrong
Loading
Loading