Skip to content

Commit 3fffcc2

Browse files
committed
feat(project): add pr iteration task type
1 parent 538602a commit 3fffcc2

45 files changed

Lines changed: 1633 additions & 115 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ To get started and understand the developer flow, follow the [Developer guide](.
2424
- **`scripts/`** (root) — Optional cross-package helpers; **`scripts/ci-build.sh`** runs the full monorepo build (same as CI).
2525
- **`cdk/`** — CDK app package (`@abca/cdk`): `cdk/src/`, `cdk/test/`, `cdk/cdk.json`, `cdk/tsconfig.json`, `cdk/tsconfig.dev.json`, and `cdk/.eslintrc.json`.
2626
- **`cli/`**`@backgroundagent/cli` — CLI tool for interacting with the deployed REST API (see below).
27-
- **`agent/`** — Python code that runs inside the agent compute environment (entrypoint, server, system prompt, Dockerfile, requirements).
27+
- **`agent/`** — Python code that runs inside the agent compute environment (entrypoint, server, system prompt, Dockerfile, requirements). The system prompt is refactored into `agent/prompts/` with a shared base template and per-task-type workflow variants (`new_task`, `pr_iteration`).
2828
- **`docs/`** — Authoritative Markdown in `guides/` (developer, user, roadmap, prompt) and `design/`; assets in `diagrams/`, `imgs/`. The Starlight docs site lives here (`astro.config.mjs`, `package.json`); `src/content/docs/` is refreshed via `docs/scripts/sync-starlight.mjs`.
2929
- **`CONTRIBUTING.md`** — Contribution guidelines at the repository root.
3030
- **`package.json`** (root), **`yarn.lock`** — Yarn workspace root (minimal manifest); dependencies live in **`cdk/`**, **`cli/`**, and **`docs/`** package manifests.
@@ -40,7 +40,7 @@ The `@backgroundagent/cli` package provides the `bgagent` executable for submitt
4040
- `src/api-client.ts` — HTTP client wrapping `fetch` with auth header injection
4141
- `src/auth.ts` — Cognito login, token caching (`~/.bgagent/credentials.json`), auto-refresh
4242
- `src/config.ts` — Read/write `~/.bgagent/config.json`
43-
- `src/types.ts` — API request/response types (mirrored from `cdk/src/handlers/shared/types.ts`)
43+
- `src/types.ts` — API request/response types (mirrored from `cdk/src/handlers/shared/types.ts`), including `TaskType` (`new_task` | `pr_iteration`)
4444
- `src/format.ts` — Output formatting (table, detail view, JSON)
4545
- `src/debug.ts` — Verbose/debug logging (`--verbose` flag)
4646
- `src/errors.ts``CliError` and `ApiError` classes

agent/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ RUN uv sync --frozen --no-dev --directory /app
5151
# Copy agent code (ARG busts cache so file edits are always picked up)
5252
ARG CACHE_BUST=0
5353
COPY entrypoint.py system_prompt.py server.py task_state.py observability.py memory.py /app/
54+
COPY prompts/ /app/prompts/
5455
COPY prepare-commit-msg.sh /app/
5556
COPY test_sdk_smoke.py test_subprocess_threading.py /app/
5657

agent/entrypoint.py

Lines changed: 99 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import memory as agent_memory
3232
import task_state
3333
from observability import task_span
34+
from prompts import get_system_prompt
3435
from system_prompt import SYSTEM_PROMPT
3536

3637
# ---------------------------------------------------------------------------
@@ -77,6 +78,9 @@ def build_config(
7778
dry_run: bool = False,
7879
task_id: str = "",
7980
system_prompt_overrides: str = "",
81+
task_type: str = "new_task",
82+
branch_name: str = "",
83+
pr_number: str = "",
8084
) -> dict:
8185
"""Build and validate configuration from explicit parameters.
8286
@@ -94,6 +98,9 @@ def build_config(
9498
"max_turns": max_turns,
9599
"max_budget_usd": max_budget_usd,
96100
"system_prompt_overrides": system_prompt_overrides,
101+
"task_type": task_type,
102+
"branch_name": branch_name,
103+
"pr_number": pr_number,
97104
}
98105

99106
errors = []
@@ -103,7 +110,10 @@ def build_config(
103110
errors.append("github_token is required")
104111
if not config["aws_region"]:
105112
errors.append("aws_region is required for Bedrock")
106-
if not config["issue_number"] and not config["task_description"]:
113+
if config["task_type"] == "pr_iteration":
114+
if not config["pr_number"]:
115+
errors.append("pr_number is required for pr_iteration task type")
116+
elif not config["issue_number"] and not config["task_description"]:
107117
errors.append("Either issue_number or task_description is required")
108118

109119
if errors:
@@ -303,15 +313,19 @@ def setup_repo(config: dict) -> dict:
303313
repo_dir = f"{AGENT_WORKSPACE}/{config['task_id']}"
304314
setup: dict[str, str | list[str] | bool] = {"repo_dir": repo_dir, "notes": []}
305315

306-
# Derive branch slug from issue title or task description
307-
title = ""
308-
if config.get("issue"):
309-
title = config["issue"]["title"]
310-
if not title:
311-
title = config["task_description"]
312-
slug = slugify(title)
313-
branch = f"bgagent/{config['task_id']}/{slug}"
314-
setup["branch"] = branch
316+
if config.get("task_type") == "pr_iteration" and config.get("branch_name"):
317+
branch = config["branch_name"]
318+
setup["branch"] = branch
319+
else:
320+
# Derive branch slug from issue title or task description
321+
title = ""
322+
if config.get("issue"):
323+
title = config["issue"]["title"]
324+
if not title:
325+
title = config["task_description"]
326+
slug = slugify(title)
327+
branch = f"bgagent/{config['task_id']}/{slug}"
328+
setup["branch"] = branch
315329

316330
# Mark the repo directory as safe for git. On persistent session storage
317331
# the mount may be owned by a different UID than the container user,
@@ -343,9 +357,22 @@ def setup_repo(config: dict) -> dict:
343357
cwd=repo_dir,
344358
)
345359

346-
# Create branch
347-
log("SETUP", f"Creating branch: {branch}")
348-
run_cmd(["git", "checkout", "-b", branch], label="create-branch", cwd=repo_dir)
360+
# Branch setup
361+
if config.get("task_type") == "pr_iteration" and config.get("branch_name"):
362+
log("SETUP", f"Checking out existing PR branch: {branch}")
363+
run_cmd(
364+
["git", "fetch", "origin", branch],
365+
label="fetch-pr-branch",
366+
cwd=repo_dir,
367+
)
368+
run_cmd(
369+
["git", "checkout", "-b", branch, f"origin/{branch}"],
370+
label="checkout-pr-branch",
371+
cwd=repo_dir,
372+
)
373+
else:
374+
log("SETUP", f"Creating branch: {branch}")
375+
run_cmd(["git", "checkout", "-b", branch], label="create-branch", cwd=repo_dir)
349376

350377
# Trust mise config files in the cloned repo (required before mise install)
351378
run_cmd(
@@ -402,7 +429,11 @@ def setup_repo(config: dict) -> dict:
402429
setup["lint_before"] = True
403430

404431
# Detect default branch
405-
setup["default_branch"] = detect_default_branch(config["repo_url"], repo_dir)
432+
# For PR iteration: use base_branch from orchestrator if available
433+
if config.get("task_type") == "pr_iteration" and config.get("base_branch"):
434+
setup["default_branch"] = config["base_branch"]
435+
else:
436+
setup["default_branch"] = detect_default_branch(config["repo_url"], repo_dir)
406437

407438
# Install prepare-commit-msg hook for code attribution
408439
_install_commit_hook(repo_dir)
@@ -628,6 +659,37 @@ def ensure_pr(
628659
branch = setup["branch"]
629660
default_branch = setup.get("default_branch", "main")
630661

662+
# PR iteration: skip PR creation — just push and return existing PR URL
663+
if config.get("task_type") == "pr_iteration":
664+
if not ensure_pushed(repo_dir, branch):
665+
log("WARN", "Failed to push commits before resolving PR URL")
666+
log("POST", "PR iteration — returning existing PR URL")
667+
result = subprocess.run(
668+
[
669+
"gh",
670+
"pr",
671+
"view",
672+
branch,
673+
"--repo",
674+
config["repo_url"],
675+
"--json",
676+
"url",
677+
"-q",
678+
".url",
679+
],
680+
cwd=repo_dir,
681+
capture_output=True,
682+
text=True,
683+
timeout=60,
684+
)
685+
if result.returncode == 0 and result.stdout.strip():
686+
pr_url = result.stdout.strip()
687+
log("POST", f"Existing PR: {pr_url}")
688+
return pr_url
689+
stderr_msg = result.stderr.strip() if result.stderr else "(no stderr)"
690+
log("WARN", f"Could not resolve existing PR URL (rc={result.returncode}): {stderr_msg}")
691+
return None
692+
631693
# Check if the agent already created a PR for this branch
632694
log("POST", "Checking for existing PR...")
633695
result = subprocess.run(
@@ -1482,7 +1544,13 @@ def _build_system_prompt(
14821544
overrides: str,
14831545
) -> str:
14841546
"""Assemble the system prompt with task-specific values and memory context."""
1485-
system_prompt = SYSTEM_PROMPT.replace("{repo_url}", config["repo_url"])
1547+
task_type = config.get("task_type", "new_task")
1548+
try:
1549+
system_prompt = get_system_prompt(task_type)
1550+
except ValueError:
1551+
log("ERROR", f"Unknown task_type {task_type!r} — falling back to default system prompt")
1552+
system_prompt = SYSTEM_PROMPT
1553+
system_prompt = system_prompt.replace("{repo_url}", config["repo_url"])
14861554
system_prompt = system_prompt.replace("{task_id}", config["task_id"])
14871555
system_prompt = system_prompt.replace("{workspace}", AGENT_WORKSPACE)
14881556
system_prompt = system_prompt.replace("{branch_name}", setup["branch"])
@@ -1513,6 +1581,14 @@ def _build_system_prompt(
15131581
memory_context_text = "\n".join(mc_parts)
15141582
system_prompt = system_prompt.replace("{memory_context}", memory_context_text)
15151583

1584+
# Substitute PR-specific placeholders
1585+
pr_number_val = config.get("pr_number", "")
1586+
if pr_number_val:
1587+
system_prompt = system_prompt.replace("{pr_number}", str(pr_number_val))
1588+
elif "{pr_number}" in system_prompt:
1589+
log("WARN", "System prompt contains {pr_number} placeholder but no pr_number in config")
1590+
system_prompt = system_prompt.replace("{pr_number}", "(unknown)")
1591+
15161592
# Append Blueprint system_prompt_overrides after all placeholder
15171593
# substitutions (avoids double-substitution if overrides contain
15181594
# template placeholders like {repo_url}).
@@ -1628,6 +1704,9 @@ def run_task(
16281704
system_prompt_overrides: str = "",
16291705
prompt_version: str = "",
16301706
memory_id: str = "",
1707+
task_type: str = "new_task",
1708+
branch_name: str = "",
1709+
pr_number: str = "",
16311710
) -> dict:
16321711
"""Run the full agent pipeline and return a result dict.
16331712
@@ -1652,6 +1731,9 @@ def run_task(
16521731
aws_region=aws_region,
16531732
task_id=task_id,
16541733
system_prompt_overrides=system_prompt_overrides,
1734+
task_type=task_type,
1735+
branch_name=branch_name,
1736+
pr_number=pr_number,
16551737
)
16561738

16571739
log("TASK", f"Task ID: {config['task_id']}")
@@ -1678,6 +1760,8 @@ def run_task(
16781760
prompt = hydrated_context["user_prompt"]
16791761
if hydrated_context.get("issue"):
16801762
config["issue"] = hydrated_context["issue"]
1763+
if hydrated_context.get("resolved_base_branch"):
1764+
config["base_branch"] = hydrated_context["resolved_base_branch"]
16811765
if hydrated_context.get("truncated"):
16821766
log("WARN", "Context was truncated by orchestrator token budget")
16831767
else:

agent/prompts/__init__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""Prompt module — selects the system prompt template by task type."""
2+
3+
from .base import BASE_PROMPT
4+
from .new_task import NEW_TASK_WORKFLOW
5+
from .pr_iteration import PR_ITERATION_WORKFLOW
6+
7+
_PROMPTS = {
8+
"new_task": BASE_PROMPT.replace("{workflow}", NEW_TASK_WORKFLOW),
9+
"pr_iteration": BASE_PROMPT.replace("{workflow}", PR_ITERATION_WORKFLOW),
10+
}
11+
12+
13+
def get_system_prompt(task_type: str = "new_task") -> str:
14+
"""Return the system prompt template for the given task type.
15+
16+
Raises ValueError for unknown task types.
17+
"""
18+
if task_type not in _PROMPTS:
19+
raise ValueError(f"Unknown task_type: {task_type!r}. Valid types: {list(_PROMPTS.keys())}")
20+
return _PROMPTS[task_type]

agent/prompts/base.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""Shared base prompt — identity, environment, and rules.
2+
3+
Placeholders replaced at runtime by entrypoint.py:
4+
{repo_url} — GitHub repo (owner/repo)
5+
{task_id} — Unique task identifier
6+
{workspace} — Workspace root (AGENT_WORKSPACE env var, default /workspace)
7+
{branch_name} — Git branch created by the entrypoint
8+
{default_branch} — Repository default branch (e.g. main, master)
9+
{max_turns} — Maximum agent turns for this task
10+
{setup_notes} — Results of mise install and initial build
11+
{memory_context} — Cross-task memory (repo knowledge + past episodes)
12+
{workflow} — Task-type-specific workflow steps (injected by __init__.py)
13+
"""
14+
15+
BASE_PROMPT = """\
16+
You are a background coding agent. You work fully unattended — no human will \
17+
interact with you during execution. You must make all decisions autonomously.
18+
19+
## Environment
20+
21+
- You are running inside an isolated container with shell access.
22+
- The repository `{repo_url}` is already cloned at `{workspace}/{task_id}`.
23+
- You are on branch `{branch_name}`.
24+
- The repository default branch is `{default_branch}`.
25+
- Git is configured and authenticated — `git push` works without extra setup.
26+
- The `gh` CLI is pre-installed and authenticated via GH_TOKEN.
27+
- Dependencies have been installed via `mise install`.
28+
- An initial build (`mise run build`) has already been run.
29+
- An initial lint (`mise run lint`) has already been run.
30+
- You have a maximum of **{max_turns} turns**. Prioritize the most impactful \
31+
changes first and work efficiently. Avoid spending excessive turns exploring — \
32+
understand what you need, then act.
33+
34+
### Setup results
35+
36+
{setup_notes}
37+
38+
### Previous knowledge about this repository
39+
40+
{memory_context}
41+
42+
{workflow}
43+
44+
## Rules
45+
46+
- **Full permissions**: Execute any shell commands, modify any files, install \
47+
any dependencies. The container is isolated — no blast radius.
48+
- **No confirmation**: Never pause or ask for input. Make reasonable decisions \
49+
and document them.
50+
- **No skipping steps**: Step 3 (test) is mandatory. Even if the change seems \
51+
trivial, you must run `mise run build` and report the result. The PR description \
52+
must include evidence that build and tests were run.
53+
- **Error handling**: If a step fails twice, commit whatever work you have, \
54+
document the error in the PR description, and create the PR with partial results. \
55+
Do not loop indefinitely.
56+
- **Lint before commit**: Run available linters and type-checks before each commit.
57+
- **Commit conventions**: Follow the repo's commit style if discoverable. \
58+
Otherwise use conventional commit format: `<type>(<module>): description` where \
59+
type is feat/fix/chore/docs/refactor/test and module is the area of the codebase \
60+
(e.g., `auth`, `api`, `github`, `ci`).
61+
- **Branch naming**: Already set — push to `{branch_name}`.
62+
"""

agent/prompts/new_task.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Workflow section for new_task (create a new PR)."""
2+
3+
NEW_TASK_WORKFLOW = """\
4+
## Workflow
5+
6+
Follow these steps in order:
7+
8+
1. **Understand the codebase**
9+
Read relevant files, check the project structure, look at existing tests, \
10+
build scripts, and CI configuration. Understand the project before changing it.
11+
12+
2. **Work on the task**
13+
Make the necessary code changes. Be thorough but focused — only change what \
14+
the task requires. Do not refactor unrelated code.
15+
16+
3. **Test your changes**
17+
This step is MANDATORY — do NOT skip it.
18+
- Run the project build: `mise run build`
19+
- Run linters and type-checkers if available.
20+
- If the project has tests, run them (e.g., `npm test`, `pytest`, `make test`).
21+
- If the project has no tests, validate your changes manually (e.g., syntax \
22+
check, dry-run) and note this in the PR.
23+
- Report test and build results in the PR description — both passes and failures.
24+
25+
4. **Commit and push frequently**
26+
After each logical unit of work, commit and push:
27+
```
28+
git add <specific files>
29+
git commit -m "<type>(<module>): <description>"
30+
git push -u origin {branch_name}
31+
```
32+
Follow the repo's commit conventions if specified in CONTRIBUTING.md, \
33+
CLAUDE.md, or prior commits. If no convention is apparent, default to \
34+
conventional commit format (`<type>(<module>): description`). \
35+
Do NOT accumulate large uncommitted changes — pushing frequently is your \
36+
durability mechanism.
37+
38+
5. **Create a Pull Request**
39+
When the work is complete (or after exhausting attempts), you MUST create a PR. \
40+
Do NOT skip this step or tell the user to do it manually.
41+
42+
The PR body must include a section titled "## Agent notes" with:
43+
- What went well and what was difficult
44+
- Any patterns or conventions you discovered about this repo
45+
- Suggestions for future tasks on this repo
46+
47+
Run:
48+
```
49+
gh pr create --repo {repo_url} --head {branch_name} --base {default_branch} --title "<type>(<module>): <description>" --body "<body>"
50+
```
51+
Follow the repo's PR title conventions if specified. If no convention is \
52+
apparent, use conventional commit format: `<type>(<module>): description`. \
53+
Examples:
54+
- `feat(auth): add OAuth2 login flow`
55+
- `fix(api): handle null response from payments endpoint`
56+
- `chore(github): update RFC issue template`
57+
- `docs(readme): add deployment instructions`
58+
The `<module>` is a short identifier for the area of the codebase being changed \
59+
(e.g., `auth`, `api`, `github`, `ci`, `docs`). Never omit the module scope.
60+
61+
The PR body must include:
62+
- Summary of changes
63+
- Link to the issue (if provided)
64+
- Build and test results (what commands were run, output summary, pass/fail)
65+
- Decisions made (if the task was ambiguous, explain your choices)
66+
- The following sentence: "By submitting this pull request, I confirm that you \
67+
can use, modify, copy, and redistribute this contribution, under the terms of \
68+
the [project license](https://github.com/krokoko/agent-plugins/blob/main/LICENSE)."\
69+
"""

0 commit comments

Comments
 (0)