From 645aed5fe868f1719578b424e9455c0a43b037c5 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Sun, 11 Oct 2026 14:02:53 +0000 Subject: [PATCH 1/3] Token soft-threshold two-call handoff with state snapshot --- simple_coding_agent/lifecycle.py | 20 ++- simple_coding_agent/model_execution.py | 213 ++++++++++++++++++++++++- skills/handoff/SKILL.md | 28 +++- tests/test_handoff_end_to_end.py | 58 +++++++ tests/test_model_execution.py | 179 +++++++++++++++++++++ 5 files changed, 490 insertions(+), 8 deletions(-) diff --git a/simple_coding_agent/lifecycle.py b/simple_coding_agent/lifecycle.py index e45705a..c3ea643 100644 --- a/simple_coding_agent/lifecycle.py +++ b/simple_coding_agent/lifecycle.py @@ -411,6 +411,8 @@ def __call__(self, claim: Claim, profile: RepositoryProfile, prepared: PreparedA issue_number=claim.issue.number, archive=archive, attempt_id=attempt_id, + branch=prepared.branch, + base_revision=prepared.base_revision, ) ) self._archive_cli_transcripts(archive, claim.issue.number) @@ -3914,15 +3916,31 @@ def _execute_model( issue_number: int | None, archive: object | None, attempt_id: str | None, + branch: str | None = None, + base_revision: str | None = None, ) -> Any: """Invoke the model executor, passing the attempt id when it accepts it. Older executor doubles in tests predate the ``attempt_id`` seam: they raise ``TypeError`` on the unexpected keyword. Fall back to the old - call shape for them instead of failing the attempt. + call shape for them instead of failing the attempt. The branch/base + seam (for the token soft-threshold state snapshot) falls back the same + way. """ execute = model_executor.execute # type: ignore[attr-defined] + try: + return execute( + issue_body=issue_body, + working_directory=working_directory, + issue_number=issue_number, + archive=archive, + attempt_id=attempt_id, + branch=branch, + base_revision=base_revision, + ) + except TypeError: + pass try: return execute( issue_body=issue_body, diff --git a/simple_coding_agent/model_execution.py b/simple_coding_agent/model_execution.py index 67087e7..bc4d83b 100644 --- a/simple_coding_agent/model_execution.py +++ b/simple_coding_agent/model_execution.py @@ -11,6 +11,7 @@ from pathlib import Path import re import shlex +import subprocess from typing import Any, Protocol from claude_agent_sdk import ( @@ -97,27 +98,185 @@ def _attempt_id_sentence(attempt_id: str | None) -> str: ) -def _cost_handoff_instruction( - issue_number: int | None, attempt_id: str | None = None +#: How many lines of each git output the token soft-threshold snapshot +#: carries before capping it with a ``+N more`` line. Forty lines fit the +#: branch state the model needs (short status, oneline log, diff stat) +#: without spending the reserve the handoff itself must write in. +_SNAPSHOT_LINE_LIMIT = 40 + + +def _cap_snapshot_lines(text: str, limit: int = _SNAPSHOT_LINE_LIMIT) -> str: + """Cap ``text`` at ``limit`` lines, appending a ``+N more`` line on overflow.""" + + lines = text.splitlines() + if len(lines) <= limit: + return text + kept = lines[:limit] + return "\n".join([*kept, f"...+{len(lines) - limit} more"]) + + +def _run_snapshot_git(working_directory: Path, *arguments: str) -> str | None: + """Best-effort git output for the handoff snapshot; ``None`` when unavailable.""" + + try: + completed = subprocess.run( + ("git", *arguments), + cwd=working_directory, + capture_output=True, + text=True, + timeout=30, + ) + except Exception: + return None + if completed.returncode != 0: + return None + return completed.stdout.strip() + + +def _snapshot_section(title: str, output: str | None, empty: str) -> str: + """Render one capped git section of the handoff snapshot.""" + + if output is None: + return f"{title}:\n(unavailable)" + body = output.strip() or empty + return f"{title}:\n{_cap_snapshot_lines(body)}" + + +def _token_handoff_prefill( + issue_number: int | None, attempt_id: str | None, head_sha: str | None +) -> str: + """The note header the model must copy verbatim into its handoff note.""" + + issue_label = str(issue_number) if issue_number is not None else "" + lines = [ + f"# Handoff note: issue #{issue_label}", + "", + f"- issue: {issue_label}", + ] + if attempt_id is not None: + lines.append(f"- started_at: {attempt_id}") + lines.append("- reason: token_soft_threshold") + lines.append(f"- last_work_commit: {head_sha or '(unknown)'}") + return "\n".join(lines) + + +def _token_handoff_snapshot( + working_directory: Path | None, + *, + branch: str | None, + base_revision: str | None, + issue_number: int | None, + attempt_id: str | None, ) -> str: + """Collect the branch state the two-call token handoff needs. + + Taken at injection time so the model never spends its remaining turns + on orientation commands: the current branch, the base revision, the + current HEAD SHA, ``git status --short``, ``git log --oneline + ..HEAD`` and ``git diff --stat `` (each capped at + ``_SNAPSHOT_LINE_LIMIT`` lines), the attempt's ``started_at``, and a + pre-filled note header whose ``last_work_commit`` is HEAD at injection + time. Anything git cannot report degrades to ``(unavailable)`` instead + of failing: the handoff must still be writable. + """ + + if working_directory is None: + head_sha: str | None = None + status = log = diff_stat = None + current_branch = branch or "(unknown)" + else: + shown_branch = _run_snapshot_git(working_directory, "branch", "--show-current") + current_branch = (shown_branch or "").strip() or branch or "(unknown)" + head_raw = _run_snapshot_git(working_directory, "rev-parse", "HEAD") + head_sha = (head_raw or "").strip() or None + status = _run_snapshot_git(working_directory, "status", "--short") + if base_revision: + log = _run_snapshot_git( + working_directory, "log", "--oneline", f"{base_revision}..HEAD" + ) + diff_stat = _run_snapshot_git( + working_directory, "diff", "--stat", base_revision + ) + else: + log = diff_stat = None + note_label = str(issue_number) if issue_number is not None else "" + started = attempt_id if attempt_id is not None else "(none given; omit started_at)" + parts = [ + f"Branch: {current_branch}", + f"Base revision: {base_revision or '(unknown)'}", + f"HEAD: {head_sha or '(unavailable)'}", + _snapshot_section("git status --short", status, "(clean)"), + ( + "git log --oneline ..HEAD:\n(unknown base revision; log unavailable)" + if base_revision is None + else _snapshot_section("git log --oneline ..HEAD", log, "(no commits)") + ), + ( + "git diff --stat :\n(unknown base revision; diff unavailable)" + if base_revision is None + else _snapshot_section("git diff --stat ", diff_stat, "(no differences)") + ), + f"started_at: {started}", + ( + f"Pre-filled note header (copy verbatim, then fill in Summary," + f" Remaining work and Evidence):\n" + f"{_token_handoff_prefill(issue_number, attempt_id, head_sha)}" + ), + f"The handoff note path is exactly `.agent/handoff/{note_label}.md`.", + ] + return "\n\n".join(parts) + + +def _token_handoff_steps(issue_number: int | None) -> str: + """The only five steps the token soft-threshold handoff may take.""" + + note_label = str(issue_number) if issue_number is not None else "" return ( + "Do exactly the following and nothing else:\n" + "1. Do not run diagnostic commands; the state above is current.\n" + "2. Do not commit work yourself. Sealing will commit any uncommitted" + " work after the attempt.\n" + "3. Write the handoff note using the pre-filled header above verbatim," + " and fill in Summary, Remaining work and Evidence.\n" + f"4. In one `Bash` call, run `git add .agent/handoff/{note_label}.md" + f" && git commit -m \"Handoff note: issue #{note_label}\"`.\n" + "5. Stop." + ) + + +def _cost_handoff_instruction( + issue_number: int | None, + attempt_id: str | None = None, + *, + snapshot: str | None = None, +) -> str: + base = ( "This attempt is approaching its token budget. Invoke the `handoff` skill now " "to preserve your progress cooperatively instead of continuing further work. " f"{_HANDOFF_SKILL_TOOL_HINT} {_handoff_target(issue_number)}" f"{_attempt_id_sentence(attempt_id)}" ) + if snapshot is None: + return base + return f"{base}\n\n{snapshot}\n\n{_token_handoff_steps(issue_number)}" def _cost_handoff_followup_prompt( - issue_number: int | None, attempt_id: str | None = None + issue_number: int | None, + attempt_id: str | None = None, + *, + snapshot: str | None = None, ) -> str: - return ( + base = ( "This attempt crossed its token soft threshold and must now end by handing off. " "Invoke the `handoff` skill now to preserve your progress: commit any outstanding " "work, then write and commit the handoff note. Do not attempt further implementation " f"work. {_HANDOFF_SKILL_TOOL_HINT} {_handoff_target(issue_number)}" f"{_attempt_id_sentence(attempt_id)}" ) + if snapshot is None: + return base + return f"{base}\n\n{snapshot}\n\n{_token_handoff_steps(issue_number)}" def _operator_handoff_instruction( @@ -341,6 +500,10 @@ def __init__( self._soft_threshold_crossed_at_tokens: float | None = None self._total_cost_usd: float | None = None self._handoff_context_delivered = False + self._working_directory: Path | None = None + self._attempt_branch: str | None = None + self._attempt_base_revision: str | None = None + self._token_handoff_snapshot: str | None = None self._running_subagents: set[str] = set() self._stopped_subagent_ids: list[str] = [] self._usage_shape_logged = False @@ -769,6 +932,8 @@ async def execute( issue_number: int | None = None, archive: EvidenceWriter | None = None, attempt_id: str | None = None, + branch: str | None = None, + base_revision: str | None = None, ) -> ModelExecution: """Dispatch ``/implement`` and classify its one terminal SDK result.""" @@ -785,6 +950,10 @@ async def execute( self._total_cost_usd = None self._token_hard_ceiling_reached = False self._handoff_context_delivered = False + self._working_directory = working_directory + self._attempt_branch = branch + self._attempt_base_revision = base_revision + self._token_handoff_snapshot = None self._running_subagents = set() self._stopped_subagent_ids = [] self._usage_shape_logged = False @@ -855,7 +1024,9 @@ async def execute( client, observed_models, prompt=_cost_handoff_followup_prompt( - self._issue_number, self._attempt_id + self._issue_number, + self._attempt_id, + snapshot=self._token_followup_snapshot(), ), success_explanation=( "Model invoked the handoff skill after a token" @@ -939,6 +1110,31 @@ def _main_thread_handoff_invoked(self) -> bool: for event in self._skill_events ) + def _build_token_snapshot(self) -> str: + """Collect the branch state for the two-call token handoff, now.""" + + return _token_handoff_snapshot( + self._working_directory, + branch=self._attempt_branch, + base_revision=self._attempt_base_revision, + issue_number=self._issue_number, + attempt_id=self._attempt_id, + ) + + def _token_followup_snapshot(self) -> str: + """The injection-time snapshot, or a fresh one when never injected. + + The follow-up prompt carries the state taken at injection time, so + the model still sees the branch as it was when the threshold + crossed even if the injection raced a commit. When no injection + happened (the crossing landed on the terminal result itself), the + snapshot is collected fresh. + """ + + if self._token_handoff_snapshot is None: + self._token_handoff_snapshot = self._build_token_snapshot() + return self._token_handoff_snapshot + def _cost_followup_blocked(self) -> bool: """Whether the cost follow-up would bypass the hard token limit.""" @@ -1975,8 +2171,13 @@ async def _record_post_tool_use( # frees the remaining budget for the handoff. self._handoff_context_delivered = True self._log("token_soft_threshold_handoff_context_injected") + self._token_handoff_snapshot = self._build_token_snapshot() contexts.append( - _cost_handoff_instruction(self._issue_number, self._attempt_id) + _cost_handoff_instruction( + self._issue_number, + self._attempt_id, + snapshot=self._token_handoff_snapshot, + ) ) else: # Subagents are still recorded as running (for example the diff --git a/skills/handoff/SKILL.md b/skills/handoff/SKILL.md index bb6d3e2..14deba8 100644 --- a/skills/handoff/SKILL.md +++ b/skills/handoff/SKILL.md @@ -12,7 +12,33 @@ The driving process owns remote publication (push, PR, comments, labels). This skill only commits locally. Never run `git push`, `gh pr create`, or any other publishing command. -## Steps +## Token soft-threshold handoff (two tool calls) + +When the handoff was triggered by a token soft-threshold notice, the +instruction already supplies the current branch state: the branch, the base +revision, the HEAD SHA, `git status --short`, `git log --oneline +..HEAD`, `git diff --stat `, and a pre-filled note header whose +`last_work_commit` is HEAD at injection time. Trust that state; do not run +diagnostic commands to re-derive it. + +The handoff then takes at most two tool calls, and work is not committed in +this case: + +1. Write `.agent/handoff/.md` using the pre-filled header + verbatim, and fill in Summary, Remaining work and Evidence. +2. In one `Bash` call, run + `git add .agent/handoff/.md && git commit -m "Handoff note: issue #"`. +3. Stop. + +Do not commit work yourself on this path: sealing commits any uncommitted +work after the attempt, so the branch ends as the earlier work commits, then +the note commit, then (if there was uncommitted work) the seal commit. + +## Steps (operator and turn/time-limit handoffs) + +This is the documented path for operator `handoff now` requests and for +turn/time-limit follow-ups. (Token soft-threshold handoffs use the two-call +path above instead and skip step 1.) 1. **Preserve outstanding work.** If there is any dirty or untracked change in the working directory, commit it now, as one or more ordinary work diff --git a/tests/test_handoff_end_to_end.py b/tests/test_handoff_end_to_end.py index 93c2e6e..3d2037c 100644 --- a/tests/test_handoff_end_to_end.py +++ b/tests/test_handoff_end_to_end.py @@ -1165,3 +1165,61 @@ def remote_has_branch(remote: Path, branch: str) -> bool: def git(cwd: Path, *arguments: str) -> str: env = dict(os.environ, GIT_EDITOR="true") return subprocess.run(("git", *arguments), cwd=cwd, env=env, check=True, text=True, capture_output=True).stdout.strip() + + +# --- Scenario 11e: token soft-threshold two-call handoff ----------------------- +# +# The model follows the injected snapshot: it writes the note with the +# supplied header (``last_work_commit`` is HEAD at injection time) and +# commits only the note, leaving its dirty work uncommitted. Sealing then +# commits that work on top of the note. The note + seal layout must still +# pass note validation and publish as a ``handoff`` outcome. + + +def test_token_handoff_note_only_commit_with_dirty_work_seals_on_top(tmp_path: Path) -> None: + remote, _ = repository_with_main(tmp_path) + github = FakeGitHub(issue(24, body="Rewrite the parser.", labels=frozenset({"ready-for-agent"}))) + clone_dir = tmp_path / "clone" + + def do_token_handoff(working_directory: Path) -> None: + # Work committed before the threshold, as usual. + write_and_commit(working_directory, "parser.py", "half done", "Half-finish the parser") + # Injection-time HEAD: the supplied header points here. + head = git(working_directory, "rev-parse", "HEAD") + # The model writes the note with the supplied header verbatim and + # commits only the note in one Bash call, leaving dirty work behind. + note = ( + "# Handoff note: issue #24\n\n" + "- issue: 24\n" + "- started_at: 2026-09-22T00:00:00Z\n" + "- reason: token_soft_threshold\n" + f"- last_work_commit: {head}\n\n" + "## Summary\n\nHalfway done.\n\n" + "## Remaining work\n\nFinish the parser.\n\n" + "## Evidence\n\nparser.py\n" + ) + note_path = working_directory / ".agent" / "handoff" / "24.md" + note_path.parent.mkdir(parents=True, exist_ok=True) + note_path.write_text(note) + git(working_directory, "add", ".agent/handoff/24.md") + git(working_directory, "commit", "-m", "Handoff note: issue #24") + # Dirty work the model must not commit itself: sealing owns it. + (working_directory / "parser.py").write_text("half done, plus uncommitted edits") + + executor = FakeModelExecutor(actions=[do_token_handoff], handoff=True) + lifecycle = build_lifecycle(remote, clone_dir, tmp_path / "data", github, executor) + + result = lifecycle.run_once() + + assert result.outcome is AttemptOutcome.HANDOFF + assert github.pull_requests == {} + assert github.added_labels == [("issue-24", "round-finished")] + subjects = git(clone_dir, "log", "agent/issue-24", "--format=%s").splitlines() + assert subjects[:3] == [ + "Seal attempt work for #24 (handoff_requested)", + "Handoff note: issue #24", + "Half-finish the parser", + ] + [comment] = [c for c in github.comments if "Agent Attempt Result: handoff" in c.body] + assert "Halfway done." in comment.body + assert git(clone_dir, "show", "agent/issue-24:parser.py") == "half done, plus uncommitted edits" diff --git a/tests/test_model_execution.py b/tests/test_model_execution.py index 56ae13a..5221a24 100644 --- a/tests/test_model_execution.py +++ b/tests/test_model_execution.py @@ -2813,3 +2813,182 @@ def run(messages: list[object]): assert logged_a == ["thread=main; trigger=auto; pre_tokens=150000"] assert logged_b == ["thread=main; trigger=None; pre_tokens=None"] assert with_meta.token_budget == baseline.token_budget == without_meta.token_budget + + +# --- Token soft-threshold two-call handoff (state snapshot) ------------------- + +import os as _os +import subprocess as _subprocess + +_ATTEMPT_ID = "2026-10-08T17:29:39.642231Z" + + +def _git(cwd: Path, *arguments: str) -> str: + env = dict(_os.environ, GIT_EDITOR="true") + return _subprocess.run( + ("git", *arguments), cwd=cwd, env=env, check=True, text=True, capture_output=True + ).stdout.strip() + + +def _snapshot_repo(tmp_path: Path, *, issue_number: int = 149) -> tuple[Path, str, str]: + """A git repo with a base commit, one work commit, and one dirty file.""" + + repo = tmp_path / "repo" + _git(tmp_path, "init", str(repo)) + _git(repo, "config", "user.name", "Test User") + _git(repo, "config", "user.email", "test@example.com") + _git(repo, "checkout", "-b", "main") + (repo / "base.txt").write_text("base\n") + _git(repo, "add", "base.txt") + _git(repo, "commit", "-m", "Initial commit") + base = _git(repo, "rev-parse", "HEAD") + _git(repo, "checkout", "-b", f"agent/issue-{issue_number}") + (repo / "work.txt").write_text("half done\n") + _git(repo, "add", "work.txt") + _git(repo, "commit", "-m", "Half-finish the work") + (repo / "dirty.txt").write_text("uncommitted\n") + return repo, base, _git(repo, "rev-parse", "HEAD") + + +def test_token_snapshot_carries_branch_state_and_prefilled_header(tmp_path: Path) -> None: + from simple_coding_agent.model_execution import _token_handoff_snapshot + + repo, base, head = _snapshot_repo(tmp_path) + + snapshot = _token_handoff_snapshot( + repo, + branch="agent/issue-149", + base_revision=base, + issue_number=149, + attempt_id=_ATTEMPT_ID, + ) + + assert f"agent/issue-149" in snapshot + assert base in snapshot + assert head in snapshot + assert "dirty.txt" in snapshot # git status --short + assert "Half-finish the work" in snapshot # git log --oneline ..HEAD + assert "work.txt" in snapshot # git diff --stat + assert _ATTEMPT_ID in snapshot + assert "# Handoff note: issue #149" in snapshot + assert "- issue: 149" in snapshot + assert "- reason: token_soft_threshold" in snapshot + assert f"- last_work_commit: {head}" in snapshot + + +def test_token_snapshot_caps_each_git_output_at_forty_lines(tmp_path: Path) -> None: + from simple_coding_agent.model_execution import ( + _SNAPSHOT_LINE_LIMIT, + _cap_snapshot_lines, + _token_handoff_snapshot, + ) + + assert _SNAPSHOT_LINE_LIMIT == 40 + assert _cap_snapshot_lines("\n".join(f"l{i}" for i in range(40))) .count("\n") == 39 + capped = _cap_snapshot_lines("\n".join(f"l{i}" for i in range(65))) + assert capped.splitlines()[-1] == "...+25 more" + assert len(capped.splitlines()) == 41 + + repo = tmp_path / "repo" + _git(tmp_path, "init", str(repo)) + _git(repo, "config", "user.name", "Test User") + _git(repo, "config", "user.email", "test@example.com") + _git(repo, "checkout", "-b", "main") + (repo / "base.txt").write_text("base\n") + _git(repo, "add", "base.txt") + _git(repo, "commit", "-m", "Initial commit") + base = _git(repo, "rev-parse", "HEAD") + for index in range(60): + (repo / f"untracked-{index:02d}.txt").write_text("noise\n") + + snapshot = _token_handoff_snapshot( + repo, branch="main", base_revision=base, issue_number=149, attempt_id=_ATTEMPT_ID + ) + + status_section = snapshot.split("git status --short:\n")[1].split("\n\n")[0] + assert status_section.splitlines()[-1] == "...+20 more" + assert len(status_section.splitlines()) == 41 + + +def test_token_instruction_and_followup_share_snapshot_and_two_call_steps( + tmp_path: Path, +) -> None: + from simple_coding_agent.model_execution import ( + _cost_handoff_followup_prompt, + _cost_handoff_instruction, + ) + + repo, base, head = _snapshot_repo(tmp_path) + + captured: list[FakeClient] = [] + + def client_factory(options: object) -> FakeClient: + client = FakeClient(options, [result()]) + captured.append(client) + return client + + executor = ModelExecutor( + replace(runtime_config(tmp_path), max_budget_tokens=1_000_000), + client_factory=client_factory, + ) + asyncio.run( + executor.execute( + issue_body="Fix it.", + working_directory=repo, + issue_number=149, + attempt_id=_ATTEMPT_ID, + branch="agent/issue-149", + base_revision=base, + ) + ) + executor._ledger.start_thread("sub-1", kind="subagent", prompt_chars=0) + executor._ledger_threads.add("sub-1") + executor._ledger.observe_response("sub-1", "r-big", 0) + executor._ledger.observe_usage("sub-1", "r-big", {"input_tokens": 900_000}) + executor._check_limits("sub-1") + post_hook = captured[0].options.hooks["PostToolUse"][0].hooks[0] + notice = asyncio.run( + post_hook( + {"tool_name": "Bash", "tool_input": {"command": "pytest"}, "tool_response": "ok"}, + None, + {}, + ) + )["hookSpecificOutput"]["additionalContext"] + + followup = _cost_handoff_followup_prompt( + 149, _ATTEMPT_ID, snapshot=executor._token_followup_snapshot() + ) + plain_instruction = _cost_handoff_instruction(149, _ATTEMPT_ID) + + for text in (notice, followup): + assert "agent/issue-149" in text + assert base in text + assert head in text + assert "dirty.txt" in text + assert "Half-finish the work" in text + assert _ATTEMPT_ID in text + assert f"- last_work_commit: {head}" in text + assert "Do not run diagnostic commands" in text + assert "Do not commit work yourself" in text + assert "Sealing will commit any uncommitted work" in text + assert "pre-filled header" in text + assert "Summary, Remaining work and Evidence" in text + assert 'git add .agent/handoff/149.md && git commit -m "Handoff note: issue #149"' in text + # The follow-up reuses the injection-time snapshot instead of re-reading git. + assert executor._token_followup_snapshot() in followup + assert executor._token_followup_snapshot() in notice + # Without a snapshot the prompts keep their legacy wording. + assert "Do not run diagnostic commands" not in plain_instruction + assert "handoff" in plain_instruction + + +def test_token_snapshot_degrades_outside_a_git_repo(tmp_path: Path) -> None: + from simple_coding_agent.model_execution import _token_handoff_snapshot + + snapshot = _token_handoff_snapshot( + tmp_path, branch=None, base_revision=None, issue_number=None, attempt_id=None + ) + + assert "(unknown)" in snapshot + assert "(unavailable)" in snapshot + assert ".agent/handoff/.md" in snapshot From 6472266f627780709b090b7d221c8f9deff0c475 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Sun, 11 Oct 2026 14:03:11 +0000 Subject: [PATCH 2/3] Handoff note: issue #179 --- .agent/handoff/179.md | 53 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .agent/handoff/179.md diff --git a/.agent/handoff/179.md b/.agent/handoff/179.md new file mode 100644 index 0000000..01ed22c --- /dev/null +++ b/.agent/handoff/179.md @@ -0,0 +1,53 @@ +# Handoff note: issue #179 + +- issue: 179 +- reason: token_soft_threshold +- last_work_commit: b6fa15edee0af58b87deb7dd8b83e8ac8a3ae230 + +## Summary + +Implemented the token soft-threshold two-call handoff. The runtime now injects +a state snapshot (branch, base revision, HEAD SHA, `git status --short`, +`git log --oneline ..HEAD`, `git diff --stat ` each capped at 40 +lines with a `+N more` line, `started_at`, and a pre-filled note header whose +`last_work_commit` is HEAD at injection time) into both the soft-threshold +handoff context and its one follow-up prompt, with instructions to write the +note and commit it in one `Bash` call and stop. `skills/handoff/SKILL.md` +gained the matching two-call section; operator and turn/time-limit prompts are +unchanged. Work is committed as b6fa15e; this note commit is separate. + +## Remaining work + +1. One-line lint fix (could not apply: file edits were denied once the outer + token soft-threshold guard engaged): in `tests/test_model_execution.py` + (`test_token_snapshot_carries_branch_state_and_prefilled_header`), change + `assert f"agent/issue-149" in snapshot` to `assert "agent/issue-149" in + snapshot` (ruff F541, f-string without placeholders). +2. Run the FULL test suite once (`python -m pytest`). Only the relevant + suites were run before the handoff boundary; all passed (see Evidence). +3. Run `/code-review` on the branch, per the implement instructions (skipped + due to the handoff). +4. Publication (push, PR, comments, labels) is owned by the driving process. + +## Evidence + +- `simple_coding_agent/model_execution.py`: `_SNAPSHOT_LINE_LIMIT`, + `_cap_snapshot_lines`, `_run_snapshot_git`, `_token_handoff_snapshot`, + `_token_handoff_prefill`, `_token_handoff_steps`; `snapshot` kwarg on + `_cost_handoff_instruction` / `_cost_handoff_followup_prompt`; executor + stores working dir/branch/base and caches the injection snapshot for the + follow-up. +- `simple_coding_agent/lifecycle.py`: `_execute_model` passes + `branch`/`base_revision` with TypeError fallback; call site passes + `prepared.branch` / `prepared.base_revision`. +- `skills/handoff/SKILL.md`: token soft-threshold two-call section; existing + flow kept for operator and turn/time-limit handoffs. +- Tests run (all passing before the boundary): + `tests/test_model_execution.py` 83 passed (4 new snapshot tests); + `tests/test_handoff_end_to_end.py` 26 passed (1 new note+seal test); + `test_operator_handoff`, `test_operator_handoff_lifecycle`, + `test_model_attempt_runner`, `test_emergency_handoff_note`, + `test_handoff_note_started_at`, `test_model_execution_background` + 132 passed. +- `ruff check` on changed files: 1 error, the F541 above. Baseline + `ruff check` is clean, so it is introduced by this attempt. From 057008f84e3546da2a28e50506055bf59119e4eb Mon Sep 17 00:00:00 2001 From: Claude Code Date: Sun, 11 Oct 2026 15:33:10 +0000 Subject: [PATCH 3/3] Fix token follow-up contradicting two-call handoff steps The snapshot-carrying token soft-threshold follow-up kept the legacy 'commit any outstanding work' sentence, directly contradicting step 2 ('Do not commit work yourself'). The snapshot variant now says to leave uncommitted work alone; legacy wording without a snapshot is unchanged. Adds regression assertions and fixes an F541 f-string lint. Co-Authored-By: Claude Code --- simple_coding_agent/model_execution.py | 10 +++++++++- tests/test_model_execution.py | 9 ++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/simple_coding_agent/model_execution.py b/simple_coding_agent/model_execution.py index bc4d83b..ec40f8d 100644 --- a/simple_coding_agent/model_execution.py +++ b/simple_coding_agent/model_execution.py @@ -276,7 +276,15 @@ def _cost_handoff_followup_prompt( ) if snapshot is None: return base - return f"{base}\n\n{snapshot}\n\n{_token_handoff_steps(issue_number)}" + token_base = ( + "This attempt crossed its token soft threshold and must now end by handing off. " + "Invoke the `handoff` skill now and use the current branch state below: " + "write and commit the handoff note, leaving any uncommitted work alone. " + "Do not attempt further implementation " + f"work. {_HANDOFF_SKILL_TOOL_HINT} {_handoff_target(issue_number)}" + f"{_attempt_id_sentence(attempt_id)}" + ) + return f"{token_base}\n\n{snapshot}\n\n{_token_handoff_steps(issue_number)}" def _operator_handoff_instruction( diff --git a/tests/test_model_execution.py b/tests/test_model_execution.py index 5221a24..0e92df4 100644 --- a/tests/test_model_execution.py +++ b/tests/test_model_execution.py @@ -2863,7 +2863,7 @@ def test_token_snapshot_carries_branch_state_and_prefilled_header(tmp_path: Path attempt_id=_ATTEMPT_ID, ) - assert f"agent/issue-149" in snapshot + assert "agent/issue-149" in snapshot assert base in snapshot assert head in snapshot assert "dirty.txt" in snapshot # git status --short @@ -2980,6 +2980,13 @@ def client_factory(options: object) -> FakeClient: # Without a snapshot the prompts keep their legacy wording. assert "Do not run diagnostic commands" not in plain_instruction assert "handoff" in plain_instruction + # The snapshot variant must not contradict the two-call steps: the legacy + # "commit any outstanding work" directive would tell the model to commit + # work the steps forbid it from touching. + assert "commit any outstanding work" not in followup + assert "commit any outstanding work" not in notice + legacy_followup = _cost_handoff_followup_prompt(149, _ATTEMPT_ID) + assert "commit any outstanding work" in legacy_followup def test_token_snapshot_degrades_outside_a_git_repo(tmp_path: Path) -> None: