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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ All runs in tmux. Close your laptop — it keeps going.
| `autopilot start --issue https://github.com/org/repo/issues/45` | Start from a cross-repo issue URL |
| `autopilot start --file task.txt` | Start from a prompt file |
| `autopilot resume --pr 42345` | Resume from an existing PR |
| `autopilot resume --pr 42345 --context "fix linting"` | Resume with additional instructions |
| `autopilot fix-ci --pr 42345` | [Fix CI failures](docs/fix-ci-workflow.md) |
| `autopilot stop <id>` | Stop a running task |
| `autopilot restart <id>` | Restart a stopped task |
Expand Down
12 changes: 11 additions & 1 deletion src/autopilot_loop/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
- ``--prompt / -p`` — inline text on the command line.
- ``--issue / -i`` — GitHub issue number or full URL (cross-repo supported).
- ``--file / -f`` — path to a plain-text file whose contents become the prompt.

The ``resume`` command accepts an optional ``--context / -c`` flag to pass
additional instructions to the agent (e.g. ``--context 'fix linting issues'``).
"""

import argparse
Expand Down Expand Up @@ -330,9 +333,14 @@ def cmd_resume(args):

task_id = _generate_task_id()

prompt = "(resumed from PR #%d)" % args.pr
if getattr(args, "context", None):
prompt = "%s\n\n## Additional Instructions\n%s" % (prompt, args.context)
logger.info("Resume context: %d chars", len(args.context))

create_task(
task_id=task_id,
prompt="(resumed from PR #%d)" % args.pr,
prompt=prompt,
max_iterations=config["max_iterations"],
model=config["model"],
)
Expand Down Expand Up @@ -755,6 +763,8 @@ def main():
# resume
p_resume = subparsers.add_parser("resume", help="Resume from an existing PR")
p_resume.add_argument("--pr", type=int, required=True, help="PR number to resume")
p_resume.add_argument("--context", "-c", type=str, default="",
help="Additional instructions for the agent")

# status
p_status = subparsers.add_parser("status", help="Show task status")
Expand Down
1 change: 1 addition & 0 deletions src/autopilot_loop/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,7 @@ def _do_fix(self):
previous_context=previous_context,
bouncing_comments=bouncing_context,
prompt_file=self.task.get("prompt_file"),
task_context=self.task.get("prompt", ""),
)

# Record head SHA before fix
Expand Down
77 changes: 52 additions & 25 deletions src/autopilot_loop/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@ def _file_protection_instruction(prompt_file):
)


def _pre_commit_verification_instruction():
"""Return a pre-commit quality gate instruction block."""
return (
"BEFORE committing, run the project's quality checks to catch errors early:\n"
" a. Look for linter/formatter configuration in the repo (e.g. pyproject.toml\n"
" with [tool.ruff], .eslintrc, Makefile lint targets, etc.) and run the\n"
" appropriate lint command. Fix every reported error before proceeding.\n"
" b. If the project has a type checker configured (mypy, pyright, tsc, etc.),\n"
" run it and fix any type errors.\n"
" c. Run the project's test suite and ensure all tests pass.\n"
" d. Only after all checks pass, proceed to commit.\n"
)


def implement_prompt(task_description, branch_name, custom_instructions="", prompt_file=None):
"""Prompt for the IMPLEMENT phase.

