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
4 changes: 2 additions & 2 deletions .abcd/development/brief/04-surfaces/34-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,8 @@ the build's own pace, sub-agent and fix-round flags, the pace written as
`<work-minutes>/<pause-minutes>`; `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
Expand Down
2 changes: 2 additions & 0 deletions .abcd/work/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2677,4 +2677,6 @@ 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.
- 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.
Original file line number Diff line number Diff line change
@@ -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).
Original file line number Diff line number Diff line change
@@ -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 <lane> after the pause, but Restart refuses any lane without an implementer out, so every later step waits on validators that are gone.
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion commands/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/cli/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <work-minutes>/<pause-minutes>,
--sub-agents <n> and --fix-rounds <n> 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
Expand Down
10 changes: 5 additions & 5 deletions internal/core/implement/loop/pace.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down
8 changes: 4 additions & 4 deletions internal/core/implement/loop/pace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(""))
Expand All @@ -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 {
Expand Down
10 changes: 8 additions & 2 deletions internal/core/implement/loop/ratelimit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"bytes"
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
Expand All @@ -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":{}}}`

Expand All @@ -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
})
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion internal/surface/cli/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ func newBuildCommand(asJSON *bool) *cobra.Command {
"four numbers are read once, when the run starts: --pace <work-minutes>/<pause-minutes>,\n" +
"--sub-agents <n> and --fix-rounds <n> 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" +
Expand Down
Loading
Loading