From f676c9fe2aecccbf6d49688f663f17ec7154e618 Mon Sep 17 00:00:00 2001 From: istos Date: Sun, 2 Aug 2026 12:39:07 +0200 Subject: [PATCH] prompts: say that a headless run is one turn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A work agent ended its turn to wait for a four-minute test suite it had backgrounded, and the run ended with it: 314 lines of passing work staged and never committed, an empty branch, a halted phase. Nothing in the prompts said the run was a single non-interactive turn, so waiting looked like a sensible strategy. All four core prompts now open with the same block: this is one turn, the process exits with your reply, and nothing you meant to finish afterwards survives. It names the trap directly — do not background a command and end the turn to wait for it, do not promise to come back — and each template follows it with what its own run loses: work.md commits around long-running commands, act-pr.md commits and pushes, review-pr.md posts its verdict inside the turn, review.md writes its report there. The block sits before the task body and carries no braces, so str.format still renders it and the NOT READY / ADDRESSED / PR REVIEW / RELEVANCE REVIEW marker lines keep their place after it. Co-Authored-By: Claude Opus 5 --- manager/core/prompts/act-pr.md | 21 +++++ manager/core/prompts/review-pr.md | 17 ++++ manager/core/prompts/review.md | 17 ++++ manager/core/prompts/work.md | 19 +++++ tests/test_prompt_one_turn.py | 132 ++++++++++++++++++++++++++++++ 5 files changed, 206 insertions(+) create mode 100644 tests/test_prompt_one_turn.py diff --git a/manager/core/prompts/act-pr.md b/manager/core/prompts/act-pr.md index 4a8e6dd..8791220 100644 --- a/manager/core/prompts/act-pr.md +++ b/manager/core/prompts/act-pr.md @@ -2,6 +2,27 @@ You are addressing review feedback on a pull request for a task from this repository's task board. You are in the task's git worktree on branch `{branch}`. The PR is {pr}. + +**This run is a single non-interactive turn.** Nobody is watching it and +there is no second turn: when your reply ends, the process exits. Work +you meant to finish afterwards is lost with it, and the board judges the +run by what you actually left behind. + +Two habits end runs early, so neither is allowed here: +- Do not start something in the background and end your turn to wait for + it. There is no monitor, no notification and no resume. If a check + takes minutes, run it in the foreground and wait for it inside this + turn. +- Do not promise to come back to something. There is no coming back — do + it now, or say plainly in your report that it is not done. + +So commit early and push often: work that is not committed dies with the +process, and a commit you never pushed never reaches the PR. Commit +before you start anything long-running — the test suite especially — +then commit and push again after it. A commit is cheap and a lost run is +not, and an early commit can always be improved on later in the same +turn. + The task, for what the work was supposed to be: --- TASK --- diff --git a/manager/core/prompts/review-pr.md b/manager/core/prompts/review-pr.md index 43abdb2..5c0fae2 100644 --- a/manager/core/prompts/review-pr.md +++ b/manager/core/prompts/review-pr.md @@ -1,6 +1,23 @@ You are reviewing a pull request for a task on this repository's task board — you are NOT implementing anything. +**This run is a single non-interactive turn.** Nobody is watching it and +there is no second turn: when your reply ends, the process exits. Work +you meant to finish afterwards is lost with it, and the board judges the +run by what you actually left behind. + +Two habits end runs early, so neither is allowed here: +- Do not start something in the background and end your turn to wait for + it. There is no monitor, no notification and no resume. If a check + takes minutes, run it in the foreground and wait for it inside this + turn. +- Do not promise to come back to something. There is no coming back — do + it now, or say plainly in your report that it is not done. + +So post your verdict to GitHub during the turn, before the reply that +ends it: a verdict you only described in your report never reached the +PR. + The task is `{filename}`, its branch is `{branch}`, and its PR is {pr}. The task content, for what the work was supposed to be: diff --git a/manager/core/prompts/review.md b/manager/core/prompts/review.md index 7a2c1f1..ad995bb 100644 --- a/manager/core/prompts/review.md +++ b/manager/core/prompts/review.md @@ -1,6 +1,23 @@ You are reviewing a task on this repository's task board for continued relevance — you are NOT implementing it. +**This run is a single non-interactive turn.** Nobody is watching it and +there is no second turn: when your reply ends, the process exits. Work +you meant to finish afterwards is lost with it, and the board judges the +run by what you actually left behind. + +Two habits end runs early, so neither is allowed here: +- Do not start something in the background and end your turn to wait for + it. There is no monitor, no notification and no resume. If a check + takes minutes, run it in the foreground and wait for it inside this + turn. +- Do not promise to come back to something. There is no coming back — do + it now, or say plainly in your report that it is not done. + +Your report is the only thing this run leaves behind, so finish your +investigation and write it in the reply that ends the turn. There is no +later message to put it in. + The task is `{stage}/{filename}`. Its content: --- TASK --- diff --git a/manager/core/prompts/work.md b/manager/core/prompts/work.md index 80421ac..22d5a0b 100644 --- a/manager/core/prompts/work.md +++ b/manager/core/prompts/work.md @@ -4,6 +4,25 @@ You are in an isolated git worktree on branch `{branch}` created for this task. All your work happens here: commit to this branch, do not push, do not merge, and do not switch branches. +**This run is a single non-interactive turn.** Nobody is watching it and +there is no second turn: when your reply ends, the process exits. Work +you meant to finish afterwards is lost with it, and the board judges the +run by what you actually left behind. + +Two habits end runs early, so neither is allowed here: +- Do not start something in the background and end your turn to wait for + it. There is no monitor, no notification and no resume. If a check + takes minutes, run it in the foreground and wait for it inside this + turn. +- Do not promise to come back to something. There is no coming back — do + it now, or say plainly in your report that it is not done. + +So commit early and commit often: work that is not committed dies with +the process. Commit before you start anything long-running — the test +suite especially — and commit again after it. A commit is cheap and a +lost run is not, and an early commit can always be improved on later in +the same turn. + Read AGENTS.md at the repo root first and follow it, including its definition of done — run whatever checks it names until they pass. diff --git a/tests/test_prompt_one_turn.py b/tests/test_prompt_one_turn.py new file mode 100644 index 0000000..439749d --- /dev/null +++ b/tests/test_prompt_one_turn.py @@ -0,0 +1,132 @@ +"""A headless run is one non-interactive turn, and the prompts say so. + +Every core prompt template carries the identical "this run is a single +non-interactive turn" block, before the task body, naming the trap that +killed card 47: backgrounding a long command and ending the turn to wait +for it. Each template then says what its own run loses when the turn ends +early — a commit, a push, a posted verdict, the report itself — and none +of it disturbs the marker lines the board parses out of the same output. +""" + +import unittest +from pathlib import Path + +REPO = Path(__file__).resolve().parents[1] +PROMPTS = REPO / "manager" / "core" / "prompts" +TEMPLATES = ("work.md", "review.md", "review-pr.md", "act-pr.md") + +# The shared block runs from the first line to the last; both ends are +# fixed so a template that paraphrases either one fails here. +START = "**This run is a single non-interactive turn.**" +END = "or say plainly in your report that it is not done." + +BODY = "--- TASK ---" + + +def _text(name: str) -> str: + return (PROMPTS / name).read_text(encoding="utf-8") + + +def _block(name: str) -> str: + text = _text(name) + start = text.find(START) + if start < 0: + raise AssertionError(f"{name} never says the run is a single turn") + end = text.find(END, start) + if end < 0: + raise AssertionError(f"{name} lost the end of the one-turn block") + return text[start:end + len(END)] + + +class SharedBlock(unittest.TestCase): + def test_identical_across_all_templates(self): + reference = _block(TEMPLATES[0]) + for name in TEMPLATES[1:]: + self.assertEqual(_block(name), reference, + f"{name} drifted from work.md's one-turn block") + + def test_it_states_the_shape_of_the_run(self): + block = " ".join(_block("work.md").split()) + self.assertIn("no second turn", block) + self.assertIn("the process exits", block) + + def test_it_names_the_trap(self): + block = " ".join(_block("work.md").split()) + self.assertIn("Do not start something in the background and end " + "your turn to wait for it", block) + self.assertIn("no monitor", block) + self.assertIn("run it in the foreground", block) + self.assertIn("Do not promise to come back to something", block) + + def test_it_comes_before_the_task_body(self): + """A brief read after the task is the one the agent skims.""" + for name in TEMPLATES: + text = _text(name) + self.assertLess(text.find(START), text.find(BODY), + f"{name} says it after the task body") + + def test_block_survives_str_format(self): + """Prompts are filled via str.format, so a literal brace here + would break every launch.""" + block = _block("work.md") + self.assertNotIn("{", block) + self.assertNotIn("}", block) + + +class WhatEachRunLoses(unittest.TestCase): + """The block is the reason; each template gives the instruction that + follows from it for the run it drives.""" + + def test_work_commits_around_long_commands(self): + text = " ".join(_text("work.md").split()) + self.assertIn("commit early and commit often", text) + self.assertIn("Commit before you start anything long-running", text) + self.assertIn("commit again after it", text) + + def test_act_pr_commits_and_pushes(self): + text = " ".join(_text("act-pr.md").split()) + self.assertIn("commit early and push often", text) + self.assertIn("Commit before you start anything long-running", text) + self.assertIn("never pushed never reaches the PR", text) + + def test_review_pr_posts_its_verdict_inside_the_turn(self): + text = " ".join(_text("review-pr.md").split()) + self.assertIn("post your verdict to GitHub during the turn", text) + + def test_review_writes_its_report_inside_the_turn(self): + text = " ".join(_text("review.md").split()) + self.assertIn("Your report is the only thing this run leaves behind", + text) + + +class MarkersUndisturbed(unittest.TestCase): + """agents.py parses the marker lines out of the same output; the new + prose must not compete for 'the first line'.""" + + MARKERS = { + "work.md": "NOT READY: ", + "act-pr.md": "ADDRESSED: ", + "review-pr.md": "PR REVIEW: ", + "review.md": "RELEVANCE REVIEW: ", + } + + def test_markers_still_follow_the_new_block(self): + for name, marker in self.MARKERS.items(): + text = _text(name) + self.assertIn(marker, text, f"{name} lost its marker line") + self.assertLess(text.find(START), text.find(marker), + f"{name} now states its marker before the block") + + def test_not_ready_keeps_its_place_in_work(self): + """The NOT READY instruction is still the only thing in work.md + claiming a reply's first line, and it still sits after the task.""" + text = _text("work.md") + self.assertEqual(text.count("FIRST line"), 1) + self.assertLess(text.find("--- END TASK ---"), text.find("FIRST line")) + self.assertNotIn("FIRST line", _block("work.md")) + self.assertNotIn("NOT READY", _block("work.md")) + + +if __name__ == "__main__": + unittest.main()