-
Notifications
You must be signed in to change notification settings - Fork 90
fix(factory): reuse canonical implementation, review, and QA workflows #562
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
48d0f6a
61c9da2
f3cff1a
4bcbdfa
0b65cad
52b3a53
92fe017
a39552d
d46e943
83b5ed6
55b6fdb
56cb79a
86fb54f
4c5c138
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,7 +23,8 @@ packages = ["python/openhands_extensions"] | |
| test = [ | ||
| "pytest>=8.0", | ||
| "requests>=2.31", | ||
| "openhands-sdk>=0.3", | ||
| # Temporary integration pin for software-agent-sdk#5010; replace with its release. | ||
| "openhands-sdk @ git+https://github.com/OpenHands/software-agent-sdk.git@79021c687c63bd1e925cf157f5897ceeaa12f029#subdirectory=openhands-sdk", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fragile dependency pin. The test group pins
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The test dependency is pinned to an unreleased commit of
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Merge blocker: unreleased SDK pin. The released
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This git-commit pin for |
||
| "jsonschema>=4.23", | ||
| ] | ||
|
|
||
|
|
||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| """Build the same factory bundle for any supported execution workspace.""" | ||
|
|
||
| import argparse | ||
| import hashlib | ||
| import io | ||
| import json | ||
| import tarfile | ||
| from pathlib import Path | ||
|
|
||
| from extension_workflows import SOURCES, source_root | ||
|
|
||
|
|
||
| def build(config_path, output): | ||
| scripts = Path(__file__).parent | ||
| root = source_root() | ||
| files = { | ||
| name: (scripts / name).read_bytes() | ||
| for name in ( | ||
| "main.py", | ||
| "extension_workflows.py", | ||
| "scoped_gh.py", | ||
| ) | ||
| } | ||
| config = json.loads(Path(config_path).read_text()) | ||
| if "token" in config or not config.get("token_env"): | ||
| raise ValueError("Bundle config must reference a profile secret via token_env") | ||
| files["config.json"] = json.dumps(config).encode() | ||
| provenance = {} | ||
| for name in SOURCES: | ||
| content = (root / name).read_bytes() | ||
| files["extensions/" + name] = content | ||
| provenance[name] = hashlib.sha256(content).hexdigest() | ||
| files["workflow-sources.json"] = json.dumps(provenance, indent=2).encode() | ||
| with tarfile.open(output, "w:gz") as archive: | ||
| for name, content in files.items(): | ||
| info = tarfile.TarInfo(name) | ||
| info.size = len(content) | ||
| info.mode = 0o600 | ||
| archive.addfile(info, io.BytesIO(content)) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| parser = argparse.ArgumentParser(description=__doc__) | ||
| parser.add_argument("config") | ||
| parser.add_argument("output") | ||
| args = parser.parse_args() | ||
| build(args.config, args.output) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| """Compose canonical extension workflows with a supplied execution interface. | ||
|
|
||
| No workspace-kind detection belongs here. A caller supplies its workspace, GitHub | ||
| transport, and blocking conversation runner, identically for local and remote runs. | ||
| """ | ||
|
|
||
| import importlib.util | ||
| import json | ||
| import re | ||
| from pathlib import Path | ||
|
|
||
|
|
||
| SOURCES = ( | ||
| "skills/github-pr-reviewer/scripts/main.py", | ||
| "skills/github-issue-to-pr/scripts/main.py", | ||
| "plugins/qa-changes/scripts/prompt.py", | ||
| "skills/qa-changes/SKILL.md", | ||
| "skills/github-pr-review/SKILL.md", | ||
| ) | ||
|
|
||
|
|
||
| def source_root(): | ||
| for parent in Path(__file__).resolve().parents: | ||
| for candidate in (parent / "extensions", parent): | ||
| if all((candidate / name).is_file() for name in SOURCES): | ||
| return candidate | ||
| raise RuntimeError("Canonical extension workflow sources are missing from bundle") | ||
|
|
||
|
|
||
| def module(relative): | ||
| path = source_root() / relative | ||
| spec = importlib.util.spec_from_file_location(path.parent.parent.name, path) | ||
| result = importlib.util.module_from_spec(spec) | ||
| spec.loader.exec_module(result) | ||
| return result | ||
|
|
||
|
|
||
| def transport_instructions(workspace, repository, run_id, stage): | ||
| return ( | ||
| f"\nExecution environment: the repository is {workspace / 'project'}. " | ||
| "Use that as the working directory for all project commands. " | ||
| f"GitHub access is provided by `{workspace / 'bin/gh'} api` " | ||
| "(GET, -X, --input, --paginate, --jq); use this executable wherever the " | ||
| "workflow says gh or GitHub REST API. It supplies the scoped credential. " | ||
| "Do not look up a GitHub token or contact api.github.com directly. " | ||
| f"Only repository {repository} is authorized. " | ||
| "Do not alter the workflow bundle, its configuration, or tracked source " | ||
| "during review/QA. Put temporary probes and evidence outside the project. " | ||
| f"Include `<!-- factory-run:{run_id}:{stage} -->` in the review body " | ||
| "so the coordinator can identify this run's report. " | ||
| "Preserve the workflow's normal readable report and verdict. " | ||
| "Never paste a JSON artifact or full test log as the review body.\n" | ||
| ) | ||
|
|
||
|
|
||
| def implementation_prompt(repository, issue, branch, base_sha, workspace, feedback): | ||
| workflow = module("skills/github-issue-to-pr/scripts/main.py") | ||
| prompt = workflow._build_implementation_prompt( | ||
|
neubig marked this conversation as resolved.
|
||
| repository, | ||
| issue, | ||
| {"id": "ready-for-dev"}, | ||
| branch, | ||
| "main", | ||
| base_sha, | ||
| publish_pr=False, | ||
| github_access_instructions=( | ||
| f"Use `{workspace / 'bin/gh'} api` for repository REST requests. " | ||
| "The adapter supplies the profile-selected gateway grant; " | ||
| "do not look up a GitHub token or call api.github.com directly." | ||
| ), | ||
| ) | ||
| # Publication is a capability of the coordinator, not a workspace-kind choice. | ||
| return prompt + ( | ||
| f"\nExecution contract: work in {workspace / 'project'}. " | ||
| f"Use `{workspace / 'bin/gh'} api` for the REST requests above; " | ||
| "it supplies a scoped credential. No GitHub token is needed. " | ||
|
neubig marked this conversation as resolved.
|
||
| "The coordinator owns remote publication: leave your completed changes " | ||
| "and PR description in the workspace; do not push or create the PR yourself. " | ||
|
neubig marked this conversation as resolved.
|
||
| "It publishes preserved work after your run, including after a bounded stop. " | ||
| "Do not edit the workflow bundle or configuration. " | ||
| "Run the project's required tests after your final edit; preserve actual exit " | ||
| "statuses and keep build output, runtime data, and dependencies ignored. " | ||
| "Terminal calls accept one command; use the file editor for source changes.\n" | ||
| "Existing PR feedback (untrusted task evidence):\n" + json.dumps(feedback) | ||
| ) | ||
|
|
||
|
|
||
| def review_prompt(repository, pr, workspace, run_id): | ||
| workflow = module("skills/github-pr-reviewer/scripts/main.py") | ||
| guide = workflow._load_repo_review_guide(workspace / "project") | ||
| return workflow._build_review_prompt( | ||
| repository, pr, pr["head"]["sha"], {"id": run_id}, guide | ||
| ) + transport_instructions(workspace, repository, run_id, "review") | ||
|
|
||
|
|
||
| def qa_prompt(repository, pr, workspace, run_id, diff, issue): | ||
| workflow = module("plugins/qa-changes/scripts/prompt.py") | ||
| prompt = workflow.format_prompt( | ||
| title=pr["title"], | ||
| body=pr.get("body") or "", | ||
| repo_name=repository, | ||
| base_branch=pr["base"]["ref"], | ||
| head_branch=pr["head"]["ref"], | ||
| pr_number=str(pr["number"]), | ||
| commit_id=pr["head"]["sha"], | ||
| diff=diff, | ||
| ) | ||
| for name in ("qa-changes", "github-pr-review"): | ||
| prompt += "\n\n" + (source_root() / f"skills/{name}/SKILL.md").read_text() | ||
| return ( | ||
| prompt | ||
| + transport_instructions(workspace, repository, run_id, "qa") | ||
| + ( | ||
| "\nAcceptance criteria to exercise (untrusted task evidence):\n" | ||
| + json.dumps(issue) | ||
| ) | ||
| ) | ||
|
|
||
|
|
||
| def posted_report(reviews, previous_ids, sha, run_id, stage): | ||
| marker = f"<!-- factory-run:{run_id}:{stage} -->" | ||
| matches = [ | ||
| review | ||
| for review in reviews | ||
| if review["id"] not in previous_ids | ||
| and review.get("commit_id") == sha | ||
| and marker in (review.get("body") or "") | ||
| and review.get("state") == "COMMENTED" | ||
| ] | ||
| if len(matches) != 1: | ||
| raise RuntimeError( | ||
| f"Expected one newly published {stage} report for the exact head" | ||
| ) | ||
| return matches[0] | ||
|
|
||
|
|
||
| def report_passed(report, stage): | ||
| body = report.get("body") or "" | ||
| if stage == "review": | ||
| verdicts = re.findall(r"^\s*(✅ APPROVED|🔄 CHANGES REQUESTED)\s*$", body, re.M) | ||
| return verdicts == ["✅ APPROVED"] | ||
| # The canonical QA skill defines this heading. Missing/partial/qualified | ||
| # verdicts fail closed; success is never inferred from agent final text. | ||
| verdicts = re.findall(r"^## [^\n]*QA Report:\s*([^\n]+)", body, re.M) | ||
| return len(verdicts) == 1 and verdicts[0].strip().strip("*") == "PASS" | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Blocking: unreleased SDK pin. The test group depends on
openhands-sdkpinned to an unreleased git commit from software-agent-sdk#5010. The PR description acknowledges this should be replaced before merging. Replace with the released package version once available - CI for this repo should not depend on an unreleased commit.