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
61 changes: 61 additions & 0 deletions .agent/handoff/202.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Handoff note: issue #202

- issue: 202
- started_at: 2026-10-11T10:30:00Z
- reason: token_soft_threshold
- last_work_commit: 43b5f51

## Summary

Implemented the `MAX_AUTO_CONTINUATIONS` core path (commit `43b5f51` on
`agent/issue-202`, on top of #201's merged HEAD `8d95b44`). `RuntimeConfig`
gains `max_auto_continuations` parsed from `.env` via a new
`_non_negative_integer` helper (default 0; invalid values raise
`ConfigurationError`); the repository profile still rejects it as an unknown
key. `GitHubTracker` accepts the budget, exposes
`is_auto_continuation_eligible` (zero budget short-circuits before any
comment reads), orders continuations before ordinary `ready-for-agent`
issues in `runnable_queue` (FIFO within groups; human requeues stay
ordinary), and revalidates at claim time in `claim_next` with a budget-aware
`ineligibility_reason` (`"automatic continuation limit reached (k/N)"` when
exhausted). `AgentLifecycle` threads the budget through `submit_next_issue`
and `_claim_prioritized_locked`, emits `auto_continuation_claimed` on FIFO
and priority automatic claims, and appends a standing line (`k/N`, plus a
human-requeue notice when the budget is exhausted) to handoff details once
at release — the publisher is untouched. `FakeTracker` gains `auto_counts`
plus `automatic_continuation_count`; `__main__.py` wires the setting
end-to-end. Lint (`ruff check`) is clean and the existing config/tracker
suites pass (104 tests).

## Remaining work

1. Write `tests/test_auto_continuations.py`: config defaults/validation +
profile refusal; queue ordering (continuations first, FIFO within groups,
human-requeued stays ordinary); `claim_next` picks in-budget / skips
over-budget (incl. claim-time relabel and count-change-between-reads);
`next issue` accept within budget + reject over budget with `(k/N)`;
handoff comment lines (progress + exhausted) and silence at N=0;
`auto_continuation_claimed` on FIFO + priority claims; N=2 e2e (two auto
continuations then wait; human requeue grants two more). Key harnesses:
`make_lifecycle`/`issue()` in `tests/test_next_issue.py`,
`FakeTransport`/`FakeCommentTransport`/`issue()` in
`tests/test_github_tracker.py`, marker helpers in
`simple_coding_agent/recovery.py` (`requeue-source: human|automatic`),
`_claim_prioritized_locked` semantics at `lifecycle.py:2869`.
2. Docs: new `docs/adr/0005-*.md`; update
`docs/agents/operator-onboarding.md`, `README.md`,
`docs/agents/triage-labels.md`, `.env.example`.
3. Run targeted test files, then the full suite once, then `/code-review`,
address findings, and commit (with `Co-Authored-By: Claude Code
<noreply@anthropic.com>` trailer) on `agent/issue-202`.

## Evidence

- Commit `43b5f51` (5 files, +267/-24): `simple_coding_agent/config.py`,
`github_tracker.py`, `lifecycle.py`, `__main__.py`, `tests/fakes.py`.
- Full prior-context transcript:
`/home/agent/.claude/projects/-data-repo/ce15b452-2e76-4739-b989-575fbcb9c956.jsonl`
- Verified: `ruff check simple_coding_agent/ tests/fakes.py` clean;
`pytest tests/test_config.py tests/test_github_tracker.py` → 104 passed.
- One fix applied during the attempt: removed an unused `READY_FOR_AGENT`
import in `lifecycle.py` (ruff F401).
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ TARGET_REPO=owner/repository
# MAX_RETRIES=3
# MAX_CONSECUTIVE_ERRORS=3

# Automatic continuation of handoff releases. With the default 0, continuing
# a `round-finished` issue always needs a human decision (re-applying
# `ready-for-agent`). With MAX_AUTO_CONTINUATIONS=N, the agent picks up a
# `round-finished` issue on its own, ahead of new `ready-for-agent` work, up
# to N automatic continuations in a row; a human requeue resets the count and
# grants another N rounds, and removing `round-finished` stops the sequence
# at any time. Operator-only: the repository profile refuses this key, so an
# agent-written PR can never raise its own round limit. See
# docs/adr/0005-automatic-continuation-budget.md.
# MAX_AUTO_CONTINUATIONS=0

# Repository profile commands (setup/check) run with a deliberately minimal
# PATH (os.defpath) that excludes the orchestrator's own environment. If a
# profile command needs a tool installed outside that default PATH (e.g. `uv`
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ The tracker selects only OPEN issues in `TARGET_REPO` that are labelled
GitHub blockers. It orders eligible issues FIFO by creation date and rechecks
eligibility immediately before assigning the authenticated GitHub identity.
That check-then-set claim assumes one process; it is not a distributed lock.
A handoff releases its issue labelled `round-finished`: with the operator
setting `MAX_AUTO_CONTINUATIONS=N` (default `0`), a `round-finished` issue
within budget is claimed automatically ahead of new work, up to N
continuations in a row before a human requeue is needed again.

Applying `ready-for-agent` is the human trust boundary: only repository
collaborators with write access may apply it. The issue body is passed to the
Expand All @@ -24,7 +28,9 @@ Copy `.env.example` and set `GITHUB_TOKEN`, `META_API_KEY`, and `TARGET_REPO`
(`owner/repo`). `DATA_DIR` defaults to `~/.simple-coding-agent/`; its default
clone location is `$DATA_DIR/repo/`. Other defaults are `POLL_INTERVAL=60`,
`LOG_LEVEL=INFO`, `MODEL_TIMEOUT=3600`, `PUBLISH_TIMEOUT=120`,
`MAX_RETRIES=3`, and `MAX_CONSECUTIVE_ERRORS=3`.
`MAX_RETRIES=3`, `MAX_CONSECUTIVE_ERRORS=3`, and `MAX_AUTO_CONTINUATIONS=0`
(no automatic continuation of `round-finished` issues without a human
requeue).

Use a fine-grained GitHub PAT limited to the configured repository with only
`contents:write`, `pull-requests:write`, and `issues:write`. Configure provider
Expand Down
61 changes: 61 additions & 0 deletions docs/adr/0005-automatic-continuation-budget.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Let the agent continue a handoff on its own within an operator-set budget

A `handoff` outcome releases the issue labelled `round-finished`: until now,
continuing always required a human decision — review the handoff note and
re-apply `ready-for-agent` to requeue, or leave the issue labelled to end the
sequence. We decided the operator may instead grant the agent up to N
automatic continuations in a row (`MAX_AUTO_CONTINUATIONS`, default `0`),
so a started branch does not wait behind new work while a human requeues
every round. A human requeue resets the count and grants another N rounds;
the default `0` keeps today's behaviour exactly.

Automatic continuation reuses the existing claim path unchanged: the issue
must be open and unassigned, have a non-empty body and no open native
blockers (the same rules as the runnable queue), plus carry only
`round-finished` (never alongside `ready-for-agent`, which stays an ordinary
human requeue) with a count below the budget. Claiming assigns, removes
`round-finished`, and restores the branch with the handoff note in the
starting prompt. Eligible continuations are claimed before ordinary
`ready-for-agent` issues, FIFO by creation date within each group. The
operator `next issue` command accepts a `round-finished` target within
budget and rejects an over-budget one naming the limit (`k/N`); every
handoff result comment reports where the issue stands (`k/N`, and whether a
human requeue is now needed).

The brakes that already exist stay in place, which is why a bounded loop is
safe to run unattended. A handoff with zero commits becomes `no_changes`
and never re-adds `round-finished`, so an issue that produces nothing stops
instead of looping. A human can stop automatic continuation at any time by
removing `round-finished`. The count itself is derived from the
requeue-source markers in the agent's own result comments (see #201), so a
human-stamped result always resets it — the agent cannot drift past the
budget by relabelling, only a human requeue grants more rounds.

## Considered Options

- **Keep requiring a human requeue after every handoff.** Rejected: each
round-trip waits on a human for a decision that is usually "keep going",
and a started branch with a restored context queues behind fresh work.
The default `0` preserves this behaviour for operators who want it.
- **Continue handoffs without any limit once enabled.** Rejected: an issue
whose work never converges would hand off forever with no human looking
at the notes. The budget bounds the unattended stretch; the handoff that
uses it up says a human requeue is needed.
- **Allow the limit in the repository profile.** Rejected for the same
reason as ADR 0004: the profile lives in the target repository, so an
agent-written PR could raise its own round limit. `MAX_AUTO_CONTINUATIONS`
is read only from the operator's `.env`, and the profile keeps rejecting
unknown keys. A non-negative integer is required; anything else fails at
startup with a configuration error.

## Consequences

- Operators opt in with `MAX_AUTO_CONTINUATIONS=N`; `0` (the default)
disables the path with no extra GitHub calls and byte-identical comments.
- With `N > 0`, `round-finished` issues within budget join the runnable
queue ahead of new work, each claim emits an `auto_continuation_claimed`
event, and each handoff comment carries the `k/N` standing line.
- A human requeue (re-applying `ready-for-agent`) resets the count; removing
`round-finished` stops the sequence at any time.
- No new token scope is needed: eligibility reads reuse the issue and
comment reads the tracker already performs.
20 changes: 16 additions & 4 deletions docs/agents/operator-onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,22 @@ without a cooperative handoff note likewise ends in an emergency handoff

A `handoff` outcome pushes the attempt branch (no pull request), posts the
handoff note as an issue comment, removes `ready-for-agent`, and adds
`round-finished`. **Continuation requires a human decision**: review the
note and comment, then re-apply `ready-for-agent` to requeue the issue (this
also clears `round-finished`), or leave it labelled `round-finished` to end
the attempt sequence.
`round-finished`. **By default, continuation requires a human decision**:
review the note and comment, then re-apply `ready-for-agent` to requeue the
issue (this also clears `round-finished`), or leave it labelled
`round-finished` to end the attempt sequence.

`MAX_AUTO_CONTINUATIONS` (default `0`) optionally lets the agent continue a
`round-finished` issue on its own, up to N automatic continuations in a row,
claimed ahead of new `ready-for-agent` work. A human requeue resets the
count and grants another N rounds; removing `round-finished` stops the
sequence at any time. Every handoff result comment then reports where the
issue stands (`k/N used`, and whether a human requeue is now needed), and
each automatic claim emits an `auto_continuation_claimed` event. The setting
is operator-only on purpose: it lives in `.env`, never in the repository
profile (an unknown profile key is refused), so an agent-written PR cannot
raise its own round limit. See
[ADR 0005](../adr/0005-automatic-continuation-budget.md).

Independent of the handoff path, any dirty or untracked change left in the
working tree once the model reports success is also committed automatically
Expand Down
12 changes: 12 additions & 0 deletions docs/agents/triage-labels.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ The skills speak in terms of five canonical triage roles. This file maps those r

When a skill mentions a role, use the corresponding label string from this table.

## `round-finished` continuation

`round-finished` is not a triage role: the agent applies it when a handoff
releases the issue (removing `ready-for-agent` and the assignment). With the
default `MAX_AUTO_CONTINUATIONS=0`, the issue waits there until a human
re-applies `ready-for-agent` to requeue it. With `MAX_AUTO_CONTINUATIONS=N`,
the agent claims a `round-finished` issue on its own — ahead of new
`ready-for-agent` work — up to N automatic continuations in a row; the
handoff that uses up the budget says a human requeue is needed. Re-applying
`ready-for-agent` resets the count and grants another N rounds, while
removing `round-finished` stops automatic continuation at any time.

## `agent-auto-merge` opt-in

The `agent-auto-merge` label opts one implementation issue into pull-request
Expand Down
5 changes: 4 additions & 1 deletion simple_coding_agent/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ def _attempt_sink(issue_number: int) -> AttemptArchive | None:
f"https://github.com/{config.target_repo}.git",
token_provider=lambda: config.github_token,
)
tracker = GitHubTracker(github, config.target_repo)
tracker = GitHubTracker(
github, config.target_repo, max_auto_continuations=config.max_auto_continuations
)
runner = ModelAttemptRunner(
attempt_state=attempt_state,
workspace=workspace,
Expand Down Expand Up @@ -115,6 +117,7 @@ def _attempt_sink(issue_number: int) -> AttemptArchive | None:
poll_interval=config.poll_interval,
error_store=ConsecutiveErrorStore(config.data_dir),
max_consecutive_errors=config.max_consecutive_errors,
max_auto_continuations=config.max_auto_continuations,
event_log=lambda event, detail="", level="INFO", issue_number=None: logger.emit(
event, phase="polling", detail=detail, level=level, issue_number=issue_number
),
Expand Down
13 changes: 13 additions & 0 deletions simple_coding_agent/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class RuntimeConfig:
model_stream_idle_timeout_ms: int = _DEFAULT_MODEL_STREAM_IDLE_TIMEOUT_MS
pr_auto_merge: str = "false"
pr_merge_method: str = "merge"
max_auto_continuations: int = 0

@property
def soft_threshold_tokens(self) -> int:
Expand Down Expand Up @@ -160,6 +161,7 @@ def load_runtime_config(environ: Mapping[str, str] | None = None) -> RuntimeConf
soft_threshold_percentage=_fraction(values, "SOFT_THRESHOLD_PERCENTAGE", 0.2),
pr_auto_merge=_auto_merge_mode(values),
pr_merge_method=_merge_method(values),
max_auto_continuations=_non_negative_integer(values, "MAX_AUTO_CONTINUATIONS", 0),
)
_warn_when_claude_code_version_is_set(values)
_warn_when_usd_backstop_is_tighter(config)
Expand Down Expand Up @@ -279,6 +281,17 @@ def _positive_integer(values: Mapping[str, str], name: str, default: int) -> int
return number


def _non_negative_integer(values: Mapping[str, str], name: str, default: int) -> int:
value = values.get(name, str(default))
try:
number = int(str(value).strip())
except ValueError as error:
raise ConfigurationError(f"{name} must be a non-negative integer") from error
if number < 0:
raise ConfigurationError(f"{name} must be a non-negative integer")
return number


def _fraction(values: Mapping[str, str], name: str, default: float) -> float:
value = values.get(name, str(default))
try:
Expand Down
Loading
Loading