Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .agents/skills/implement-issue/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ When the prompt asks for `pr-metadata.json`, the agent must produce a JSON file
}
```

- **`branch_name`**: the branch the agent pushed to. Must start with the prefix supplied in the prompt (e.g. `oz-agent/implement--{N}`) and contain a short auto-generated suffix describing the change.
- **`branch_name`**: the branch the agent pushed to. Follow the branch contract in the prompt exactly. When the prompt says the run is using an approved spec PR branch, `branch_name` must equal that branch exactly and must not include a descriptive suffix. For standalone implementation PR branches, a short descriptive suffix is allowed when the prompt permits it, as long as the branch starts with the supplied prefix (e.g. `oz-agent/implement-issue-{N}-add-retry-logic`).
- **`pr_title`**: a conventional-commit-style PR title derived from the actual changes.
- **`pr_summary`**: the full markdown PR body. The first line must be `Closes #<issue_number>` so GitHub auto-closes the issue when the PR merges.

Expand Down
61 changes: 55 additions & 6 deletions core/workflows/create_implementation_from_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,54 @@ def build_create_implementation_prompt(
spec_driven_implementation_skill_path: str,
implement_issue_skill_path: str,
coauthor_directives: str,
selected_spec_pr_number: int = 0,
) -> str:
"""Render the cloud-mode create-implementation prompt.

Used by the webhook dispatch path to feed the implementation agent
the issue/spec context and required handoff contract.
"""
if selected_spec_pr_number:
branch_contract_lines = "\n ".join(
[
"- This implementation is running on the approved spec PR branch, "
f"so keep spec and implementation changes together on `{target_branch}` exactly.",
f"- Do not append a descriptive suffix to `{target_branch}` or push "
"implementation changes to any other branch.",
"- If you produce changes, write `pr-metadata.json` at the repository "
"root containing a JSON object with these required fields:",
" - `branch_name`: the approved spec PR branch you pushed to. "
f"It must equal `{target_branch}` exactly.",
" - `pr_title`: a conventional-commit-style PR title derived from "
"the actual changes (e.g. `feat: add retry logic for transient API failures`).",
" - `pr_summary`: the full markdown PR body. The first line must be "
f"`Closes #{issue_number}` so GitHub auto-closes the issue when the PR merges.",
]
)
commit_branch_line = (
f"- If you produce changes, commit them to `{target_branch}` and push "
"that branch to origin."
)
else:
branch_contract_lines = "\n ".join(
[
"- If you produce changes, write `pr-metadata.json` at the repository "
"root containing a JSON object with these required fields:",
" - `branch_name`: the branch you pushed to. You may customize it by "
"appending a short descriptive slug to the default "
f"(e.g. `{target_branch}-add-retry-logic`), but it must start with "
f"`{target_branch}`.",
" - `pr_title`: a conventional-commit-style PR title derived from "
"the actual changes (e.g. `feat: add retry logic for transient API failures`).",
" - `pr_summary`: the full markdown PR body. The first line must be "
f"`Closes #{issue_number}` so GitHub auto-closes the issue when the PR merges.",
]
)
commit_branch_line = (
"- If you produce changes, commit them to the branch specified in "
"your `pr-metadata.json` `branch_name` field and push that branch "
"to origin."
)
return dedent(
f"""
Create an implementation update for GitHub issue #{issue_number} in repository {owner}/{repo}.
Expand All @@ -96,12 +138,9 @@ def build_create_implementation_prompt(
- If that branch already exists, fetch it and continue from it. Otherwise create it from `{default_branch}`.
- Align the implementation with the plan context above when present.
- Run the most relevant validation available in the repository.
- If you produce changes, write `pr-metadata.json` at the repository root containing a JSON object with these required fields:
- `branch_name`: the branch you pushed to. You may customize it by appending a short descriptive slug to the default (e.g. `{target_branch}-add-retry-logic`), but it must start with `{target_branch}`.
- `pr_title`: a conventional-commit-style PR title derived from the actual changes (e.g. `feat: add retry logic for transient API failures`).
- `pr_summary`: the full markdown PR body. The first line must be `Closes #{issue_number}` so GitHub auto-closes the issue when the PR merges.
{branch_contract_lines}
- After writing `pr-metadata.json`, upload it as an artifact via `oz artifact upload pr-metadata.json` (or `oz-preview artifact upload pr-metadata.json` if the `oz` CLI is not available). Either CLI is acceptable — use whichever one is installed in the environment. The subcommand is `artifact` (singular) on both CLIs; do not use `artifacts`.
- If you produce changes, commit them to the branch specified in your `pr-metadata.json` `branch_name` field and push that branch to origin.
{commit_branch_line}
- After pushing, stop. Do not open or update the pull request yourself, and do not invoke `gh pr create`, `gh pr edit`, or equivalent commands.
- The outer workflow owns any pull-request creation or pull-request title/body refresh after your branch push and `pr-metadata.json` upload.
- If no implementation diff is warranted, do not push the branch.
Expand Down Expand Up @@ -314,6 +353,7 @@ def build_create_implementation_prompt_for_dispatch(
context.get("implement_issue_skill_path") or ""
),
coauthor_directives=str(context.get("coauthor_directives") or ""),
selected_spec_pr_number=int(context.get("selected_spec_pr_number") or 0),
)


Expand Down Expand Up @@ -390,6 +430,11 @@ def apply_create_implementation_result(

if metadata is not None:
agent_branch = str(metadata.get("branch_name") or "").strip()
if selected_spec_pr_number and agent_branch and agent_branch != target_branch:
raise RuntimeError(
"pr-metadata.json branch_name must equal the approved spec PR "
f"branch {target_branch!r}, got {agent_branch!r}."
)
# Allow the agent to extend the default target branch with a
# descriptive slug. Reject any other branch name to avoid
# accidentally pushing onto an unrelated branch.
Expand All @@ -402,7 +447,11 @@ def apply_create_implementation_result(
)
):
target_branch = agent_branch
created_after = created_at.replace(tzinfo=timezone.utc) if created_at.tzinfo is None else created_at
created_after = (
created_at.replace(tzinfo=timezone.utc)
if created_at.tzinfo is None
else created_at
)
created_after = created_after - timedelta(minutes=1)

if not branch_updated_since(
Expand Down
99 changes: 99 additions & 0 deletions tests/test_create_workflow_apply.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,65 @@
from . import conftest # noqa: F401


class CreateImplementationPromptTest(unittest.TestCase):
def _prompt(self, **overrides: object) -> str:
from core.workflows.create_implementation_from_issue import (
build_create_implementation_prompt,
)

kwargs: dict[str, object] = {
"owner": "acme",
"repo": "widgets",
"issue_number": 12,
"issue_title": "Add retries",
"issue_labels": ["ready-to-implement"],
"issue_assignees": ["oz-agent"],
"spec_context_text": "Spec body",
"target_branch": "oz-agent/implement-issue-12",
"default_branch": "main",
"implement_specs_skill_path": ".agents/skills/implement-specs/SKILL.md",
"spec_driven_implementation_skill_path": (
".agents/skills/spec-driven-implementation/SKILL.md"
),
"implement_issue_skill_path": ".agents/skills/implement-issue/SKILL.md",
"coauthor_directives": "",
}
kwargs.update(overrides)
return build_create_implementation_prompt(**kwargs) # type: ignore[arg-type]

def test_approved_spec_prompt_requires_exact_branch(self) -> None:
prompt = self._prompt(
target_branch="oz-agent/spec-issue-12",
selected_spec_pr_number=44,
)

self.assertIn(
"approved spec PR branch, so keep spec and implementation "
"changes together on `oz-agent/spec-issue-12` exactly",
prompt,
)
self.assertIn(
"Do not append a descriptive suffix to `oz-agent/spec-issue-12`",
prompt,
)
self.assertIn(
"`branch_name`: the approved spec PR branch you pushed to. It "
"must equal `oz-agent/spec-issue-12` exactly.",
prompt,
)
self.assertNotIn("You may customize it by appending", prompt)

def test_standalone_prompt_still_allows_suffixed_branch(self) -> None:
prompt = self._prompt()

self.assertIn(
"You may customize it by appending a short descriptive slug to "
"the default (e.g. `oz-agent/implement-issue-12-add-retry-logic`)",
prompt,
)
self.assertNotIn("approved spec PR branch", prompt)


class CreateImplementationApplyTest(unittest.TestCase):
def _context(self) -> dict[str, object]:
return {
Expand Down Expand Up @@ -92,6 +151,46 @@ def test_accepts_delimiter_bounded_branch_override_and_uses_cushion(self) -> Non
run_created_at.replace(tzinfo=timezone.utc) - timedelta(minutes=1),
)

def test_approved_spec_rejects_branch_override(self) -> None:
from core.workflows.create_implementation_from_issue import (
apply_create_implementation_result,
)

progress = MagicMock()
run = SimpleNamespace(
run_id="run-1",
created_at=datetime(2026, 4, 30, 12, 0, tzinfo=timezone.utc),
)
context = self._context()
context.update(
{
"target_branch": "oz-agent/spec-issue-12",
"selected_spec_pr_number": 44,
"selected_spec_pr_url": "https://github.com/acme/widgets/pull/44",
}
)
metadata = {
"branch_name": "oz-agent/spec-issue-12-add-retries",
"pr_title": "fix: add retries",
"pr_summary": "Closes #12\n\nSummary",
}

with patch(
"core.workflows.create_implementation_from_issue.branch_updated_since"
) as branch_updated_since, self.assertRaisesRegex(
RuntimeError,
"branch_name must equal the approved spec PR branch",
):
apply_create_implementation_result(
MagicMock(),
context=context,
run=run,
result=metadata,
progress=progress,
)

branch_updated_since.assert_not_called()


class CreateSpecApplyTest(unittest.TestCase):
def test_branch_updated_since_uses_one_minute_cushion(self) -> None:
Expand Down
Loading