Skip to content

Commit fde8302

Browse files
authored
feat: add solve-bug entrypoints (#13)
1 parent a5f235f commit fde8302

6 files changed

Lines changed: 330 additions & 0 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ Artifacts:
4343
- durable reports: `docs/test-reports/agentic-bug-sweep/`
4444
- ephemeral execution state: `target/agentic-bug-sweep/`
4545

46+
## Solve-Bug Entrypoints
47+
48+
Use `bash ai/run-codex-solve-bug.sh` or `bash ai/run-claude-solve-bug.sh` when you want a headless agent to pick one actionable bug or bug-like issue, fix it, and drive the repository-local PR workflow.
49+
50+
If there are effectively no open bug or bug-like issues, the workflow should terminate cleanly with no code changes and no PR creation.
51+
4652
## Testing
4753

4854
Run unit and integration tests with nextest, and keep doctests as a separate step:

ai/manifest.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,21 @@
7878
"source": "scripts/sync-agent-assets.sh",
7979
"target": "scripts/sync-agent-assets.sh"
8080
},
81+
{
82+
"id": "run-codex-solve-bug-script",
83+
"source": "ai/run-codex-solve-bug.sh",
84+
"target": "ai/run-codex-solve-bug.sh"
85+
},
86+
{
87+
"id": "run-claude-solve-bug-script",
88+
"source": "ai/run-claude-solve-bug.sh",
89+
"target": "ai/run-claude-solve-bug.sh"
90+
},
91+
{
92+
"id": "solve-bug-issue-prompt",
93+
"source": "ai/solve_bug_issue.md",
94+
"target": "ai/solve_bug_issue.md"
95+
},
8196
{
8297
"id": "check-repo-settings-script",
8398
"source": "scripts/check-repo-settings.sh",

ai/run-claude-solve-bug.sh

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
usage() {
5+
cat <<'EOF'
6+
Usage: run-claude-solve-bug.sh [options] [-- <extra claude args>]
7+
8+
Run Claude Code headlessly from the repository root using a prompt file under ai/.
9+
10+
Options:
11+
--prompt PATH Prompt file. Relative paths are resolved from this script's directory.
12+
Default: solve_bug_issue.md
13+
--model MODEL Pass --model MODEL to claude.
14+
--run-dir PATH Directory for logs.
15+
Default: a fresh temporary directory.
16+
--text Disable stream-json output and print plain text instead.
17+
-h, --help Show this help.
18+
EOF
19+
}
20+
21+
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
22+
repo_root="$(git -C "$script_dir" rev-parse --show-toplevel)"
23+
24+
prompt_arg="solve_bug_issue.md"
25+
model=""
26+
run_dir=""
27+
json_mode=1
28+
extra_args=()
29+
30+
while (($# > 0)); do
31+
case "$1" in
32+
--prompt)
33+
prompt_arg="${2:?missing value for --prompt}"
34+
shift 2
35+
;;
36+
--model)
37+
model="${2:?missing value for --model}"
38+
shift 2
39+
;;
40+
--run-dir)
41+
run_dir="${2:?missing value for --run-dir}"
42+
shift 2
43+
;;
44+
--text)
45+
json_mode=0
46+
shift
47+
;;
48+
-h|--help)
49+
usage
50+
exit 0
51+
;;
52+
--)
53+
shift
54+
extra_args=("$@")
55+
break
56+
;;
57+
*)
58+
echo "unknown argument: $1" >&2
59+
usage >&2
60+
exit 2
61+
;;
62+
esac
63+
done
64+
65+
if [[ "$prompt_arg" = /* ]]; then
66+
prompt_path="$prompt_arg"
67+
else
68+
prompt_path="$script_dir/$prompt_arg"
69+
fi
70+
71+
if [[ ! -f "$prompt_path" ]]; then
72+
echo "prompt file not found: $prompt_path" >&2
73+
exit 1
74+
fi
75+
76+
if [[ -z "$run_dir" ]]; then
77+
run_dir="$(mktemp -d "${TMPDIR:-/tmp}/claude-solve-bug.XXXXXX")"
78+
else
79+
mkdir -p "$run_dir"
80+
run_dir="$(cd -- "$run_dir" && pwd -P)"
81+
fi
82+
83+
log_path="$run_dir/output.log"
84+
if ((json_mode)); then
85+
log_path="$run_dir/events.jsonl"
86+
fi
87+
88+
prompt_text="$(cat "$prompt_path")"
89+
90+
cmd=(
91+
claude
92+
--print
93+
--dangerously-skip-permissions
94+
)
95+
96+
if ((json_mode)); then
97+
cmd+=(--output-format stream-json)
98+
else
99+
cmd+=(--output-format text)
100+
fi
101+
102+
if [[ -n "$model" ]]; then
103+
cmd+=(--model "$model")
104+
fi
105+
106+
cmd+=("${extra_args[@]}" "$prompt_text")
107+
108+
echo "repo_root=$repo_root"
109+
echo "prompt_path=$prompt_path"
110+
echo "run_dir=$run_dir"
111+
echo "log_path=$log_path"
112+
113+
(
114+
cd "$repo_root"
115+
"${cmd[@]}"
116+
) | tee "$log_path"

ai/run-codex-solve-bug.sh

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
usage() {
5+
cat <<'EOF'
6+
Usage: run-codex-solve-bug.sh [options] [-- <extra codex args>]
7+
8+
Run Codex headlessly from the repository root using a prompt file under ai/.
9+
10+
Options:
11+
--prompt PATH Prompt file. Relative paths are resolved from this script's directory.
12+
Default: solve_bug_issue.md
13+
--model MODEL Pass --model MODEL to codex exec.
14+
--run-dir PATH Directory for logs and final message output.
15+
Default: a fresh temporary directory.
16+
--text Disable JSONL output and stream plain Codex terminal output instead.
17+
-h, --help Show this help.
18+
EOF
19+
}
20+
21+
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
22+
repo_root="$(git -C "$script_dir" rev-parse --show-toplevel)"
23+
24+
prompt_arg="solve_bug_issue.md"
25+
model=""
26+
run_dir=""
27+
json_mode=1
28+
extra_args=()
29+
30+
while (($# > 0)); do
31+
case "$1" in
32+
--prompt)
33+
prompt_arg="${2:?missing value for --prompt}"
34+
shift 2
35+
;;
36+
--model)
37+
model="${2:?missing value for --model}"
38+
shift 2
39+
;;
40+
--run-dir)
41+
run_dir="${2:?missing value for --run-dir}"
42+
shift 2
43+
;;
44+
--text)
45+
json_mode=0
46+
shift
47+
;;
48+
-h|--help)
49+
usage
50+
exit 0
51+
;;
52+
--)
53+
shift
54+
extra_args=("$@")
55+
break
56+
;;
57+
*)
58+
echo "unknown argument: $1" >&2
59+
usage >&2
60+
exit 2
61+
;;
62+
esac
63+
done
64+
65+
if [[ "$prompt_arg" = /* ]]; then
66+
prompt_path="$prompt_arg"
67+
else
68+
prompt_path="$script_dir/$prompt_arg"
69+
fi
70+
71+
if [[ ! -f "$prompt_path" ]]; then
72+
echo "prompt file not found: $prompt_path" >&2
73+
exit 1
74+
fi
75+
76+
if [[ -z "$run_dir" ]]; then
77+
run_dir="$(mktemp -d "${TMPDIR:-/tmp}/codex-solve-bug.XXXXXX")"
78+
else
79+
mkdir -p "$run_dir"
80+
run_dir="$(cd -- "$run_dir" && pwd -P)"
81+
fi
82+
83+
log_path="$run_dir/output.log"
84+
last_message_path="$run_dir/final.txt"
85+
86+
cmd=(
87+
codex exec
88+
--cd "$repo_root"
89+
--dangerously-bypass-approvals-and-sandbox
90+
--output-last-message "$last_message_path"
91+
)
92+
93+
if ((json_mode)); then
94+
cmd+=(--json)
95+
log_path="$run_dir/events.jsonl"
96+
fi
97+
98+
if [[ -n "$model" ]]; then
99+
cmd+=(--model "$model")
100+
fi
101+
102+
cmd+=("${extra_args[@]}" -)
103+
104+
echo "repo_root=$repo_root"
105+
echo "prompt_path=$prompt_path"
106+
echo "run_dir=$run_dir"
107+
echo "log_path=$log_path"
108+
echo "last_message_path=$last_message_path"
109+
110+
"${cmd[@]}" < "$prompt_path" | tee "$log_path"

ai/solve_bug_issue.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
You are running one iteration of an automated bug-fix workflow for this repository.
2+
3+
Your job is to:
4+
5+
1. Inspect open bug and bug-like issues.
6+
2. Choose exactly one actionable workstream.
7+
3. If open bug / bug-like issues are effectively zero, terminate cleanly with no code changes and no PR creation. In other words, if effectively no open bug or bug-like issues remain, end the run without mutation.
8+
4. Otherwise, fix the issue or close it only when it is clearly irrelevant, duplicate, or already fixed.
9+
5. Use the repository-local PR helpers:
10+
- `bash scripts/create-pr.sh`
11+
- `bash scripts/monitor-pr-checks.sh`
12+
6. Continue until merge or a defined stop condition.
13+
7. Restore the local checkout as part of normal cleanup.
14+
15+
Hard rules:
16+
17+
- Work from the latest `origin/main`.
18+
- Use a dedicated worktree only after selecting a real candidate.
19+
- Do not touch unrelated work.
20+
- Stop after two core fix attempts.
21+
- Do not ask the user questions.
22+
- Do not create a PR if there is nothing actionable to fix.
23+
24+
When you terminate cleanly without action, summarize that no worthwhile bug or bug-like issue was available and that no code was changed.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import os
2+
import subprocess
3+
import unittest
4+
from pathlib import Path
5+
6+
7+
REPO_ROOT = Path(__file__).resolve().parents[1]
8+
RUN_CODEX = REPO_ROOT / "ai" / "run-codex-solve-bug.sh"
9+
RUN_CLAUDE = REPO_ROOT / "ai" / "run-claude-solve-bug.sh"
10+
PROMPT = REPO_ROOT / "ai" / "solve_bug_issue.md"
11+
MANIFEST = REPO_ROOT / "ai" / "manifest.json"
12+
13+
14+
class TemplateSolveBugEntrypointsTests(unittest.TestCase):
15+
def test_prompt_contract(self) -> None:
16+
self.assertTrue(RUN_CODEX.is_file(), msg=f"missing file: {RUN_CODEX}")
17+
self.assertTrue(RUN_CLAUDE.is_file(), msg=f"missing file: {RUN_CLAUDE}")
18+
self.assertTrue(PROMPT.is_file(), msg=f"missing file: {PROMPT}")
19+
self.assertTrue(MANIFEST.is_file(), msg=f"missing file: {MANIFEST}")
20+
21+
prompt = PROMPT.read_text(encoding="utf-8")
22+
manifest = MANIFEST.read_text(encoding="utf-8")
23+
24+
self.assertIn("bash scripts/create-pr.sh", prompt)
25+
self.assertIn("bash scripts/monitor-pr-checks.sh", prompt)
26+
self.assertIn("effectively no open bug or bug-like issues", prompt)
27+
self.assertIn("run-codex-solve-bug.sh", manifest)
28+
self.assertIn("run-claude-solve-bug.sh", manifest)
29+
self.assertIn("solve_bug_issue.md", manifest)
30+
31+
def test_run_codex_help(self) -> None:
32+
result = self._run_wrapper_help(RUN_CODEX)
33+
self.assertEqual(result.returncode, 0, msg=result.stderr)
34+
self.assertIn("--prompt", result.stdout)
35+
self.assertIn("--run-dir", result.stdout)
36+
self.assertIn("--text", result.stdout)
37+
38+
def test_run_claude_help(self) -> None:
39+
result = self._run_wrapper_help(RUN_CLAUDE)
40+
self.assertEqual(result.returncode, 0, msg=result.stderr)
41+
self.assertIn("--prompt", result.stdout)
42+
self.assertIn("--run-dir", result.stdout)
43+
self.assertIn("--text", result.stdout)
44+
45+
def _run_wrapper_help(self, source_script: Path) -> subprocess.CompletedProcess[str]:
46+
self.assertTrue(source_script.is_file(), msg=f"missing file: {source_script}")
47+
env = os.environ.copy()
48+
return subprocess.run(
49+
["bash", str(source_script), "--help"],
50+
cwd=REPO_ROOT,
51+
text=True,
52+
capture_output=True,
53+
env=env,
54+
check=False,
55+
)
56+
57+
58+
if __name__ == "__main__":
59+
unittest.main()

0 commit comments

Comments
 (0)