Expand All @@ -50,18 +64,19 @@ def implement_prompt(task_description, branch_name, custom_instructions="", prom
parts.append(
"1. Implement the task described above.\n"
"2. Run any relevant tests to verify your changes work.\n"
"3. Create a new git branch named `%s`:\n"
"3. %s"
"4. Create a new git branch named `%s`:\n"
" git checkout -b %s\n"
"4. Stage and commit your changes with a proper, descriptive commit message\n"
"5. Stage and commit your changes with a proper, descriptive commit message\n"
" that explains what was changed and why. Do NOT use generic messages.\n"
"5. Create a draft pull request using `gh pr create --draft`. Use the repo's\n"
"6. Create a draft pull request using `gh pr create --draft`. Use the repo's\n"
" PR template (check `.github/PULL_REQUEST_TEMPLATE.md` or similar) to\n"
" structure the PR body. Write a clear title and fill in the template sections.\n"
"6. Push the branch: `git push -u origin %s`\n"
"7. After pushing, review your own changes by running `git diff main` and\n"
"7. Push the branch: `git push -u origin %s`\n"
"8. After pushing, review your own changes by running `git diff main` and\n"
" examining the output. If you find any issues (bugs, missing tests, style\n"
" problems), fix them, commit with a descriptive message, and push again.\n"
% (branch_name, branch_name, branch_name)
% (_pre_commit_verification_instruction(), branch_name, branch_name, branch_name)
)

return "\n".join(parts)
Expand Down Expand Up @@ -93,17 +108,18 @@ def implement_on_existing_branch_prompt(task_description, branch_name, custom_in
"Work directly on this branch.\n\n"
"1. Implement the task described above.\n"
"2. Run any relevant tests to verify your changes work.\n"
"3. Stage and commit your changes with a proper, descriptive commit message\n"
"3. %s"
"4. Stage and commit your changes with a proper, descriptive commit message\n"
" that explains what was changed and why. Do NOT use generic messages.\n"
"4. If a PR already exists for this branch, push your changes. If no PR exists,\n"
"5. If a PR already exists for this branch, push your changes. If no PR exists,\n"
" create a draft pull request using `gh pr create --draft`. Use the repo's\n"
" PR template (check `.github/PULL_REQUEST_TEMPLATE.md` or similar) to\n"
" structure the PR body. Write a clear title and fill in the template sections.\n"
"5. Push the branch: `git push origin %s`\n"
"6. After pushing, review your own changes by running `git diff HEAD~1` and\n"
"6. Push the branch: `git push origin %s`\n"
"7. After pushing, review your own changes by running `git diff HEAD~1` and\n"
" examining the output. If you find any issues (bugs, missing tests, style\n"
" problems), fix them, commit with a descriptive message, and push again.\n"
% (branch_name, branch_name)
% (branch_name, _pre_commit_verification_instruction(), branch_name)
)

return "\n".join(parts)
Expand Down Expand Up @@ -136,25 +152,26 @@ def plan_and_implement_prompt(task_description, branch_name, custom_instructions
" - Any edge cases or risks\n"
"2. Then implement the plan.\n"
"3. Run any relevant tests to verify your changes work.\n"
"4. Create a new git branch named `%s`:\n"
"4. %s"
"5. Create a new git branch named `%s`:\n"
" git checkout -b %s\n"
"5. Stage and commit your changes with a proper, descriptive commit message\n"
"6. Stage and commit your changes with a proper, descriptive commit message\n"
" that explains what was changed and why. Do NOT use generic messages.\n"
"6. Create a draft pull request using `gh pr create --draft`. Use the repo's\n"
"7. Create a draft pull request using `gh pr create --draft`. Use the repo's\n"
" PR template (check `.github/PULL_REQUEST_TEMPLATE.md` or similar) to\n"
" structure the PR body. Write a clear title and fill in the template sections.\n"
"7. Push the branch: `git push -u origin %s`\n"
"8. After pushing, review your own changes by running `git diff main` and\n"
"8. Push the branch: `git push -u origin %s`\n"
"9. After pushing, review your own changes by running `git diff main` and\n"
" examining the output. If you find any issues (bugs, missing tests, style\n"
" problems), fix them, commit with a descriptive message, and push again.\n"
% (branch_name, branch_name, branch_name)
% (_pre_commit_verification_instruction(), branch_name, branch_name, branch_name)
)

return "\n".join(parts)


def fix_prompt(review_comments_text, custom_instructions="", previous_context="",
bouncing_comments="", prompt_file=None):
bouncing_comments="", prompt_file=None, task_context=""):
"""Prompt for the FIX phase.

Agent addresses PR review comments using a 3-tier verification model,
Expand All @@ -170,6 +187,12 @@ def fix_prompt(review_comments_text, custom_instructions="", previous_context=""
parts.append(custom_instructions.strip())
parts.append("")

if task_context:
parts.append("## Task Context")
parts.append("")
parts.append(task_context.strip())
parts.append("")

if previous_context:
parts.append("## Previous Iteration Context")
parts.append("")
Expand Down Expand Up @@ -222,16 +245,17 @@ def fix_prompt(review_comments_text, custom_instructions="", previous_context=""
" using the 3-tier model.\n"
"2. Make code changes ONLY for Tier 1 (fixed) comments.\n"
"3. Run any relevant tests to verify your fixes work.\n"
"4. Commit with a descriptive message that explains what review feedback\n"
"4. %s"
"5. Commit with a descriptive message that explains what review feedback\n"
" was addressed. Do NOT use generic messages like 'fix review comments'.\n"
" Instead, describe the specific changes, e.g.:\n"
" 'Fix dirty-tracking semantics in user_queries, add missing\n"
" type annotation to billing_service'\n"
"5. Push the changes.\n"
"6. After pushing, review your own fix by running `git diff HEAD~1` and\n"
"6. Push the changes.\n"
"7. After pushing, review your own fix by running `git diff HEAD~1` and\n"
" examining the output. If you introduced any new issues while fixing\n"
" the review comments, fix them, commit, and push again.\n"
"7. IMPORTANT: Write a JSON file at `.autopilot-fix-summary.json` in the\n"
"8. IMPORTANT: Write a JSON file at `.autopilot-fix-summary.json` in the\n"
" repo root with your resolution for EACH comment. Use this exact format:\n"
" ```json\n"
' [\n'
Expand All @@ -247,6 +271,7 @@ def fix_prompt(review_comments_text, custom_instructions="", previous_context=""
" Status must be `fixed`, `skipped`, `dismissed`, or `uncertain`.\n"
" The `evidence` field is REQUIRED for `dismissed` and `uncertain` statuses.\n"
" Do NOT commit this file \u2014 just write it to disk.\n"
% _pre_commit_verification_instruction()
)

return "\n".join(parts)
Expand Down Expand Up @@ -314,13 +339,15 @@ def fix_ci_prompt(ci_annotations_text, custom_instructions="", prompt_file=None)
" (e.g., a missing test case for a new route), fix it once.\n"
"3. Run the full test suite to verify your fixes don't break anything \u2014\n"
" not just the tests that failed. Adjacent test files often break too.\n"
"4. Commit with a descriptive message that explains what CI failures\n"
"4. %s"
"5. Commit with a descriptive message that explains what CI failures\n"
" were addressed. Do NOT use generic messages like 'fix CI'.\n"
" Instead, describe the specific changes.\n"
"5. Push the changes.\n"
"6. After pushing, review your own fix by running `git diff HEAD~1` and\n"
"6. Push the changes.\n"
"7. After pushing, review your own fix by running `git diff HEAD~1` and\n"
" examining the output. If you introduced any new issues, fix them,\n"
" commit, and push again.\n"
% _pre_commit_verification_instruction()
)

return "\n".join(parts)
Expand Down
78 changes: 77 additions & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ def fake_run(cmd, **kw):

monkeypatch.setattr(subprocess, "run", fake_run)

args = SimpleNamespace(pr=42)
args = SimpleNamespace(pr=42, context="")
# Should not raise
cmd_resume(args)

Expand Down Expand Up @@ -428,6 +428,82 @@ def fake_run(cmd, **kw):
assert "other-owner/other-repo" in captured.err


class TestCmdResumeContext:
"""Test that cmd_resume stores --context in the task prompt."""

def _gh_pr_view_output(self, branch, state, nwo):
return "%s\t%s\t%s\n" % (branch, state, nwo)

def test_context_stored_in_task_prompt(self, monkeypatch):
"""cmd_resume with --context includes it in the task prompt."""
monkeypatch.setattr(
"autopilot_loop.cli.load_config",
lambda **kw: {"model": "m", "max_iterations": 3},
)
monkeypatch.setattr(
"autopilot_loop.github_api.get_repo_nwo",
lambda: "owner/my-repo",
)
monkeypatch.setattr("autopilot_loop.cli._check_branch_lock", lambda b: None)
monkeypatch.setattr("autopilot_loop.cli._launch_in_tmux", lambda *a, **kw: None)

def fake_run(cmd, **kw):
if "pr" in cmd and "view" in cmd:
return subprocess.CompletedProcess(
args=cmd, returncode=0,
stdout=self._gh_pr_view_output(
"fix/branch", "OPEN", "owner/my-repo",
),
stderr="",
)
return subprocess.CompletedProcess(args=cmd, returncode=0, stdout="", stderr="")

monkeypatch.setattr(subprocess, "run", fake_run)

args = SimpleNamespace(pr=42, context="fix all linting issues")
cmd_resume(args)

# Verify the task was created with context in the prompt
tasks = list(persistence.list_tasks())
assert len(tasks) == 1
assert "fix all linting issues" in tasks[0]["prompt"]
assert "Additional Instructions" in tasks[0]["prompt"]

def test_no_context_default_prompt(self, monkeypatch):
"""cmd_resume without --context uses default prompt."""
monkeypatch.setattr(
"autopilot_loop.cli.load_config",
lambda **kw: {"model": "m", "max_iterations": 3},
)
monkeypatch.setattr(
"autopilot_loop.github_api.get_repo_nwo",
lambda: "owner/my-repo",
)
monkeypatch.setattr("autopilot_loop.cli._check_branch_lock", lambda b: None)
monkeypatch.setattr("autopilot_loop.cli._launch_in_tmux", lambda *a, **kw: None)

def fake_run(cmd, **kw):
if "pr" in cmd and "view" in cmd:
return subprocess.CompletedProcess(
args=cmd, returncode=0,
stdout=self._gh_pr_view_output(
"fix/branch", "OPEN", "owner/my-repo",
),
stderr="",
)
return subprocess.CompletedProcess(args=cmd, returncode=0, stdout="", stderr="")

monkeypatch.setattr(subprocess, "run", fake_run)

args = SimpleNamespace(pr=42, context="")
cmd_resume(args)

tasks = list(persistence.list_tasks())
assert len(tasks) == 1
assert tasks[0]["prompt"] == "(resumed from PR #42)"
assert "Additional Instructions" not in tasks[0]["prompt"]


class TestParseIssueArg:
"""Tests for _parse_issue_arg: plain numbers and full URLs."""

Expand Down
53 changes: 53 additions & 0 deletions tests/test_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -1316,3 +1316,56 @@ def test_handles_empty_body(self):
diff_stat="",
)
assert "(empty)" in result


class TestPreCommitVerification:
"""Tests for pre-commit quality gate instructions in prompt builders."""

def test_implement_prompt_has_pre_commit_block(self):
from autopilot_loop.prompts import implement_prompt
prompt = implement_prompt("Do X", "autopilot/abc")
assert "BEFORE committing" in prompt
assert "linter" in prompt

def test_existing_branch_prompt_has_pre_commit_block(self):
from autopilot_loop.prompts import implement_on_existing_branch_prompt
prompt = implement_on_existing_branch_prompt("Do X", "autopilot/abc")
assert "BEFORE committing" in prompt

def test_plan_and_implement_prompt_has_pre_commit_block(self):
from autopilot_loop.prompts import plan_and_implement_prompt
prompt = plan_and_implement_prompt("Do X", "autopilot/abc")
assert "BEFORE committing" in prompt

def test_fix_prompt_has_pre_commit_block(self):
from autopilot_loop.prompts import fix_prompt
prompt = fix_prompt("some comments")
assert "BEFORE committing" in prompt

def test_fix_ci_prompt_has_pre_commit_block(self):
from autopilot_loop.prompts import fix_ci_prompt
prompt = fix_ci_prompt("ci failures")
assert "BEFORE committing" in prompt


class TestFixPromptTaskContext:
"""Tests for task_context parameter in fix_prompt."""

def test_task_context_included(self):
from autopilot_loop.prompts import fix_prompt
prompt = fix_prompt(
review_comments_text="some comments",
task_context="Fix all linting issues before committing",
)
assert "## Task Context" in prompt
assert "Fix all linting issues" in prompt

def test_task_context_omitted_when_empty(self):
from autopilot_loop.prompts import fix_prompt
prompt = fix_prompt(review_comments_text="some comments")
assert "## Task Context" not in prompt

def test_task_context_omitted_when_blank(self):
from autopilot_loop.prompts import fix_prompt
prompt = fix_prompt(review_comments_text="comments", task_context="")
assert "## Task Context" not in prompt
Loading