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()