From d875a06ebc2408a41033c9b233155a5b9fc42b2b Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Sat, 10 Oct 2026 20:27:50 +0100 Subject: [PATCH 1/3] docs: put a verifier's copy outside every working tree AGENTS.md and the CONCURRENCY rule told a verifier to extract its copy into a scratch directory without saying where, and the local tier's scratch/ lives inside the worktree. An extracted copy is not a checkout, so every verb run from a copy there resolves upward to the live worktree and verifies it instead of the copy. Both forms now say the copy goes outside every working tree, in the session's scratchpad or under ~/.abcd.noindex/, and the long form names the rev-parse check that proves it. A test holds both forms to the wording. Refs: iss-2610090642392144 Assisted-by: Claude:claude-opus-5-5 --- .abcd/rules.json | 2 +- AGENTS.md | 5 ++- internal/core/lint/conventionsfile_test.go | 50 ++++++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/.abcd/rules.json b/.abcd/rules.json index f196a3275..135fc07e6 100644 --- a/.abcd/rules.json +++ b/.abcd/rules.json @@ -123,7 +123,7 @@ "**That worktree goes in the machine-scoped store, and nowhere else.** A session's own checkout lives at `~/.abcd.noindex/worktrees///`, keyed on the repository's root commit the way the history, transcript and voyage stores already are \u2014 a checkout moves, is renamed and is cloned twice on one machine, while its root commit does none of that. `` is the full object name (forty hex digits under SHA-1, sixty-four under SHA-256), as the verbs that create a store key it; an abbreviated key names a second directory that no verb reads. Not beside the checkout, not in the directory the user keeps their projects in, and not inside the working tree, which every tree scan walks. A tool never creates a directory in space the user did not hand it, and beside a checkout there is no declared tier at all: [adr-2610031751065746](.abcd/development/decisions/adrs/2610031751065746-the-worktree-store-lives-under-the-renamed-home-abcd-noindex.md) is the rule, carrying [adr-2609091248200336](.abcd/development/decisions/adrs/2609091248200336-a-tool-never-creates-directories-in-user-owned-project-space.md) forward under the renamed home, and [`the-users-directory-is-theirs`](.abcd/development/principles/the-users-directory-is-theirs.md) is the stance. **The store has no verbs yet.** Aim a plain `git worktree add` at the path and create the lane by hand; the store's own `add`, its listing and its reclaim are [itd-2609091014076309](.abcd/development/intents/planned/itd-2609091014076309-session-and-agent-worktrees-live-in-a-machine-scoped-store-t.md), in `drafts/`, so until it ships nothing enumerates the lane or prunes a spent worktree for you, and a worktree in the store is retired with `git worktree remove` like any other.", "**Scan before mutating anything a peer reads or runs.** Before a mutation another session could read or execute, check for peer sessions via the harness's session listing, and announce the mutation to any peer found. The test is the blast radius, not the operation: a commit, branch switch, stash, rebase, or `git worktree add`/`remove` in a checkout that might be shared are examples, and so is a rebuilt or replaced build artefact outside git \u2014 the gitignored binary a PATH entry points at runs under every session on the machine at once, a wider reach than a branch switch in one checkout. Before capturing, resolving or picking a record, also run `go run ./cmd/abcd peers` (`--json` for a machine reader): it lists what every sibling worktree and local branch of this checkout holds that this tree does not, uncommitted captures included, and writes nothing. It sees records, not sessions: an empty listing says no peer holds anything that differs, not that no peer is running. A worktree counts even though it leaves HEAD alone. The sharpest case is a worktree created *inside* the checkout \u2014 the shape the store above exists to keep out \u2014 which churns the tree a peer's scan walks, so a concurrent `make preflight` can fail `TestPayloadTreeImplementationsResolveIdentically` with `the payload carries N rejected file(s)` while the directory populates. That signature, during another session's worktree churn, is a retry rather than a bisect \u2014 the steady-state worktree is harmless, and only the creation window flakes (iss-2608261331317889).", "**A diff you did not make is a peer's work.** Never commit it, revert it, or stash it away silently \u2014 coordinate with the session that made it; uncommitted peer work is untouchable. (Mechanical presence detection is seeded as iss-2608220750029993; until it ships, this convention is the gate.)", - "**A verifier works on a copy.** An agent that mutates code to see whether a test catches the mutation, or patches or instruments a tree to probe it, does that on a scratch copy (`git -C archive HEAD | tar -x -C `), never on a live worktree, and proves `git status --porcelain` empty before reporting. The hazard is the window, not the intent: while a mutation is applied, a gate reports on code nobody wrote and a merge can take it into a branch, and the restore step is itself fallible. It also breaks the rule above from the other end \u2014 a peer seeing the modification cannot tell a mutation from real work. Correspondingly, a merge, commit, push or gate run proves the tree clean **immediately before the act**, never inheriting an emptiness check from earlier in the sequence (itd-193).", + "**A verifier works on a copy.** An agent that mutates code to see whether a test catches the mutation, or patches or instruments a tree to probe it, does that on a scratch copy (`git -C archive HEAD | tar -x -C `), never on a live worktree, and proves `git status --porcelain` empty before reporting. The copy goes outside every working tree: the session's scratchpad, or a directory of its own under `~/.abcd.noindex/` keyed on the root-commit SHA, never the local tier's `.abcd/.work.local/scratch/`. An extracted copy is not a checkout, so a verb run from a copy inside a worktree resolves upward to that worktree and reads its git state and record store instead of the copy; `git -C rev-parse --show-toplevel` failing proves the copy stands alone. The hazard is the window, not the intent: while a mutation is applied, a gate reports on code nobody wrote and a merge can take it into a branch, and the restore step is itself fallible. It also breaks the rule above from the other end \u2014 a peer seeing the modification cannot tell a mutation from real work. Correspondingly, a merge, commit, push or gate run proves the tree clean **immediately before the act**, never inheriting an emptiness check from earlier in the sequence (itd-193).", "**A session cutting a release has the final say on what merges before its tag.** A change ready to land while a peer is mid-cut is handed over as a pull request, announced to the cutting session with the records it would move into the cut, and merged or held on that session's ruling, never on the landing session's. The cutting session reads the diff for anything its release gate's semantic review will see and answers with one of two orderings: merge now and sync into the release branch, so the records join this cut, or hold until the tag, so they fall into the next. A record closed for work an earlier release already carried is stamped `shipped_in:` with that release, whichever ordering is chosen, or it lands in the changelog as if it shipped today. The rule holds even when the change is orthogonal to the release gates: orthogonal to the gates is not orthogonal to the release branch, the cutting session is the one that can see the overlap, and a full-tier release receipt names the commit its reviewers read, so nothing merges between the CHANGELOG roll and the tag (iss-2609091037191879 seeds the mechanical form).", "**Record ids need no coordination between checkouts.** Captures, intents and specs mint timestamp-numeric ids through one allocator that reads no maximum (adr-45), so two current checkouts minting in the same window allocate distinct ids unless they share the same second and the same four-digit draw, a coincidence the armed uniqueness detectors assert against; the per-checkout mint lock only serialises minters inside one checkout. ADRs mint through that same seam: `abcd decide \"\"` allocates `adr-<yymmddHHMMSS><rrrr>` and files it as `<stamp>-<slug>.md` (the 2026-09-01 ruling in `.abcd/work/DECISIONS.md`, the turn adr-45 ruling 3 deferred), so two checkouts deciding in the same window cannot allocate one number either. The ordinals `0001`\u2013`0058` keep their ids and their filenames and every reader admits both vintages through one derivation, so no record family needs a word first \u2014 mint the ADR.", "**A record is resolved on the branch that carries it**, never re-added to the default branch after a branch was cut from it. The ledger's status signal IS folder membership, so the one state it cannot represent is a record in two status folders at once \u2014 and that is what the re-add produces: the branch moves the record `open/` \u2192 `resolved/` while the default branch adds it back into `open/`, git pairs an add on one side with a delete-plus-add on the other, and the integration tree carries both copies with no status at all. Every read of the ledger now refuses on it and names both files (iss-2609100507430423), so the remedy is to move or remove one; the convention is what stops it arising." diff --git a/AGENTS.md b/AGENTS.md index 850472a8d..fd7199cf5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -258,7 +258,10 @@ with `*CONCURRENCY` to force it). stash it away silently; uncommitted peer work is untouchable. - **A verifier works on a copy** (`git -C <wt> archive HEAD | tar -x -C <scratch>`), never on a live worktree, and a merge, commit, push or gate run - proves the tree clean **immediately before the act** (itd-193). + proves the tree clean **immediately before the act** (itd-193). The copy goes + outside every working tree: the session's scratchpad or a directory under + `~/.abcd.noindex/`, never `.abcd/.work.local/scratch/`, where every verb run + from it reads the worktree around it instead. - **A session cutting a release has the final say on what merges before its tag.** A change ready while a peer is mid-cut is handed over as a pull request and merged or held on the cutting session's ruling. diff --git a/internal/core/lint/conventionsfile_test.go b/internal/core/lint/conventionsfile_test.go index 98c54c6d2..6dc73cc85 100644 --- a/internal/core/lint/conventionsfile_test.go +++ b/internal/core/lint/conventionsfile_test.go @@ -11,6 +11,7 @@ import ( "strings" "testing" + "github.com/intentdriven/abcd/internal/abcdhome" "github.com/intentdriven/abcd/internal/core/rules" "github.com/intentdriven/abcd/internal/gittest" ) @@ -185,3 +186,52 @@ func TestAgentsMDDetailMovedUnderTheLoaderIsRecalled(t *testing.T) { }) } } + +// TestVerifierCopyGoesOutsideEveryWorkingTree holds the verifier-copy rule, in +// both its short form (AGENTS.md, § Concurrent sessions) and its long form (the +// CONCURRENCY domain of .abcd/rules.json), to naming where the copy goes +// (iss-2610090642392144). A copy extracted under the local tier's scratch/ sits +// inside the live worktree; it is not a checkout, so every verb run from it that +// reads git or the record store resolves upward to the worktree around it and +// verifies that tree, not the copy. Both forms must say the copy goes outside +// every working tree and name the two homes that are: the session's scratchpad +// and a directory under the machine-scoped abcd home (abcdhome.Display). +func TestVerifierCopyGoesOutsideEveryWorkingTree(t *testing.T) { + root := filepath.Join("..", "..", "..") + body := readRepoFile(t, root, "AGENTS.md") + var repo rules.RuleSet + if err := json.Unmarshal([]byte(readRepoFile(t, root, ".abcd/rules.json")), &repo); err != nil { + t.Fatalf("parse .abcd/rules.json: %v", err) + } + + const shortHead = "- **A verifier works on a copy**" + start := strings.Index(body, shortHead) + if start < 0 { + t.Fatalf("AGENTS.md has no %q rule", shortHead) + } + short := body[start+len(shortHead):] + if next := strings.Index(short, "\n- "); next >= 0 { + short = short[:next] + } + + const longHead = "**A verifier works on a copy.**" + long := "" + for _, r := range repo.Domains["CONCURRENCY"].Rules { + if strings.HasPrefix(r, longHead) { + long = r + } + } + if long == "" { + t.Fatalf("the CONCURRENCY domain of .abcd/rules.json has no rule opening %q", longHead) + } + + for name, text := range map[string]string{"AGENTS.md": short, "the CONCURRENCY domain": long} { + flat := strings.Join(strings.Fields(text), " ") + for _, want := range []string{"outside every working tree", "scratchpad", abcdhome.Display() + "/"} { + if !strings.Contains(flat, want) { + t.Errorf("%s's verifier-copy rule does not say %q; a copy made inside a worktree is "+ + "read as that worktree by every verb run from it", name, want) + } + } + } +} From 137ef4e7b8663b9d9dd80841378a70a9469ff456 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Sat, 10 Oct 2026 21:43:15 +0100 Subject: [PATCH 2/3] docs: hold itd-193 and the users-directory principle to the copy's home The verifier-copy rule now says the copy goes outside every working tree, but the discipline it cites (itd-193) still put the copy in the local ephemeral tier, and the principle the CONCURRENCY domain links as its stance sent it to .abcd/.work.local/scratch/. Both sentences now match the rule, and the detector holds them to the same three phrases. The bundled OPINIONS rule, which still allows a verifier's copy in the local tier, is left as it is: changing it changes what every managed repository is taught. DECISIONS.md records the deferral, and iss-2610102021042657 holds the question. Refs: iss-2610090642392144 Refs: iss-2610102021042657 Assisted-by: Claude:claude-opus-5-5 --- ...a-copy-no-agent-mutates-a-live-worktree.md | 4 +++- .../the-users-directory-is-theirs.md | 4 +++- .abcd/work/DECISIONS.md | 1 + internal/core/lint/conventionsfile_test.go | 24 +++++++++++++++++-- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/.abcd/development/intents/disciplines/itd-193-a-verifier-works-on-a-copy-no-agent-mutates-a-live-worktree.md b/.abcd/development/intents/disciplines/itd-193-a-verifier-works-on-a-copy-no-agent-mutates-a-live-worktree.md index 926c588d8..d9d26bf55 100644 --- a/.abcd/development/intents/disciplines/itd-193-a-verifier-works-on-a-copy-no-agent-mutates-a-live-worktree.md +++ b/.abcd/development/intents/disciplines/itd-193-a-verifier-works-on-a-copy-no-agent-mutates-a-live-worktree.md @@ -28,7 +28,9 @@ does that on a **copy**, never on the live worktree: git -C <worktree> archive HEAD | tar -x -C <scratch>/mut ``` -The copy lives in the local ephemeral tier. Before reporting, the verifier +The copy goes outside every working tree: the session's scratchpad or a +directory under `~/.abcd.noindex/`, never `.abcd/.work.local/scratch/`. Before +reporting, the verifier proves the original is untouched, and says so: ```text diff --git a/.abcd/development/principles/the-users-directory-is-theirs.md b/.abcd/development/principles/the-users-directory-is-theirs.md index ceeb3a2be..0cdc2a92c 100644 --- a/.abcd/development/principles/the-users-directory-is-theirs.md +++ b/.abcd/development/principles/the-users-directory-is-theirs.md @@ -48,7 +48,9 @@ planned and unbuilt. Until it ships the rule is applied by hand: a session that worktree puts it under `~/.abcd.noindex/worktrees/<root-sha>/<name>/`, the location [adr-2610031751065746](../decisions/adrs/2610031751065746-the-worktree-store-lives-under-the-renamed-home-abcd-noindex.md) sets, a verifier's -copy goes to `.abcd/.work.local/scratch/`, and a reviewer who sees a directory +copy goes outside every working tree (the session's scratchpad or a directory +under `~/.abcd.noindex/`), never `.abcd/.work.local/scratch/`, and a reviewer +who sees a directory appear beside a checkout names it. **Promotion.** The intent's verbs promote the store half; a check that counts diff --git a/.abcd/work/DECISIONS.md b/.abcd/work/DECISIONS.md index b868fea3e..7fd43a390 100644 --- a/.abcd/work/DECISIONS.md +++ b/.abcd/work/DECISIONS.md @@ -2680,3 +2680,4 @@ together (the script's header says why there is no escape hatch). - 2026-10-10 — Ruling of the product thinker on the bundled pace (given in a build session, on the report that its run had entered a 300-minute pause): "Remove that forever, we don't need a pause", then "it must be set to zero by default". The bundled pause of decision 5 of itd-2609201925079472 (120 minutes of work, 300 of pause) becomes 0: `BundledPauseMinutes` is 0, so a run that sets no pace runs window after window with no pause, and a repository or machine that wants one sets `pace.pause_minutes`. The working window, the sub-agent ceiling and the fix rounds are unchanged. The intent's record keeps decision 5 and its first criterion as written (120/300), which this entry supersedes for the pause; iss-2610100546338578 carries the change. - 2026-10-10 — Four points spc-2609301921521360 left open, settled by lane-1 of run-2610100550297632 in building it (itd-2609201925079472 criteria 7 and 8). (1) A runner's quota is counted in agent runs (`runner.Quota.Remaining`, reported through the optional `runner.QuotaReporter`), and the estimate from the spec's size is the least the run starts: per step to build, one implementer and one round of the two reviewers, and the fidelity audit once for an intent; fix rounds and syncs are not foreseen. Neither shipped runner reports a quota (the claude CLI and opencode expose none a launch can read before it starts), so today every run names its runners and skips the check out loud; the comparison is reached by a runner that reports one. (2) The rate-limit response is read from the claude CLI's own events, a `rate_limit_event` whose `rate_limit_info.status` is `rejected` or an assistant message whose `error` is `rate_limit`, on a run that did not finish; opencode's error events carry no rate-limit shape this build has verified, so an opencode limit stays a refusal and falls back as before. A rate limit is never fallen back on: every route spends the budget the window paces. (3) "Checkpointed to its branch" reads as the restart of 2026-10-09 reads a gone agent (iss-2610080620372731): the limited agent's uncommitted work and partial receipt are saved aside for review, never built on, its worktree reset to the branch's last commit, and its await dropped so the first step after the pause hands the same work to a fresh agent; a limited validator has only its partial return saved aside, since it edits nothing and may share the worktree with its round. Every other lane with work in flight is checkpointed at its branch's head in the record and left running, its agents' receipts still taken inside the pause, because their agents are not this process's to stop. (4) The pause after a rate limit is the run's own pause minutes, not the reset time a harness reports (decision 2 of the intent), and a pause already running is kept. - 2026-10-10 — Three defects the reviewers of PR #886 (itd-2609201925079472, run-2610100550297632) noted below their bar are deferred, not fixed in the change that set the bundled pause to zero: iss-2610100913086397 (a rate-limited validator whose partial return cannot be saved aside leaves its lane stuck, its named `--restart` refused), iss-2610100913088696 (the rate-limit checkpoint matches an await by receipt path alone, against its comment) and iss-2610100913096412 (the run-start quota query has no deadline and its error is not home-redacted). Reason: each needs a tested change to the rate-limit and budget code in `internal/core/implement/loop/`, which steps 3 to 5 of spc-2609212015054359 (itd-82, the drain) rework next, and none is reachable in a run today without a second fault: a refused save-aside on a rate limit, an operator restart racing the dispatch, or a quota-reporting runner, of which none ships. They are taken up with the rate limit's own wait (iss-2610100602595553), or by the drain's steps where those touch the same code. +- 2026-10-10 — A finding of the ruthless reviewer of lane-1 of run-2610101926030564 (iss-2610090642392144) is deferred, not fixed in the change that puts a verifier's copy outside every working tree: the bundled `OPINIONS` rule in `internal/core/rules/defaults/rules.json` ("The user's directory is theirs") still allows a verifier's copy in the checkout's `.abcd/.work.local/` tier, against the repository's own `CONCURRENCY` rule, AGENTS.md, itd-193 and the users-directory principle, which now forbid it. Reason: the bundled rule is injected into every managed repository, so changing it is a user-visible change the issue's remedy did not name, and it waits on a person's ruling, recorded as iss-2610102021042657. diff --git a/internal/core/lint/conventionsfile_test.go b/internal/core/lint/conventionsfile_test.go index 6dc73cc85..859b5de02 100644 --- a/internal/core/lint/conventionsfile_test.go +++ b/internal/core/lint/conventionsfile_test.go @@ -195,7 +195,9 @@ func TestAgentsMDDetailMovedUnderTheLoaderIsRecalled(t *testing.T) { // reads git or the record store resolves upward to the worktree around it and // verifies that tree, not the copy. Both forms must say the copy goes outside // every working tree and name the two homes that are: the session's scratchpad -// and a directory under the machine-scoped abcd home (abcdhome.Display). +// and a directory under the machine-scoped abcd home (abcdhome.Display). The +// itd-193 discipline and the users-directory principle, which both forms lean +// on, are held to the same three phrases. func TestVerifierCopyGoesOutsideEveryWorkingTree(t *testing.T) { root := filepath.Join("..", "..", "..") body := readRepoFile(t, root, "AGENTS.md") @@ -225,7 +227,25 @@ func TestVerifierCopyGoesOutsideEveryWorkingTree(t *testing.T) { t.Fatalf("the CONCURRENCY domain of .abcd/rules.json has no rule opening %q", longHead) } - for name, text := range map[string]string{"AGENTS.md": short, "the CONCURRENCY domain": long} { + // The two records both forms lean on say it too: the discipline they cite + // (itd-193) and the principle the CONCURRENCY domain links as the stance. + // Each is held by the paragraph its anchor falls in. + paragraph := func(rel, anchor string) string { + for _, p := range strings.Split(readRepoFile(t, root, rel), "\n\n") { + if flat := strings.Join(strings.Fields(p), " "); strings.Contains(flat, anchor) { + return flat + } + } + t.Fatalf("%s has no paragraph saying %q", rel, anchor) + return "" + } + itd193 := paragraph(".abcd/development/intents/disciplines/itd-193-a-verifier-works-on-a-copy-no-agent-mutates-a-live-worktree.md", + "Before reporting, the verifier proves the original is untouched") + principle := paragraph(".abcd/development/principles/the-users-directory-is-theirs.md", "a verifier's copy goes") + + for name, text := range map[string]string{ + "AGENTS.md": short, "the CONCURRENCY domain": long, "itd-193": itd193, "the users-directory principle": principle, + } { flat := strings.Join(strings.Fields(text), " ") for _, want := range []string{"outside every working tree", "scratchpad", abcdhome.Display() + "/"} { if !strings.Contains(flat, want) { From f33bd56984b4e4a519112185f434e54f772fbbfa Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Sun, 11 Oct 2026 01:09:48 +0100 Subject: [PATCH 3/3] chore(record): land lane-1 of run-2610101926030564 The implement loop's landing for iss-2610090642392144: resolved iss-2610090642392144 with d875a06ebc24. Resolves: iss-2610090642392144 Assisted-by: Claude:claude-opus-5-5 --- ...md-tells-a-verifier-to-work-on-a-copy-made-with-git.md | 8 ++++++++ 1 file changed, 8 insertions(+) rename .abcd/work/issues/{open => resolved}/iss-2610090642392144-agents-md-tells-a-verifier-to-work-on-a-copy-made-with-git.md (65%) diff --git a/.abcd/work/issues/open/iss-2610090642392144-agents-md-tells-a-verifier-to-work-on-a-copy-made-with-git.md b/.abcd/work/issues/resolved/iss-2610090642392144-agents-md-tells-a-verifier-to-work-on-a-copy-made-with-git.md similarity index 65% rename from .abcd/work/issues/open/iss-2610090642392144-agents-md-tells-a-verifier-to-work-on-a-copy-made-with-git.md rename to .abcd/work/issues/resolved/iss-2610090642392144-agents-md-tells-a-verifier-to-work-on-a-copy-made-with-git.md index 8e8fdcace..4d3c822f0 100644 --- a/.abcd/work/issues/open/iss-2610090642392144-agents-md-tells-a-verifier-to-work-on-a-copy-made-with-git.md +++ b/.abcd/work/issues/resolved/iss-2610090642392144-agents-md-tells-a-verifier-to-work-on-a-copy-made-with-git.md @@ -10,6 +10,14 @@ origin: researcher-authored production_mode: hand-written found_at: "AGENTS.md" remedy: "Say in AGENTS.md and the CONCURRENCY rule that a verifier's copy goes outside every working tree (the session scratchpad or a machine-store directory), or have the verbs refuse when the nearest checkout above a directory is not the one the directory was extracted from." +resolution: "AGENTS.md and the CONCURRENCY rule now say a verifier's copy goes outside every working tree, in the session's scratchpad or under ~/.abcd.noindex/, never the local tier's scratch/, and a test holds both forms to it." +impact: internal +resolved_by: + commit: "d875a06ebc2408a41033c9b233155a5b9fc42b2b" --- AGENTS.md tells a verifier to work on a copy made with git archive into a scratch directory, and the local scratch tier lives inside the worktree. A copy extracted there is not a git checkout, so every abcd verb run from it that reads git or the record store resolves upward to the live worktree that contains it. In the v0.13.3 cut the binary verbs run from the export read the release worktree, which was only equivalent because that worktree was clean at the content commit. + +## Grounds + +- pursued: a verifier following either form extracts its copy where no enclosing checkout exists, so git -C <copy> rev-parse --show-toplevel fails; a form that drops the location, or a copy that still resolves to a worktree, shows it wrong