From 9fe29c1b0a8555c20f3973c159e7b7e151637957 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Sat, 10 Oct 2026 06:46:46 +0100 Subject: [PATCH 1/4] fix: make the bundled pace pause zero minutes A run that set no pace stopped every stage, landings included, for 300 minutes after each 120-minute window. The product thinker ruled that no pause is needed and that the default must be zero, so the bundled pace is now 120/0: a run with no pace configured opens window after window without pausing, and a repository or machine that wants a pause sets pace.pause_minutes. The window, the sub-agent ceiling and the fix rounds are unchanged. The ruling is in the decision log. Resolves: iss-2610100546338578 Assisted-by: Claude:claude-opus-5-5 --- .abcd/development/brief/04-surfaces/34-build.md | 4 ++-- .abcd/work/DECISIONS.md | 1 + ...300-minutes-after-every-120-minute-window.md | 17 +++++++++++++++++ commands/build.md | 2 +- docs/reference/cli/commands.md | 2 +- internal/core/implement/loop/pace.go | 10 +++++----- internal/core/implement/loop/pace_test.go | 8 ++++---- internal/surface/cli/build.go | 2 +- internal/surface/cli/build_pace_surface_test.go | 8 ++++---- 9 files changed, 36 insertions(+), 18 deletions(-) create mode 100644 .abcd/work/issues/resolved/iss-2610100546338578-a-paced-run-pauses-300-minutes-after-every-120-minute-window.md diff --git a/.abcd/development/brief/04-surfaces/34-build.md b/.abcd/development/brief/04-surfaces/34-build.md index 1f576580e..d11ea2881 100644 --- a/.abcd/development/brief/04-surfaces/34-build.md +++ b/.abcd/development/brief/04-surfaces/34-build.md @@ -165,8 +165,8 @@ the build's own pace, sub-agent and fix-round flags, the pace written as `/`; `pace.work_minutes`, `pace.pause_minutes`, `pace.sub_agents` and `pace.fix_rounds` in the repository's `.abcd/config.json`; the same keys in `~/.abcd.noindex/config.json`; and -the bundled default, 120 minutes of work, 300 of pause, 2 sub-agents and 3 fix -rounds (decision 5 and ruling DR1), held in one set of constants. The files are read through the +the bundled default, 120 minutes of work, no pause, 2 sub-agents and 3 fix +rounds (decision 5, the pause removed by the ruling of 2026-10-10, and ruling DR1), held in one set of constants. The files are read through the reader's guards (a regular file inside the checkout; on the machine, one the caller owns and nobody else can write), and the reader claims the `pace` namespace, so a key under it the loop does not read is refused rather than diff --git a/.abcd/work/DECISIONS.md b/.abcd/work/DECISIONS.md index feade8374..ed8554c76 100644 --- a/.abcd/work/DECISIONS.md +++ b/.abcd/work/DECISIONS.md @@ -2677,3 +2677,4 @@ together (the script's header says why there is no escape hatch). - 2026-10-05 — Decided without asking (facilitator default, recorded by abcd-e8 at abcd-d7's direction while the person is away): the dashboard refuses a node shared into the tailnet from another account, and a request relayed by another node's Serve or Funnel, until the person decides; decision 4's 'anyone on your Tailscale' did not settle either, a shared-in node belongs to someone else and a Funnel relay is the open internet, so refusing is the safe default. A raw TCP relay cannot be told apart and stays a residual. - 2026-10-07 — Ruling DR3 of the person (2026-09-29, in the human-step interview run for autonomous run A and relayed by its interviewer abcd-23 [b1e81b]), restated for iss-2610020728128805 (recorded by lane-1 of run-2610071733272557, records only). The answers of that interview are kept in the local-tier file `.abcd/.work.local/scratch/reports/rulings-answered-2609-29-b.md`, line 31, and are restated here because that file is not committed. DR3, verbatim: "itd-60 docs-fidelity verdict: FOUND AUTOMATICALLY by spec close and launch ship (a saved verdict labelled with the reviewed code's fingerprint; match -> proceed; none/stale -> refuse 'run the docs review first'), like the preflight receipt." It is the ruling the doc-fidelity gate's shape rests on (itd-60, ac-2: the semantic layer is a saved review for HEAD that `spec close` and `launch ship` find automatically, rather than a review run at the close), cited by `internal/core/docfidelity/store.go`, `internal/core/docfidelity/docfidelity.go` and `internal/core/lint/gatereceipt.go`. The issue was captured on 2026-10-02 against the 2026-09-30 entry above, which restates DR1, DR2, DR4, DR5 and DR6 and skips DR3; that entry is left as written (DA002). The 2026-10-02 entry of lane rd2 above already restated DR3 later the same day, after the capture, together with CB1, CC1, CD1 and CD2, and DQ2b and CJ1b have their own 2026-09-30 entries, so none of the rulings the issue names as owed is still missing; this entry adds the issue's own pointer to the ruling and changes no earlier line. - 2026-10-09 — Rulings of the product thinker on the three hand-backs of the 2026-10-07/08 autonomous drain (asked by abcd-0b, one question at a time, answers verbatim): (1) iss-2609251618079479, whether the build-sequence chapter's build-milestone sense retires with the phase: "Rewrite in today's terms" — 06-delivery/01-build-sequence.md stays a current chapter, its build order restated as dependencies with no milestones (neither retired as history nor kept as a second sense of the word). (2) iss-2610040758569116, whether an agent may run 'abcd decide accept', which stamps the person's git name as accepted_by: "Agents too, disclosed" — no guard block as for 'source ledger --flip'; the commit's Assisted-by trailer is the disclosure. (3) iss-2610030956156354's open design point, whether a step still falls back to the agent harness when the size count already sent the brief to the person's own model server and the chat call then fails: "Move to the harness" — no answer received is the trigger, not nothing sent; recorded as iss-2610090701491123, since the resolved fix stops instead. +- 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. diff --git a/.abcd/work/issues/resolved/iss-2610100546338578-a-paced-run-pauses-300-minutes-after-every-120-minute-window.md b/.abcd/work/issues/resolved/iss-2610100546338578-a-paced-run-pauses-300-minutes-after-every-120-minute-window.md new file mode 100644 index 000000000..edc03e742 --- /dev/null +++ b/.abcd/work/issues/resolved/iss-2610100546338578-a-paced-run-pauses-300-minutes-after-every-120-minute-window.md @@ -0,0 +1,17 @@ +--- +schema_version: 1 +id: "iss-2610100546338578" +slug: "a-paced-run-pauses-300-minutes-after-every-120-minute-window" +severity: "minor" +category: "ux" +source: "user-observation" +found_during: "manual-capture" +origin: researcher-authored +production_mode: hand-written +found_at: "internal/core/implement/loop/pace.go" +remedy: "Set BundledPauseMinutes to 0 so the bundled pace is 120/0; a repository or machine that wants a pause sets pace.pause_minutes" +resolution: "the bundled pause is 0 (BundledPauseMinutes), so a run with no pace configured never pauses; a repository or machine sets pace.pause_minutes for one" +impact: fix +--- + +A paced run pauses 300 minutes after every 120-minute window by default: the bundled pace is 120/300, so an implement run that outlives its first window stops every stage, landing moves included, for five hours unless a flag or a configuration layer sets the pause. The product thinker ruled on 2026-10-10 that no pause is needed and that the default must be zero. diff --git a/commands/build.md b/commands/build.md index 68a90e89b..8c08dd85e 100644 --- a/commands/build.md +++ b/commands/build.md @@ -166,7 +166,7 @@ run starts, each from the highest layer that sets it: 2. `pace.work_minutes`, `pace.pause_minutes`, `pace.sub_agents` and `pace.fix_rounds` in the repository's `.abcd/config.json`; 3. the same keys in `~/.abcd.noindex/config.json`, for every checkout on the machine; -4. the bundled 120/300 with 2 sub-agents and 3 fix rounds. +4. the bundled 120/0 (no pause) with 2 sub-agents and 3 fix rounds. The payload's `pace` carries each number as `value`, `layer` (`flag`, `repo`, `machine` or `bundled`) and `origin` (the flag as typed, or the file), and the diff --git a/docs/reference/cli/commands.md b/docs/reference/cli/commands.md index 6b75dda4f..5b47e386c 100644 --- a/docs/reference/cli/commands.md +++ b/docs/reference/cli/commands.md @@ -269,7 +269,7 @@ validators alive at once, and the fix rounds a lane may take before it is handed four numbers are read once, when the run starts: --pace /, --sub-agents and --fix-rounds for this run, else pace.work_minutes, pace.pause_minutes, pace.sub_agents and pace.fix_rounds in the repository's .abcd/config.json, else in -~/.abcd.noindex/config.json, else the bundled 120/300 with 2 sub-agents and 3 fix rounds. The result and the run +~/.abcd.noindex/config.json, else the bundled 120/0 (no pause) with 2 sub-agents and 3 fix rounds. The result and the run record name each number's layer. A malformed pace or ceiling, typed or configured, is refused naming the value and the accepted form, and writes nothing. Starting again keeps the run's pace; a flag naming another is refused. The window, the pause and the ceiling diff --git a/internal/core/implement/loop/pace.go b/internal/core/implement/loop/pace.go index 88af3e16f..b940f57ba 100644 --- a/internal/core/implement/loop/pace.go +++ b/internal/core/implement/loop/pace.go @@ -20,13 +20,13 @@ import ( "github.com/intentdriven/abcd/internal/core/layered" ) -// The bundled pace: 120 minutes of work, 300 of pause, two lanes (decision 5, -// the product thinker's numbers for this repository's runs on 2026-09-20). A -// repository that measured otherwise writes its own under `pace` in its -// .abcd/config.json. +// The bundled pace: 120 minutes of work, no pause, two lanes (decision 5, the +// product thinker's numbers for this repository's runs on 2026-09-20, with the +// pause removed by the product thinker's ruling of 2026-10-10). A repository +// that wants a pause writes its own under `pace` in its .abcd/config.json. const ( BundledWorkMinutes = 120 - BundledPauseMinutes = 300 + BundledPauseMinutes = 0 BundledSubAgents = 2 ) diff --git a/internal/core/implement/loop/pace_test.go b/internal/core/implement/loop/pace_test.go index 971ed4868..c93ab9fdf 100644 --- a/internal/core/implement/loop/pace_test.go +++ b/internal/core/implement/loop/pace_test.go @@ -64,7 +64,7 @@ func wantPace(t *testing.T, p *Pace, work, pause, subs int, layer string) { } // TestARunWithNoConfigurationRunsOnTheBundledPace is criterion 1: with no flag -// and no configuration the run is paced 120/300 with two lanes, the run record +// and no configuration the run is paced 120/0 (no pause) with two lanes, the run record // names the bundled layer, and the run's first window opens at its start. func TestARunWithNoConfigurationRunsOnTheBundledPace(t *testing.T) { repo := loopRepo(t, readyIntent("", settledQuestions), specWithSteps("")) @@ -73,13 +73,13 @@ func TestARunWithNoConfigurationRunsOnTheBundledPace(t *testing.T) { if err != nil { t.Fatal(err) } - wantPace(t, res.Pace, 120, 300, 2, "bundled") + wantPace(t, res.Pace, 120, 0, 2, "bundled") st, err := ReadState(repo.Root(), res.RunID) if err != nil { t.Fatal(err) } - wantPace(t, st.Pace, 120, 300, 2, "bundled") - if note := paceRecord(t, st); !strings.Contains(note, "120/300") || !strings.Contains(note, "2 sub-agents") || !strings.Contains(note, "bundled") { + wantPace(t, st.Pace, 120, 0, 2, "bundled") + if note := paceRecord(t, st); !strings.Contains(note, "120/0") || !strings.Contains(note, "2 sub-agents") || !strings.Contains(note, "bundled") { t.Fatalf("the record names the pace and the bundled layer: %q", note) } if st.WindowStartedAt == nil || !st.WindowStartedAt.Equal(now) || st.NextEligibleAt != nil { diff --git a/internal/surface/cli/build.go b/internal/surface/cli/build.go index ae6060dc3..eb391f93c 100644 --- a/internal/surface/cli/build.go +++ b/internal/surface/cli/build.go @@ -123,7 +123,7 @@ func newBuildCommand(asJSON *bool) *cobra.Command { "four numbers are read once, when the run starts: --pace /,\n" + "--sub-agents and --fix-rounds for this run, else pace.work_minutes, pace.pause_minutes,\n" + "pace.sub_agents and pace.fix_rounds in the repository's .abcd/config.json, else in\n" + - abcdhome.Display("config.json") + ", else the bundled 120/300 with 2 sub-agents and 3 fix rounds. The result and the run\n" + + abcdhome.Display("config.json") + ", else the bundled 120/0 (no pause) with 2 sub-agents and 3 fix rounds. The result and the run\n" + "record name each number's layer. A malformed pace or ceiling, typed or configured, is\n" + "refused naming the value and the accepted form, and writes nothing. Starting again keeps\n" + "the run's pace; a flag naming another is refused. The window, the pause and the ceiling\n" + diff --git a/internal/surface/cli/build_pace_surface_test.go b/internal/surface/cli/build_pace_surface_test.go index 7e19c16b1..377fbb66d 100644 --- a/internal/surface/cli/build_pace_surface_test.go +++ b/internal/surface/cli/build_pace_surface_test.go @@ -47,21 +47,21 @@ func buildPace(t *testing.T, args ...string) paceJSON { } // TestBuildRunsOnTheBundledPaceAndNamesIt is criterion 1 at the surface: with -// no flag and no configuration the run is paced 120/300 with two lanes, and +// no flag and no configuration the run is paced 120/0 (no pause) with two lanes, and // the build and the status name the bundled layer. func TestBuildRunsOnTheBundledPaceAndNamesIt(t *testing.T) { buildRepo(t) res := buildPace(t) p := res.Pace - if p.WorkMinutes.Value != 120 || p.PauseMinutes.Value != 300 || p.SubAgents.Value != 2 || + if p.WorkMinutes.Value != 120 || p.PauseMinutes.Value != 0 || p.SubAgents.Value != 2 || p.WorkMinutes.Layer != "bundled" || p.SubAgents.Layer != "bundled" { t.Fatalf("pace = %+v", *p) } status := mustImplement(t, "implement", "status") - if !strings.Contains(status, "pace: 120/300 minutes, 2 sub-agents (bundled)") { + if !strings.Contains(status, "pace: 120/0 minutes, 2 sub-agents (bundled)") { t.Fatalf("the status names the pace and its layer:\n%s", status) } - if !strings.Contains(status, "pace 120/300 minutes, 2 sub-agents (bundled)") { + if !strings.Contains(status, "pace 120/0 minutes, 2 sub-agents (bundled)") { t.Fatalf("the run record names the pace and its layer:\n%s", status) } } From 3c66efa4c5aa5d87721d101c4cfe3d67b6b9df71 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Sat, 10 Oct 2026 07:03:04 +0100 Subject: [PATCH 2/4] chore: capture that a rate limit gets no wait once the pause is zero Pacing's rate-limit checkpoint waits the run's pause, which is now zero by default, so a rate-limited run would retry at once. The product thinker ruled that a rate limit waits on its own, as later work. Refs: iss-2610100602595553, iss-2610100546338578 Assisted-by: Claude:claude-opus-5-5 --- ...ets-no-wait-once-the-pause-is-zero-pacing-s.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .abcd/work/issues/open/iss-2610100602595553-a-rate-limit-gets-no-wait-once-the-pause-is-zero-pacing-s.md diff --git a/.abcd/work/issues/open/iss-2610100602595553-a-rate-limit-gets-no-wait-once-the-pause-is-zero-pacing-s.md b/.abcd/work/issues/open/iss-2610100602595553-a-rate-limit-gets-no-wait-once-the-pause-is-zero-pacing-s.md new file mode 100644 index 000000000..c9673faa8 --- /dev/null +++ b/.abcd/work/issues/open/iss-2610100602595553-a-rate-limit-gets-no-wait-once-the-pause-is-zero-pacing-s.md @@ -0,0 +1,15 @@ +--- +schema_version: 1 +id: "iss-2610100602595553" +slug: "a-rate-limit-gets-no-wait-once-the-pause-is-zero-pacing-s" +severity: "minor" +category: "bug" +source: "user-observation" +found_during: "manual-capture" +origin: researcher-authored +production_mode: hand-written +found_at: "pacing's rate-limit checkpoint, itd-2609201925079472 criterion 8" +remedy: "Give the rate-limit checkpoint its own wait, independent of pace.pause_minutes: the reset time the runner reports when it reports one, else a configured or bundled rate-limit wait" +--- + +A rate limit gets no wait once the pause is zero: pacing's rate-limit checkpoint (itd-2609201925079472) ends the run's window early and sets next_eligible_at to now plus the run's pause, and with the bundled pause now 0 (iss-2610100546338578) the next step hands the same work out again at once, so a rate-limited run can hit the same limit again and again. The product thinker ruled on 2026-10-10 that a rate limit waits on its own, independent of the pause, as work after pacing and the drain (itd-82). From 77958aeb2af52ff818684290e03484f2b538691a Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Sat, 10 Oct 2026 10:12:34 +0100 Subject: [PATCH 3/4] test: start the rate-limit runs on a pause of their own The rate-limit tests measured the window's early end against the bundled pause, which is now zero minutes: a zero pause lets the next step start at once, so "a step inside the pause refuses" had no pause to refuse inside. They now start their runs with a 300-minute pause. Assisted-by: Claude:claude-opus-5-5 --- internal/core/implement/loop/ratelimit_test.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/internal/core/implement/loop/ratelimit_test.go b/internal/core/implement/loop/ratelimit_test.go index 8ec6a6b51..2b7498e30 100644 --- a/internal/core/implement/loop/ratelimit_test.go +++ b/internal/core/implement/loop/ratelimit_test.go @@ -11,6 +11,7 @@ import ( "bytes" "context" "errors" + "fmt" "os" "path/filepath" "strings" @@ -20,6 +21,11 @@ import ( "github.com/intentdriven/abcd/internal/core/runner" ) +// rateLimitPause is the pause these runs are started with. The bundled pause +// is zero, and a zero pause would let the step after a rate limit start +// straight away, so the window's early end is pinned to a pause of its own. +const rateLimitPause = 300 + // claudeImplementer routes the implementer alone to the claude runner. const claudeImplementer = `{"roles":{"implementer":{"runner":"claude"}},"runner":{"claude":{}}}` @@ -30,7 +36,7 @@ const claudeImplementer = `{"roles":{"implementer":{"runner":"claude"}},"runner" // and lane-1's commit. func rateLimitedPair(t *testing.T) (*parFixture, StepResult, string) { t.Helper() - f := newParFixture(t, "1. One\n2. Two\n - needs: none\n", Options{SubAgents: strp("3")}) + f := newParFixture(t, "1. One\n2. Two\n - needs: none\n", Options{Pace: strp(fmt.Sprintf("%d/%d", BundledWorkMinutes, rateLimitPause)), SubAgents: strp("3")}) f.stepUntil(t, "lane-1's implementer is out and lane-2 is at its implement stage", func(st State) bool { return len(st.Lanes) == 2 && len(st.Lanes[0].Awaits) == 1 && st.Lanes[1].Stage == StageImplement && len(st.Lanes[1].Awaits) == 0 }) @@ -74,7 +80,7 @@ func entries(st State, stage string) map[string][]string { func TestARateLimitMidLaneCheckpointsEveryLaneAndEndsTheWindow(t *testing.T) { f, res, head1 := rateLimitedPair(t) - until := f.now.Add(BundledPauseMinutes * time.Minute) + until := f.now.Add(rateLimitPause * time.Minute) // The window ends early, for the whole run, with the response named. if res.NextEligibleAt == nil || !res.NextEligibleAt.Equal(until) { From 63276545911743e0f17b766f634c2695764025bb Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Sat, 10 Oct 2026 10:13:22 +0100 Subject: [PATCH 4/4] chore: capture the pacing review's notes and defer three of them The reviewers of PR #886 passed it with notes below their bar. Five are captured: three small defects in the rate-limit and budget code, the budget check that no shipped runner can reach, and the runners that cannot recognise a rate limit. The three defects are deferred in DECISIONS.md with the reason: none is reachable without a second fault, and the drain's next steps rework the same code. Refs: itd-2609201925079472 Assisted-by: Claude:claude-opus-5-5 --- .abcd/work/DECISIONS.md | 1 + ...meets-a-rate-limit-and-whose-partial-return.md | 15 +++++++++++++++ ...-checkpoint-matches-an-await-by-its-receipt.md | 15 +++++++++++++++ ...-runner-recognises-a-rate-limit-an-opencode.md | 15 +++++++++++++++ ...dget-check-never-compares-anything-today-no.md | 15 +++++++++++++++ ...uota-query-has-no-deadline-and-its-error-is.md | 15 +++++++++++++++ 6 files changed, 76 insertions(+) create mode 100644 .abcd/work/issues/open/iss-2610100913086397-a-validator-that-meets-a-rate-limit-and-whose-partial-return.md create mode 100644 .abcd/work/issues/open/iss-2610100913088696-the-rate-limit-checkpoint-matches-an-await-by-its-receipt.md create mode 100644 .abcd/work/issues/open/iss-2610100913095172-only-the-claude-runner-recognises-a-rate-limit-an-opencode.md create mode 100644 .abcd/work/issues/open/iss-2610100913096346-the-run-start-budget-check-never-compares-anything-today-no.md create mode 100644 .abcd/work/issues/open/iss-2610100913096412-the-run-start-quota-query-has-no-deadline-and-its-error-is.md diff --git a/.abcd/work/DECISIONS.md b/.abcd/work/DECISIONS.md index 4a1a53a81..b868fea3e 100644 --- a/.abcd/work/DECISIONS.md +++ b/.abcd/work/DECISIONS.md @@ -2679,3 +2679,4 @@ together (the script's header says why there is no escape hatch). - 2026-10-09 — Rulings of the product thinker on the three hand-backs of the 2026-10-07/08 autonomous drain (asked by abcd-0b, one question at a time, answers verbatim): (1) iss-2609251618079479, whether the build-sequence chapter's build-milestone sense retires with the phase: "Rewrite in today's terms" — 06-delivery/01-build-sequence.md stays a current chapter, its build order restated as dependencies with no milestones (neither retired as history nor kept as a second sense of the word). (2) iss-2610040758569116, whether an agent may run 'abcd decide accept', which stamps the person's git name as accepted_by: "Agents too, disclosed" — no guard block as for 'source ledger --flip'; the commit's Assisted-by trailer is the disclosure. (3) iss-2610030956156354's open design point, whether a step still falls back to the agent harness when the size count already sent the brief to the person's own model server and the chat call then fails: "Move to the harness" — no answer received is the trigger, not nothing sent; recorded as iss-2610090701491123, since the resolved fix stops instead. - 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. diff --git a/.abcd/work/issues/open/iss-2610100913086397-a-validator-that-meets-a-rate-limit-and-whose-partial-return.md b/.abcd/work/issues/open/iss-2610100913086397-a-validator-that-meets-a-rate-limit-and-whose-partial-return.md new file mode 100644 index 000000000..4a4cf0650 --- /dev/null +++ b/.abcd/work/issues/open/iss-2610100913086397-a-validator-that-meets-a-rate-limit-and-whose-partial-return.md @@ -0,0 +1,15 @@ +--- +schema_version: 1 +id: "iss-2610100913086397" +slug: "a-validator-that-meets-a-rate-limit-and-whose-partial-return" +severity: "minor" +category: "bug" +source: "impl-review" +found_during: "review of PR #886 (itd-2609201925079472, run-2610100550297632)" +origin: researcher-authored +production_mode: hand-written +found_at: "internal/core/implement/loop/ratelimit.go" +remedy: "let --restart re-tell a kept validator await, or name a remedy the loop accepts, with a test that drives a refused save-aside to a resumed lane" +--- + +A validator that meets a rate limit and whose partial return cannot be saved aside leaves its lane stuck: the record tells the user to run abcd implement step --restart after the pause, but Restart refuses any lane without an implementer out, so every later step waits on validators that are gone. diff --git a/.abcd/work/issues/open/iss-2610100913088696-the-rate-limit-checkpoint-matches-an-await-by-its-receipt.md b/.abcd/work/issues/open/iss-2610100913088696-the-rate-limit-checkpoint-matches-an-await-by-its-receipt.md new file mode 100644 index 000000000..6fa20ee83 --- /dev/null +++ b/.abcd/work/issues/open/iss-2610100913088696-the-rate-limit-checkpoint-matches-an-await-by-its-receipt.md @@ -0,0 +1,15 @@ +--- +schema_version: 1 +id: "iss-2610100913088696" +slug: "the-rate-limit-checkpoint-matches-an-await-by-its-receipt" +severity: "nitpick" +category: "bug" +source: "impl-review" +found_during: "review of PR #886 (itd-2609201925079472, run-2610100550297632)" +origin: researcher-authored +production_mode: hand-written +found_at: "internal/core/implement/loop/ratelimit.go" +remedy: "match the await by its receipt path and its Since, as the comment says" +--- + +The rate-limit checkpoint matches an await by its receipt path alone, though its comment says a restarted await is skipped: Restart keeps the receipt path, so an await re-told while the dispatch was out is saved aside and reset again. diff --git a/.abcd/work/issues/open/iss-2610100913095172-only-the-claude-runner-recognises-a-rate-limit-an-opencode.md b/.abcd/work/issues/open/iss-2610100913095172-only-the-claude-runner-recognises-a-rate-limit-an-opencode.md new file mode 100644 index 000000000..dcb4220a9 --- /dev/null +++ b/.abcd/work/issues/open/iss-2610100913095172-only-the-claude-runner-recognises-a-rate-limit-an-opencode.md @@ -0,0 +1,15 @@ +--- +schema_version: 1 +id: "iss-2610100913095172" +slug: "only-the-claude-runner-recognises-a-rate-limit-an-opencode" +severity: "minor" +category: "future-work-seed" +source: "impl-review" +found_during: "review of PR #886 (itd-2609201925079472, run-2610100550297632)" +origin: researcher-authored +production_mode: hand-written +found_at: "internal/core/runner/claude.go" +remedy: "detect a rate limit in the opencode runner, and give the host a verb to report one a sub-agent met" +--- + +Only the claude runner recognises a rate limit: an opencode limit is an ordinary failure that falls back to the next runner, and a limit met by a host-run sub-agent is never seen by the loop, since no verb lets the host report one. diff --git a/.abcd/work/issues/open/iss-2610100913096346-the-run-start-budget-check-never-compares-anything-today-no.md b/.abcd/work/issues/open/iss-2610100913096346-the-run-start-budget-check-never-compares-anything-today-no.md new file mode 100644 index 000000000..fbbf1398e --- /dev/null +++ b/.abcd/work/issues/open/iss-2610100913096346-the-run-start-budget-check-never-compares-anything-today-no.md @@ -0,0 +1,15 @@ +--- +schema_version: 1 +id: "iss-2610100913096346" +slug: "the-run-start-budget-check-never-compares-anything-today-no" +severity: "minor" +category: "future-work-seed" +source: "impl-review" +found_during: "review of PR #886 (itd-2609201925079472, run-2610100550297632)" +origin: researcher-authored +production_mode: hand-written +found_at: "internal/core/implement/loop/budget.go" +remedy: "implement QuotaReporter for a runner that can report a quota, and count fix rounds in the estimate" +--- + +The run-start budget check never compares anything today: no shipped runner implements QuotaReporter, so every real run skips the check, and the estimate leaves out fix rounds, so it is a lower bound. diff --git a/.abcd/work/issues/open/iss-2610100913096412-the-run-start-quota-query-has-no-deadline-and-its-error-is.md b/.abcd/work/issues/open/iss-2610100913096412-the-run-start-quota-query-has-no-deadline-and-its-error-is.md new file mode 100644 index 000000000..21f4f9d2c --- /dev/null +++ b/.abcd/work/issues/open/iss-2610100913096412-the-run-start-quota-query-has-no-deadline-and-its-error-is.md @@ -0,0 +1,15 @@ +--- +schema_version: 1 +id: "iss-2610100913096412" +slug: "the-run-start-quota-query-has-no-deadline-and-its-error-is" +severity: "minor" +category: "security" +source: "impl-review" +found_during: "review of PR #886 (itd-2609201925079472, run-2610100550297632)" +origin: researcher-authored +production_mode: hand-written +found_at: "internal/core/implement/loop/budget.go" +remedy: "give the quota query a deadline and pass its error through fsutil.RedactHome like the loop's other record notes" +--- + +The run-start quota query has no deadline and its error is not home-redacted: Config.Quota runs on context.Background, so the first runner that reports a quota over I/O can hang build and drain at start, and its error reaches the run record and the checks row with the home path in it.