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.
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..ec40f8d 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,193 @@ 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
+ 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(
@@ -341,6 +508,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 +940,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 +958,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 +1032,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 +1118,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 +2179,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..0e92df4 100644
--- a/tests/test_model_execution.py
+++ b/tests/test_model_execution.py
@@ -2813,3 +2813,189 @@ 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 "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
+ # 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:
+ 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