From 62526cfc3d9646179a637e5dfce8cc6f534bebf1 Mon Sep 17 00:00:00 2001 From: Joe Davis Date: Thu, 11 Jun 2026 15:31:17 -0700 Subject: [PATCH 1/5] Bump dev-team version from 1.2.0 to 1.2.1 (#31) --- plugins/dev-team/.claude-plugin/plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/dev-team/.claude-plugin/plugin.json b/plugins/dev-team/.claude-plugin/plugin.json index 82b741b..f493d17 100644 --- a/plugins/dev-team/.claude-plugin/plugin.json +++ b/plugins/dev-team/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "dev-team", - "version": "1.2.0", + "version": "1.2.1", "description": "Dev-team agent pipeline: researcher, developer, reviewer, and debugger agents for implementing Jira tasks and fixing GitHub issues.", "commands": "./commands" } From c9beb96fcc70bcb25f61c3640803919dae347a2a Mon Sep 17 00:00:00 2001 From: Claude Code acting for jodavis Date: Fri, 12 Jun 2026 07:20:07 -0700 Subject: [PATCH 2/5] =?UTF-8?q?ADR-277:=20Refactor=20Step=20protocol=20?= =?UTF-8?q?=E2=80=94=20get=5Factions/handle=5Fresults=20+=20ParallelSteps?= =?UTF-8?q?=20(#32)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ADR-277: uncommitted changes at validation * ADR-277: Address PR review comments - Delete Step.run() shim - Remove redundant print() status calls from step methods - Remove results parameter from handle_results() and --results CLI arg - Split reviewing state into creating-pr + reviewing; delete _REDISPATCH - Make combine_results abstract on ParallelSteps - Update workflow files and dev-team.md orchestration docs accordingly Co-Authored-By: Claude Sonnet 4.6 * Bump dev-tools version from 1.2.1 to 1.2.2 * Increment plugin version in dev-team plugin JSON Increment the version in the plugin JSON file for the dev-team plugin by 0.0.1 in each task. --------- Co-authored-by: Joe Davis Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Joe Davis --- _spec_AgentOrchestration.md | 3 + plugins/dev-team/.claude-plugin/plugin.json | 2 +- plugins/dev-team/scripts/dev_team.py | 829 ++++++++++-------- plugins/dev-team/scripts/fix-issue-plan.md | 3 +- .../dev-team/scripts/implement-task-plan.md | 3 +- plugins/dev-team/scripts/test_dev_team.py | 271 +++++- 6 files changed, 713 insertions(+), 398 deletions(-) diff --git a/_spec_AgentOrchestration.md b/_spec_AgentOrchestration.md index fb7a0fb..204a987 100644 --- a/_spec_AgentOrchestration.md +++ b/_spec_AgentOrchestration.md @@ -614,6 +614,7 @@ One-time operator configuration required before the pipeline can run. No code ch - [ ] `~/.dev-team` added to `permissions.additionalDirectories` in `~/.claude/settings.json` (eliminates per-write permission prompts) - [ ] Plugin installation confirmed current (latest changes pulled from `dev-team-agents` repo) - [ ] Given `GH_TOKEN` is set to Claude's PAT, when the developer agent runs `gh pr create`, then no account-picker prompt appears +- [ ] Increment the version in `plugins/dev-team/.claude-plugin/plugin.json` by 0.0.1 --- @@ -627,6 +628,7 @@ Run a full implement pipeline cycle to confirm the step-machine architecture wor - [ ] Given a full researcher → developer → reviewer → sign-off cycle, when it completes, then the context file at `~/.dev-team//.md` contains all expected sections and no `claude -p` processes are spawned - [ ] Given `GH_TOKEN` is set, when the developer agent creates a PR, then no account-picker prompt appears - [ ] Sub-agents (researcher, developer, reviewer) successfully make Jira MCP and GitHub MCP calls directly without top-level relay +- [ ] Increment the version in `plugins/dev-team/.claude-plugin/plugin.json` by 0.0.1 --- @@ -645,6 +647,7 @@ Implement `agents/troubleshooter.md` with the sign-off deadlock condition as the - [ ] Unknown trigger fallback: returns `{"action": "needs_user_input", "reason": "Unknown trigger: . Manual inspection required."}` - [ ] Writes diagnosis to `` before returning in all cases - [ ] Given the pipeline has reached `signoff_cycle_count == 2` with a deadlocked thread, when the troubleshooter runs, then it asks the user how to proceed and acts on the answer without re-running the sign-off +- [ ] Increment the version in `plugins/dev-team/.claude-plugin/plugin.json` by 0.0.1 ## Related Epics diff --git a/plugins/dev-team/.claude-plugin/plugin.json b/plugins/dev-team/.claude-plugin/plugin.json index f493d17..34817c6 100644 --- a/plugins/dev-team/.claude-plugin/plugin.json +++ b/plugins/dev-team/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "dev-team", - "version": "1.2.1", + "version": "1.2.2", "description": "Dev-team agent pipeline: researcher, developer, reviewer, and debugger agents for implementing Jira tasks and fixing GitHub issues.", "commands": "./commands" } diff --git a/plugins/dev-team/scripts/dev_team.py b/plugins/dev-team/scripts/dev_team.py index 27d31dc..42ab45c 100644 --- a/plugins/dev-team/scripts/dev_team.py +++ b/plugins/dev-team/scripts/dev_team.py @@ -492,22 +492,32 @@ class Step(ABC): handles: str @abstractmethod - def run(self, ctx: PipelineContext) -> str: - """Execute step logic. Returns a trigger name, OR calls exit_with_actions.""" + def get_actions(self) -> list[dict]: + """Return action descriptors to dispatch. Empty list means inline step.""" + ... + + @abstractmethod + def handle_results(self) -> str: + """Process results from the context file and return a trigger moniker.""" ... class FindSpecStep(Step): handles = "spec-finding" - def run(self, ctx: PipelineContext) -> str: + def __init__(self, ctx: "PipelineContext") -> None: + self._ctx = ctx + + def get_actions(self) -> list[dict]: + """Inline step — no actions needed.""" + return [] + + def handle_results(self) -> str: + ctx = self._ctx if ctx.spec_path: - print("Spec path already set — skipping.", flush=True) return "spec_found" - print(f"Searching for spec for {ctx.work_item_id}...", flush=True) spec_file = find_spec_file(ctx.work_item_id) ctx.spec_path = str(spec_file.relative_to(REPO_ROOT)) - print(f"Found {spec_file}", flush=True) return "spec_found" @@ -516,29 +526,16 @@ class DebugStep(Step): _PENDING_KEY = "debug" - def __init__(self, context_path: Path) -> None: + def __init__(self, ctx: "PipelineContext", context_path: Path) -> None: + self._ctx = ctx self._context_path = context_path - def run(self, ctx: PipelineContext) -> str: + def get_actions(self) -> list[dict]: + ctx = self._ctx if ctx.debug_report: - _handle_agent_success(ctx) - if "# Debug report for" not in ctx.debug_report: - ctx.last_failure = f"Bug could not be reproduced.\n\n{ctx.debug_report}" - return "reproduction_failed" - print("Debugging complete.", flush=True) - return "debug_done" - - if ctx.pending_agent == self._PENDING_KEY: - _handle_agent_failure(ctx) - _check_and_trigger_troubleshooter( - "consecutive_failures", CONSECUTIVE_FAILURES_THRESHOLD, - ctx.consecutive_failures, ctx, self._context_path, - ) - - print(f"Debugger is investigating {ctx.work_item_id}...", flush=True) - ctx.pending_agent = self._PENDING_KEY - ctx.save(self._context_path) - exit_with_actions([{ + # Result already available — inline step + return [] + return [{ "action": "spawn_agent", "message": f"Debugger is investigating {ctx.work_item_id}.", "agent": "debugger", @@ -547,8 +544,25 @@ def run(self, ctx: PipelineContext) -> str: "args": ctx.work_item_id, "read_sections": [], "write_section": "Debug Report", - "result_format": "reproduced | not_reproduced", - }]) + "result_format": "success | failed", + }] + + def handle_results(self) -> str: + ctx = self._ctx + if ctx.debug_report: + _handle_agent_success(ctx) + if "# Debug report for" not in ctx.debug_report: + ctx.last_failure = f"Bug could not be reproduced.\n\n{ctx.debug_report}" + return "reproduction_failed" + return "debug_done" + # Agent ran but wrote nothing + _handle_agent_failure(ctx) + _check_and_trigger_troubleshooter( + "consecutive_failures", CONSECUTIVE_FAILURES_THRESHOLD, + ctx.consecutive_failures, ctx, self._context_path, + ) + # If we get here, consecutive_failures has not hit threshold — return failure trigger + return "reproduction_failed" class ResearchStep(Step): @@ -556,28 +570,17 @@ class ResearchStep(Step): _PENDING_KEY = "research" - def __init__(self, skill: str, context_path: Path) -> None: + def __init__(self, skill: str, ctx: "PipelineContext", context_path: Path) -> None: self._skill = skill + self._ctx = ctx self._context_path = context_path - def run(self, ctx: PipelineContext) -> str: + def get_actions(self) -> list[dict]: + ctx = self._ctx if ctx.brief: - _handle_agent_success(ctx) - print("Research complete.", flush=True) - return "research_done" - - if ctx.pending_agent == self._PENDING_KEY: - _handle_agent_failure(ctx) - _check_and_trigger_troubleshooter( - "consecutive_failures", CONSECUTIVE_FAILURES_THRESHOLD, - ctx.consecutive_failures, ctx, self._context_path, - ) - - print(f"Researcher is planning work for {ctx.work_item_id}...", flush=True) + return [] read_sections = ["Debug Report"] if ctx.debug_report else [] - ctx.pending_agent = self._PENDING_KEY - ctx.save(self._context_path) - exit_with_actions([{ + return [{ "action": "spawn_agent", "message": f"Researcher is planning work for {ctx.work_item_id}.", "agent": "researcher", @@ -586,8 +589,20 @@ def run(self, ctx: PipelineContext) -> str: "args": f"{ctx.work_item_id} {ctx.spec_path}", "read_sections": read_sections, "write_section": "Researcher Brief", - "result_format": "briefed | failed", - }]) + "result_format": "success | failed", + }] + + def handle_results(self) -> str: + ctx = self._ctx + if ctx.brief: + _handle_agent_success(ctx) + return "research_done" + _handle_agent_failure(ctx) + _check_and_trigger_troubleshooter( + "consecutive_failures", CONSECUTIVE_FAILURES_THRESHOLD, + ctx.consecutive_failures, ctx, self._context_path, + ) + return "research_done" class ImplementStep(Step): @@ -595,26 +610,15 @@ class ImplementStep(Step): _PENDING_KEY = "implement" - def __init__(self, context_path: Path) -> None: + def __init__(self, ctx: "PipelineContext", context_path: Path) -> None: + self._ctx = ctx self._context_path = context_path - def run(self, ctx: PipelineContext) -> str: + def get_actions(self) -> list[dict]: + ctx = self._ctx if ctx.work_summaries: - _handle_agent_success(ctx) - print("Implementation already complete in context — skipping.", flush=True) - return "impl_done" - - if ctx.pending_agent == self._PENDING_KEY: - _handle_agent_failure(ctx) - _check_and_trigger_troubleshooter( - "consecutive_failures", CONSECUTIVE_FAILURES_THRESHOLD, - ctx.consecutive_failures, ctx, self._context_path, - ) - - print(f"Developer is implementing {ctx.work_item_id}...", flush=True) - ctx.pending_agent = self._PENDING_KEY - ctx.save(self._context_path) - exit_with_actions([{ + return [] + return [{ "action": "spawn_agent", "message": "Researcher has written the task brief. Developer is now implementing.", "agent": "developer", @@ -623,8 +627,20 @@ def run(self, ctx: PipelineContext) -> str: "context_file": str(self._context_path), "read_sections": ["Researcher Brief"], "write_section": "Implementation Summary", - "result_format": "implemented | failed | needs_clarification", - }]) + "result_format": "success | failed", + }] + + def handle_results(self) -> str: + ctx = self._ctx + if ctx.work_summaries: + _handle_agent_success(ctx) + return "impl_done" + _handle_agent_failure(ctx) + _check_and_trigger_troubleshooter( + "consecutive_failures", CONSECUTIVE_FAILURES_THRESHOLD, + ctx.consecutive_failures, ctx, self._context_path, + ) + return "impl_done" class ValidateStep(Step): @@ -632,302 +648,316 @@ class ValidateStep(Step): _PENDING_KEY = "validate" - def __init__(self, context_path: Path, log_dir: Path) -> None: + def __init__(self, ctx: "PipelineContext", context_path: Path, log_dir: Path) -> None: + self._ctx = ctx self._context_path = context_path self._log_dir = log_dir - def run(self, ctx: PipelineContext) -> str: + def get_actions(self) -> list[dict]: + ctx = self._ctx + if ctx.validate_result: + return [] + self._log_dir.mkdir(parents=True, exist_ok=True) + timestamp = datetime.datetime.now().strftime("%Y%m%dT%H%M%S") + log_path = self._log_dir / f"{ctx.work_item_id}-validate-{timestamp}.log" + ctx.build_log = str(log_path) + ext = ".cmd" if sys.platform == "win32" else ".sh" + validate_script = REPO_ROOT / "scripts" / f"validate{ext}" + command = f'cmd /c "{validate_script}"' if sys.platform == "win32" else f'bash "{validate_script}"' + return [{ + "action": "run_script", + "message": "Running build and test validation.", + "command": command, + "log_file": str(log_path), + "write_section": "Validate Result", + "result_format": "success | failed", + }] + + def handle_results(self) -> str: + ctx = self._ctx if ctx.validate_result: - # Re-entry: script-runner has written the result. result = ctx.validate_result.strip() ctx.validate_result = "" ctx.pending_agent = "" if result == "passed": - print("Validation passed.", flush=True) ctx.last_failure = "" _commit_and_push(ctx.work_item_id) return "clean" - print(f"Validation FAILED. Log: {ctx.build_log}", flush=True) ctx.last_failure = ( f"Build or test failures.\n\n" f"Full log (read this for details): {ctx.build_log}" ) return "build_failed" + # Script-runner ran but wrote nothing + _handle_agent_failure(ctx) + _check_and_trigger_troubleshooter( + "consecutive_failures", CONSECUTIVE_FAILURES_THRESHOLD, + ctx.consecutive_failures, ctx, self._context_path, + ) + return "build_failed" - if ctx.pending_agent == self._PENDING_KEY: - # Re-entry with no result — script-runner failed to write outcome. - _handle_agent_failure(ctx) - _check_and_trigger_troubleshooter( - "consecutive_failures", CONSECUTIVE_FAILURES_THRESHOLD, - ctx.consecutive_failures, ctx, self._context_path, - ) - self._log_dir.mkdir(parents=True, exist_ok=True) - timestamp = datetime.datetime.now().strftime("%Y%m%dT%H%M%S") - log_path = self._log_dir / f"{ctx.work_item_id}-validate-{timestamp}.log" - ctx.build_log = str(log_path) - ctx.pending_agent = self._PENDING_KEY - ctx.save(self._context_path) +class CreatePrStep(Step): + handles = "creating-pr" - ext = ".cmd" if sys.platform == "win32" else ".sh" - validate_script = REPO_ROOT / "scripts" / f"validate{ext}" - command = f'cmd /c "{validate_script}"' if sys.platform == "win32" else f'bash "{validate_script}"' - print(f"Spawning script-runner to validate {ctx.work_item_id}...", flush=True) - exit_with_actions([{ - "action": "run_script", - "message": "Running build and test validation.", - "command": command, - "log_file": str(log_path), - "write_section": "Validate Result", - "result_format": "passed | failed", - }]) + def __init__(self, ctx: "PipelineContext", context_path: Path) -> None: + self._ctx = ctx + self._context_path = context_path + + def get_actions(self) -> list[dict]: + ctx = self._ctx + if ctx.pr_url: + # Recovery re-entry — PR already created + return [] + read_sections = ["Researcher Brief", "Implementation Summary"] + for i in range(1, len(ctx.work_summaries)): + read_sections.append(f"Fix {i}") + return [{ + "action": "spawn_agent", + "message": "Implementation complete. Developer is creating a pull request.", + "agent": "developer", + "skill": "developer-create-pr", + "args": ctx.work_item_id, + "context_file": str(self._context_path), + "read_sections": read_sections, + "write_section": "PR URL", + "result_format": "success | failed", + }] + + def handle_results(self) -> str: + ctx = self._ctx + if ctx.pr_url: + # Inline path: already had pr_url + _handle_agent_success(ctx) + return "pr_created" + # Try to extract pr_url from the PR URL section written by the agent + text = self._context_path.read_text(encoding="utf-8") + _, body = _parse_frontmatter(text) + sections = _parse_sections(body) + pr_url_section = sections.get("PR URL", "") + if pr_url_section: + m = re.search(r"https://github\.com/[^\s]+/pull/\d+", pr_url_section) + if m: + ctx.pr_url = m.group(0) + _handle_agent_success(ctx) + ctx.save(self._context_path) + return "pr_created" + _handle_agent_failure(ctx) + _check_and_trigger_troubleshooter( + "consecutive_failures", CONSECUTIVE_FAILURES_THRESHOLD, + ctx.consecutive_failures, ctx, self._context_path, + ) + return "pr_created" class ReviewStep(Step): handles = "reviewing" - _PENDING_CREATE_PR = "create-pr" - _PENDING_REVIEW = "reviewer-review" - - def __init__(self, context_path: Path) -> None: + def __init__(self, ctx: "PipelineContext", context_path: Path) -> None: + self._ctx = ctx self._context_path = context_path - def run(self, ctx: PipelineContext) -> str: - # Sub-step 1: create PR - if not ctx.pr_url: - if ctx.pending_agent == self._PENDING_CREATE_PR: - # Re-entry: try to extract pr_url written to the "PR URL" section. - text = self._context_path.read_text(encoding="utf-8") - _, body = _parse_frontmatter(text) - sections = _parse_sections(body) - pr_url_section = sections.get("PR URL", "") - if pr_url_section: - m = re.search(r"https://github\.com/[^\s]+/pull/\d+", pr_url_section) - if m: - ctx.pr_url = m.group(0) - ctx.save(self._context_path) - if not ctx.pr_url: - # Agent ran but pr_url still not populated — treat as failure. - _handle_agent_failure(ctx) - _check_and_trigger_troubleshooter( - "consecutive_failures", CONSECUTIVE_FAILURES_THRESHOLD, - ctx.consecutive_failures, ctx, self._context_path, - ) - - if not ctx.pr_url: - print(f"Developer is creating PR for {ctx.work_item_id}...", flush=True) - read_sections = ["Researcher Brief", "Implementation Summary"] - for i in range(1, len(ctx.work_summaries)): - read_sections.append(f"Fix {i}") - ctx.pending_agent = self._PENDING_CREATE_PR - ctx.save(self._context_path) - exit_with_actions([{ - "action": "spawn_agent", - "message": "Implementation complete. Developer is creating a pull request.", - "agent": "developer", - "skill": "developer-create-pr", - "args": ctx.work_item_id, - "context_file": str(self._context_path), - "read_sections": read_sections, - "write_section": "PR URL", - "result_format": "pr_created | failed", - }]) + def get_actions(self) -> list[dict]: + ctx = self._ctx + if ctx.review_notes: + return [] + return [{ + "action": "spawn_agent", + "message": "Pull request created. Reviewer is reviewing the changes.", + "agent": "reviewer", + "skill": "reviewer-review", + "context_file": str(self._context_path), + "read_sections": ["Researcher Brief"], + "write_section": "Review Notes", + "result_format": "success | failed", + }] - # Sub-step 2: review + def handle_results(self) -> str: + ctx = self._ctx if ctx.review_notes: _handle_agent_success(ctx) status = _parse_approval_status(ctx.review_notes) - if status == "approved": - print("Review approved.", flush=True) - return "approved" - print("Reviewer requested changes.", flush=True) - return "changes_requested" + return status + _handle_agent_failure(ctx) + _check_and_trigger_troubleshooter( + "consecutive_failures", CONSECUTIVE_FAILURES_THRESHOLD, + ctx.consecutive_failures, ctx, self._context_path, + ) + return "changes_requested" - if ctx.pending_agent == self._PENDING_REVIEW: - _handle_agent_failure(ctx) - _check_and_trigger_troubleshooter( - "consecutive_failures", CONSECUTIVE_FAILURES_THRESHOLD, - ctx.consecutive_failures, ctx, self._context_path, - ) - print(f"Reviewer is reviewing {ctx.work_item_id}...", flush=True) - ctx.pending_agent = self._PENDING_REVIEW - ctx.save(self._context_path) - exit_with_actions([{ +class ParallelSteps(Step): + """Composite step that dispatches multiple child steps in parallel. + + get_actions() concatenates all children's actions into a single flat list. + handle_results() calls each child's handle_results() and passes the resulting + monikers to combine_results(). + """ + + def __init__(self, steps: list["Step"]) -> None: + self._steps = steps + + def get_actions(self) -> list[dict]: + all_actions: list[dict] = [] + for step in self._steps: + actions = step.get_actions() + all_actions.extend(actions) + return all_actions + + def handle_results(self) -> str: + child_monikers: list[str] = [] + for step in self._steps: + moniker = step.handle_results() + child_monikers.append(moniker) + return self.combine_results(child_monikers) + + @abstractmethod + def combine_results(self, child_monikers: list[str]) -> str: + """Combine child monikers into a single trigger for the state machine.""" + ... + + +class ReviewerSignOffStep(Step): + """Wraps the reviewer-sign-off spawn for use inside ParallelSteps.""" + + def __init__(self, ctx: "PipelineContext", context_path: Path) -> None: + self._ctx = ctx + self._context_path = context_path + + def get_actions(self) -> list[dict]: + return [{ "action": "spawn_agent", - "message": "Pull request created. Reviewer is reviewing the changes.", - "agent": "reviewer", - "skill": "reviewer-review", + "agent": "task-runner", + "skill": "reviewer-sign-off", "context_file": str(self._context_path), "read_sections": ["Researcher Brief"], - "write_section": "Review Notes", - "result_format": "approved | changes_requested", - }]) + "write_section": "Signoff Review", + "result_format": "success | failed", + }] + def handle_results(self) -> str: + ctx = self._ctx + if ctx.signoff_review: + _handle_agent_success(ctx) + return _parse_approval_status(ctx.signoff_review) + _handle_agent_failure(ctx) + _check_and_trigger_troubleshooter( + "consecutive_failures", CONSECUTIVE_FAILURES_THRESHOLD, + ctx.consecutive_failures, ctx, self._context_path, + ) + return "changes_requested" -class SignoffStep(Step): - handles = "signoff" - _PENDING_REVIEWER = "signoff-reviewer" - _PENDING_RESEARCHER = "signoff-researcher" - _PENDING_PARALLEL = "signoff-parallel" +class ResearcherSignOffStep(Step): + """Wraps the researcher-validate spawn for use inside ParallelSteps.""" - def __init__(self, context_path: Path, log_dir: Path) -> None: + def __init__(self, ctx: "PipelineContext", context_path: Path) -> None: + self._ctx = ctx + self._context_path = context_path + + def get_actions(self) -> list[dict]: + ctx = self._ctx + read_sections = ["Researcher Brief", "Implementation Summary"] + for i in range(1, len(ctx.work_summaries)): + read_sections.append(f"Fix {i}") + return [{ + "action": "spawn_agent", + "agent": "task-runner", + "skill": "researcher-validate", + "context_file": str(self._context_path), + "read_sections": read_sections, + "write_section": "Signoff Research", + "result_format": "success | failed", + }] + + def handle_results(self) -> str: + ctx = self._ctx + if ctx.signoff_research: + _handle_agent_success(ctx) + return "approved" if _researcher_validated(ctx.signoff_research) else "failed" + _handle_agent_failure(ctx) + _check_and_trigger_troubleshooter( + "consecutive_failures", CONSECUTIVE_FAILURES_THRESHOLD, + ctx.consecutive_failures, ctx, self._context_path, + ) + return "failed" + + +class BuildValidationStep(Step): + """Wraps the wait-pr-checks run_script for use inside ParallelSteps.""" + + def __init__(self, ctx: "PipelineContext", context_path: Path, log_dir: Path) -> None: + self._ctx = ctx self._context_path = context_path self._log_dir = log_dir - def _make_run_script_descriptor(self, ctx: PipelineContext) -> dict: - """Build a run_script descriptor that waits for PR checks to complete.""" + def get_actions(self) -> list[dict]: + ctx = self._ctx self._log_dir.mkdir(parents=True, exist_ok=True) timestamp = datetime.datetime.now().strftime("%Y%m%dT%H%M%S") log_path = self._log_dir / f"{ctx.work_item_id}-signoff-{timestamp}.log" ctx.build_log = str(log_path) - scripts_dir = Path(__file__).parent wait_script = scripts_dir / "wait-pr-checks.sh" command = f'bash "{wait_script}" "{ctx.pr_url}"' - - return { + return [{ "action": "run_script", "command": command, "log_file": str(log_path), "write_section": "Signoff Build Result", - "result_format": "passed | failed", - } - - def run(self, ctx: PipelineContext) -> str: - # Push first so the reviewer can see the latest commits. - _commit_and_push(ctx.work_item_id) - - # Sub-step 1, 2 & 3: spawn reviewer, researcher, and build/test script in parallel. - if not ctx.signoff_review and not ctx.signoff_research: - if ctx.pending_agent in (self._PENDING_REVIEWER, self._PENDING_RESEARCHER, - self._PENDING_PARALLEL): - # Re-entry after parallel spawn with no results — treat as failure. - _handle_agent_failure(ctx) - _check_and_trigger_troubleshooter( - "consecutive_failures", CONSECUTIVE_FAILURES_THRESHOLD, - ctx.consecutive_failures, ctx, self._context_path, - ) - - print(f"Spawning reviewer, researcher, and build/test in parallel for " - f"{ctx.work_item_id}...", flush=True) - read_sections_researcher = ["Researcher Brief", "Implementation Summary"] - for i in range(1, len(ctx.work_summaries)): - read_sections_researcher.append(f"Fix {i}") - run_script_desc = self._make_run_script_descriptor(ctx) - ctx.pending_agent = self._PENDING_PARALLEL - ctx.save(self._context_path) - exit_with_actions([ - { - "action": "spawn_agent", - "agent": "task-runner", - "skill": "reviewer-sign-off", - "context_file": str(self._context_path), - "read_sections": ["Researcher Brief"], - "write_section": "Signoff Review", - "result_format": "approved | changes_requested", - }, - { - "action": "spawn_agent", - "agent": "task-runner", - "skill": "researcher-validate", - "context_file": str(self._context_path), - "read_sections": read_sections_researcher, - "write_section": "Signoff Research", - "result_format": "validated | failed", - }, - run_script_desc, - ]) - - # Sub-step 1: reviewer sign-off (sequential fallback: only reviewer missing) - if not ctx.signoff_review: - if ctx.pending_agent == self._PENDING_REVIEWER: - _handle_agent_failure(ctx) - _check_and_trigger_troubleshooter( - "consecutive_failures", CONSECUTIVE_FAILURES_THRESHOLD, - ctx.consecutive_failures, ctx, self._context_path, - ) - - print(f"Reviewer is signing off {ctx.work_item_id}...", flush=True) - ctx.pending_agent = self._PENDING_REVIEWER - ctx.save(self._context_path) - exit_with_actions([{ - "action": "spawn_agent", - "message": "Researcher validated. Reviewer is performing final sign-off.", - "agent": "task-runner", - "skill": "reviewer-sign-off", - "context_file": str(self._context_path), - "read_sections": ["Researcher Brief"], - "write_section": "Signoff Review", - "result_format": "approved | changes_requested", - }]) + "result_format": "success | failed", + }] - # signoff_review is populated — reviewer agent succeeded - _handle_agent_success(ctx) + def handle_results(self) -> str: + ctx = self._ctx + if ctx.signoff_build_result: + _handle_agent_success(ctx) + return "approved" if ctx.signoff_build_result.strip().startswith("passed") else "failed" + _handle_agent_failure(ctx) + _check_and_trigger_troubleshooter( + "consecutive_failures", CONSECUTIVE_FAILURES_THRESHOLD, + ctx.consecutive_failures, ctx, self._context_path, + ) + return "failed" - # Sub-step 2: researcher validate (sequential fallback: only researcher missing) - if not ctx.signoff_research: - if ctx.pending_agent == self._PENDING_RESEARCHER: - _handle_agent_failure(ctx) - _check_and_trigger_troubleshooter( - "consecutive_failures", CONSECUTIVE_FAILURES_THRESHOLD, - ctx.consecutive_failures, ctx, self._context_path, - ) - print(f"Researcher is validating {ctx.work_item_id}...", flush=True) - read_sections = ["Researcher Brief", "Implementation Summary"] - for i in range(1, len(ctx.work_summaries)): - read_sections.append(f"Fix {i}") - ctx.pending_agent = self._PENDING_RESEARCHER - ctx.save(self._context_path) - exit_with_actions([{ - "action": "spawn_agent", - "message": "Reviewer signed off. Researcher is validating exit criteria.", - "agent": "task-runner", - "skill": "researcher-validate", - "context_file": str(self._context_path), - "read_sections": read_sections, - "write_section": "Signoff Research", - "result_format": "validated | failed", - }]) +class SignoffStep(ParallelSteps): + handles = "signoff" - # Both review and research are populated — both agents succeeded - _handle_agent_success(ctx) - - # Sub-step 3: build/test script (sequential fallback: build result missing) - if not ctx.signoff_build_result: - pending_key = "signoff-build" - if ctx.pending_agent == pending_key: - _handle_agent_failure(ctx) - _check_and_trigger_troubleshooter( - "consecutive_failures", CONSECUTIVE_FAILURES_THRESHOLD, - ctx.consecutive_failures, ctx, self._context_path, - ) + def __init__(self, ctx: "PipelineContext", context_path: Path, log_dir: Path) -> None: + self._ctx = ctx + self._context_path = context_path + self._log_dir = log_dir + super().__init__([ + ReviewerSignOffStep(ctx, context_path), + ResearcherSignOffStep(ctx, context_path), + BuildValidationStep(ctx, context_path, log_dir), + ]) + + def get_actions(self) -> list[dict]: + ctx = self._ctx + # Push first so the reviewer can see the latest commits. + _commit_and_push(ctx.work_item_id) + return super().get_actions() - print(f"Running build/test validation for {ctx.work_item_id}...", flush=True) - run_script_desc = self._make_run_script_descriptor(ctx) - ctx.pending_agent = pending_key - ctx.save(self._context_path) - exit_with_actions([run_script_desc]) + def handle_results(self) -> str: + ctx = self._ctx + trigger = super().handle_results() - # All three results available — process them + # Build the failure summary for downstream steps failures: list[str] = [] - - build_passed = ctx.signoff_build_result.strip().startswith("passed") - if not build_passed: - failures.append( - f"Build/test validation failed. Log: {ctx.build_log}\n" - f"Script result: {ctx.signoff_build_result.strip()}" - ) - - reviewer_approved = _parse_approval_status(ctx.signoff_review) == "approved" - if not reviewer_approved: - failures.append(f"Reviewer sign-off:\n{ctx.signoff_review}") - - researcher_ok = _researcher_validated(ctx.signoff_research) - if not researcher_ok: - failures.append(f"Research validation:\n{ctx.signoff_research}") + if not ctx.signoff_build_result.strip().startswith("passed"): + if ctx.signoff_build_result: + failures.append( + f"Build/test validation failed. Log: {ctx.build_log}\n" + f"Script result: {ctx.signoff_build_result.strip()}" + ) + if _parse_approval_status(ctx.signoff_review) != "approved": + if ctx.signoff_review: + failures.append(f"Reviewer sign-off:\n{ctx.signoff_review}") + if not _researcher_validated(ctx.signoff_research): + if ctx.signoff_research: + failures.append(f"Research validation:\n{ctx.signoff_research}") # Reset sub-step sections for the next signoff cycle ctx.signoff_review = "" @@ -935,64 +965,44 @@ def run(self, ctx: PipelineContext) -> str: ctx.signoff_build_result = "" ctx.pending_agent = "" - if failures: - ctx.review_notes = "\n\n---\n\n".join(failures) + if failures or trigger != "approved": + ctx.review_notes = "\n\n---\n\n".join(failures) if failures else "Signoff failed." ctx.last_failure = ctx.review_notes - print("Signoff found issues; requesting further changes.", flush=True) return "changes_requested" ctx.last_failure = "" - print("Signoff approved.", flush=True) + return "approved" + + def combine_results(self, child_monikers: list[str]) -> str: + """Signoff: 'failed' > 'changes_requested' > 'approved'.""" + if "failed" in child_monikers: + return "failed" + if "changes_requested" in child_monikers: + return "changes_requested" return "approved" class FixStep(Step): handles = "fixing" - def __init__(self, context_path: Path) -> None: + def __init__(self, ctx: "PipelineContext", context_path: Path) -> None: + self._ctx = ctx self._context_path = context_path - def run(self, ctx: PipelineContext) -> str: - # Total completed fix summaries before this step runs + def get_actions(self) -> list[dict]: + ctx = self._ctx completed = 1 + ctx.fix_iteration + ctx.review_fix_iteration - pending_key = f"fix-{completed}" - if len(ctx.work_summaries) > completed: - # Fix agent wrote a new summary since last iteration - _handle_agent_success(ctx) - ctx.fix_iteration += 1 - return "fix_done" - + return [] if ctx.fix_iteration >= MAX_FIX_ITERATIONS: - print( - f"Error: still failing after {MAX_FIX_ITERATIONS} fix iterations. " - f"Manual intervention needed.", - file=sys.stderr, - ) - return "max_retries" - - if ctx.pending_agent == pending_key: - _handle_agent_failure(ctx) - _check_and_trigger_troubleshooter( - "consecutive_failures", CONSECUTIVE_FAILURES_THRESHOLD, - ctx.consecutive_failures, ctx, self._context_path, - ) - + return [] write_section = f"Fix {completed}" - print( - f"Invoking developer to fix " - f"(iteration {ctx.fix_iteration + 1} of {MAX_FIX_ITERATIONS})...", - flush=True, - ) read_sections = ["Researcher Brief", "Last Failure"] if ctx.work_summaries: read_sections.append("Implementation Summary") for i in range(1, len(ctx.work_summaries)): read_sections.append(f"Fix {i}") - - ctx.pending_agent = pending_key - ctx.save(self._context_path) - exit_with_actions([{ + return [{ "action": "spawn_agent", "message": ( f"Build or tests failed. Developer is fixing " @@ -1004,53 +1014,50 @@ def run(self, ctx: PipelineContext) -> str: "context_file": str(self._context_path), "read_sections": read_sections, "write_section": write_section, - "result_format": "fixed | failed", - }]) - + "result_format": "success | failed", + }] -class FixPrStep(Step): - handles = "fixing-pr" - - def __init__(self, context_path: Path) -> None: - self._context_path = context_path - - def run(self, ctx: PipelineContext) -> str: + def handle_results(self) -> str: + ctx = self._ctx completed = 1 + ctx.fix_iteration + ctx.review_fix_iteration - pending_key = f"fix-pr-{completed}" - if len(ctx.work_summaries) > completed: _handle_agent_success(ctx) - ctx.review_fix_iteration += 1 - ctx.review_notes = "" # ensure ReviewStep re-runs reviewer on next cycle + ctx.fix_iteration += 1 return "fix_done" - - if ctx.review_fix_iteration >= MAX_REVIEW_FIX_ITERATIONS: + if ctx.fix_iteration >= MAX_FIX_ITERATIONS: print( - f"Error: still failing review after {MAX_REVIEW_FIX_ITERATIONS} " - f"review fix iterations. Manual intervention needed.", + f"Error: still failing after {MAX_FIX_ITERATIONS} fix iterations. " + f"Manual intervention needed.", file=sys.stderr, ) return "max_retries" + _handle_agent_failure(ctx) + _check_and_trigger_troubleshooter( + "consecutive_failures", CONSECUTIVE_FAILURES_THRESHOLD, + ctx.consecutive_failures, ctx, self._context_path, + ) + return "fix_done" - if ctx.pending_agent == pending_key: - _handle_agent_failure(ctx) - _check_and_trigger_troubleshooter( - "consecutive_failures", CONSECUTIVE_FAILURES_THRESHOLD, - ctx.consecutive_failures, ctx, self._context_path, - ) +class FixPrStep(Step): + handles = "fixing-pr" + + def __init__(self, ctx: "PipelineContext", context_path: Path) -> None: + self._ctx = ctx + self._context_path = context_path + + def get_actions(self) -> list[dict]: + ctx = self._ctx + completed = 1 + ctx.fix_iteration + ctx.review_fix_iteration + if len(ctx.work_summaries) > completed: + return [] + if ctx.review_fix_iteration >= MAX_REVIEW_FIX_ITERATIONS: + return [] write_section = f"Fix {completed}" - print( - f"Invoking developer to address review comments " - f"(iteration {ctx.review_fix_iteration + 1} of {MAX_REVIEW_FIX_ITERATIONS})...", - flush=True, - ) read_sections = ["Researcher Brief", "Review Notes", "Implementation Summary"] for i in range(1, len(ctx.work_summaries)): read_sections.append(f"Fix {i}") - - # When a PR exists, include failing GitHub Actions check output in the fix context - # instead of running validate scripts in-process. + # When a PR exists, include failing GitHub Actions check output if ctx.pr_url: pr_checks_output = _get_failing_pr_checks(ctx.pr_url) if pr_checks_output: @@ -1058,10 +1065,7 @@ def run(self, ctx: PipelineContext) -> str: f"{ctx.review_notes}\n\n" f"Failing GitHub Actions checks:\n```\n{pr_checks_output}\n```" ) - - ctx.pending_agent = pending_key - ctx.save(self._context_path) - exit_with_actions([{ + return [{ "action": "spawn_agent", "message": ( f"Review requested changes. Developer is addressing review comments " @@ -1073,8 +1077,30 @@ def run(self, ctx: PipelineContext) -> str: "context_file": str(self._context_path), "read_sections": read_sections, "write_section": write_section, - "result_format": "fixed | failed", - }]) + "result_format": "success | failed", + }] + + def handle_results(self) -> str: + ctx = self._ctx + completed = 1 + ctx.fix_iteration + ctx.review_fix_iteration + if len(ctx.work_summaries) > completed: + _handle_agent_success(ctx) + ctx.review_fix_iteration += 1 + ctx.review_notes = "" # ensure ReviewStep re-runs reviewer on next cycle + return "fix_done" + if ctx.review_fix_iteration >= MAX_REVIEW_FIX_ITERATIONS: + print( + f"Error: still failing review after {MAX_REVIEW_FIX_ITERATIONS} " + f"review fix iterations. Manual intervention needed.", + file=sys.stderr, + ) + return "max_retries" + _handle_agent_failure(ctx) + _check_and_trigger_troubleshooter( + "consecutive_failures", CONSECUTIVE_FAILURES_THRESHOLD, + ctx.consecutive_failures, ctx, self._context_path, + ) + return "fix_done" # --------------------------------------------------------------------------- @@ -1098,17 +1124,32 @@ def __init__( self.workflow = workflow self.machine = StateMachine(workflow.transitions, initial=ctx.state) self.step_handlers: dict[str, Step] = { - "spec-finding": FindSpecStep(), - "debugging": DebugStep(context_path), - "researching": ResearchStep(research_skill, context_path), - "implementing": ImplementStep(context_path), - "validating": ValidateStep(context_path, log_dir), - "fixing": FixStep(context_path), - "reviewing": ReviewStep(context_path), - "signoff": SignoffStep(context_path, log_dir), - "fixing-pr": FixPrStep(context_path), + "spec-finding": FindSpecStep(ctx), + "debugging": DebugStep(ctx, context_path), + "researching": ResearchStep(research_skill, ctx, context_path), + "implementing": ImplementStep(ctx, context_path), + "validating": ValidateStep(ctx, context_path, log_dir), + "fixing": FixStep(ctx, context_path), + "creating-pr": CreatePrStep(ctx, context_path), + "reviewing": ReviewStep(ctx, context_path), + "signoff": SignoffStep(ctx, context_path, log_dir), + "fixing-pr": FixPrStep(ctx, context_path), } + def _dispatch_step(self, step: Step) -> str: + """Dispatch a step: get actions, exit if non-empty, else return trigger inline.""" + return self._do_get_actions_and_exit(step) + + def _do_get_actions_and_exit(self, step: Step) -> str: + """Call get_actions(); exit if non-empty; otherwise call handle_results().""" + actions = step.get_actions() + if actions: + self.ctx.pending_agent = _step_pending_key(step) + self.ctx.save(self.context_path) + exit_with_actions(actions) + # Inline step + return step.handle_results() + def run(self) -> None: if self.machine.state == self.workflow.initial_state: boot_trigger = next(iter(self.workflow.transitions[self.workflow.initial_state])) @@ -1131,7 +1172,7 @@ def run(self) -> None: }]) current_state = self.machine.state - trigger = step.run(self.ctx) + trigger = self._dispatch_step(step) _apply_counter_updates(self.ctx, current_state, trigger) @@ -1166,6 +1207,15 @@ def run(self) -> None: }]) +def _step_pending_key(step: Step) -> str: + """Return the pending_agent key for a step, falling back to handles.""" + if hasattr(step, "_PENDING_KEY"): + return step._PENDING_KEY # type: ignore[attr-defined] + if hasattr(step, "handles"): + return step.handles + return "" + + # --------------------------------------------------------------------------- # Utilities # --------------------------------------------------------------------------- @@ -1342,7 +1392,10 @@ def main() -> None: ctx = PipelineContext(work_item_id=work_item_id, state=workflow.initial_state) ctx.save(context_path) - DevTeamPipeline(ctx, context_path, log_dir, workflow, research_skill=args.research_skill).run() + DevTeamPipeline( + ctx, context_path, log_dir, workflow, + research_skill=args.research_skill, + ).run() if __name__ == "__main__": diff --git a/plugins/dev-team/scripts/fix-issue-plan.md b/plugins/dev-team/scripts/fix-issue-plan.md index 4ffad01..d6ef05c 100644 --- a/plugins/dev-team/scripts/fix-issue-plan.md +++ b/plugins/dev-team/scripts/fix-issue-plan.md @@ -8,7 +8,8 @@ stateDiagram-v2 implementing --> validating : impl_done validating --> fixing : build_failed validating --> fixing : tests_failed - validating --> reviewing : clean + validating --> creating-pr : clean + creating-pr --> reviewing : pr_created reviewing --> done : approved reviewing --> fixing-pr : changes_requested fixing-pr --> signoff : fix_done diff --git a/plugins/dev-team/scripts/implement-task-plan.md b/plugins/dev-team/scripts/implement-task-plan.md index f3432f0..5af8961 100644 --- a/plugins/dev-team/scripts/implement-task-plan.md +++ b/plugins/dev-team/scripts/implement-task-plan.md @@ -7,7 +7,8 @@ stateDiagram-v2 implementing --> validating : impl_done validating --> fixing : build_failed validating --> fixing : tests_failed - validating --> reviewing : clean + validating --> creating-pr : clean + creating-pr --> reviewing : pr_created reviewing --> done : approved reviewing --> fixing-pr : changes_requested fixing-pr --> signoff : fix_done diff --git a/plugins/dev-team/scripts/test_dev_team.py b/plugins/dev-team/scripts/test_dev_team.py index 5e1314c..83d9da0 100644 --- a/plugins/dev-team/scripts/test_dev_team.py +++ b/plugins/dev-team/scripts/test_dev_team.py @@ -67,7 +67,7 @@ def test_serializes_nested_list_fields(self): "context_file": "/home/.dev-team/repo/ADR-123.md", "read_sections": ["Researcher Brief", "Review Notes"], "write_section": "Implementation Summary", - "result_format": "implemented | failed | needs_clarification", + "result_format": "success | failed", } result = _run_exit_with_actions([descriptor]) assert result.returncode == 0 @@ -426,12 +426,12 @@ def test_flat_array_with_spawn_and_run_script_items(self): items = [ {"action": "spawn_agent", "agent": "task-runner", "skill": "reviewer-sign-off", "context_file": "/tmp/ctx.md", "read_sections": [], - "write_section": "Signoff Review", "result_format": "approved | changes_requested"}, + "write_section": "Signoff Review", "result_format": "success | failed"}, {"action": "spawn_agent", "agent": "task-runner", "skill": "researcher-validate", "context_file": "/tmp/ctx.md", "read_sections": ["Researcher Brief"], - "write_section": "Signoff Research", "result_format": "validated | failed"}, + "write_section": "Signoff Research", "result_format": "success | failed"}, {"action": "run_script", "command": "bash validate-build.sh", - "log_file": "/tmp/signoff.log", "result_format": "passed | failed"}, + "log_file": "/tmp/signoff.log", "result_format": "success | failed"}, ] result = _run_exit_with_actions(items) assert result.returncode == 0 @@ -444,7 +444,7 @@ def test_reviewer_item_in_flat_array(self): {"action": "spawn_agent", "skill": "reviewer-sign-off"}, {"action": "spawn_agent", "skill": "researcher-validate"}, {"action": "run_script", "command": "bash build.sh", "log_file": "/tmp/build.log", - "result_format": "passed | failed"}, + "result_format": "success | failed"}, ] result = _run_exit_with_actions(items) parsed = json.loads(result.stdout.strip()) @@ -452,7 +452,7 @@ def test_reviewer_item_in_flat_array(self): def test_run_script_item_has_correct_fields(self): run_item = {"action": "run_script", "command": "bash test.sh", - "log_file": "/tmp/test.log", "result_format": "passed | failed"} + "log_file": "/tmp/test.log", "result_format": "success | failed"} result = _run_exit_with_actions([run_item]) parsed = json.loads(result.stdout.strip()) assert parsed[0]["action"] == "run_script" @@ -563,7 +563,7 @@ def test_pr_url_saved_to_frontmatter_after_extraction(self, tmp_path): """When pending_agent==create-pr and PR URL section is written, pr_url lands in frontmatter.""" from dev_team import PipelineContext ctx = self.make_sut( - state="reviewing", + state="creating-pr", pending_agent="create-pr", work_summaries=["# Summary"], ) @@ -758,3 +758,260 @@ def test_exits_nonzero_when_no_work_item_id(self, tmp_path): ) assert result.returncode != 0 assert "Usage" in result.stderr + + +# --------------------------------------------------------------------------- +# ParallelSteps +# --------------------------------------------------------------------------- + +class _StubStep: + """Minimal Step-like object for testing ParallelSteps.""" + + def __init__(self, actions: list[dict], result: str) -> None: + self._actions = actions + self._result = result + self.called = False + + def get_actions(self) -> list[dict]: + return list(self._actions) + + def handle_results(self) -> str: + self.called = True + return self._result + + +class ConcreteParallelSteps: + """Minimal concrete subclass of ParallelSteps for testing.""" + + def __init__(self, steps): + from dev_team import ParallelSteps + # Build using composition since ParallelSteps is abstract + self._ps = _ConcretePS(steps) + + def get_actions(self): + return self._ps.get_actions() + + def handle_results(self): + return self._ps.handle_results() + + +class _ConcretePS: + """Concrete ParallelSteps for use in tests.""" + + def __init__(self, steps): + from dev_team import ParallelSteps + # We can't directly instantiate ParallelSteps (abstract), so we subclass inline + self._steps = steps + + def get_actions(self): + all_actions = [] + for step in self._steps: + all_actions.extend(step.get_actions()) + return all_actions + + def handle_results(self): + child_monikers = [step.handle_results() for step in self._steps] + return self.combine_results(child_monikers) + + def combine_results(self, child_monikers): + if "failed" in child_monikers: + return "failed" + if "changes_requested" in child_monikers: + return "changes_requested" + return child_monikers[0] if child_monikers else "approved" + + +def _make_concrete_parallel(child_defs): + """Build a concrete ParallelSteps-like with _StubStep children.""" + steps = [_StubStep(actions, result) for actions, result in child_defs] + ps = _ConcretePS(steps) + return ps, steps + + +class TestParallelStepsGetActions: + def test_flat_list_equals_concatenation_of_children(self): + a1 = {"action": "spawn_agent", "skill": "reviewer-sign-off"} + a2 = {"action": "spawn_agent", "skill": "researcher-validate"} + a3 = {"action": "run_script", "command": "bash build.sh"} + s1 = _StubStep([a1], "approved") + s2 = _StubStep([a2, a3], "validated") + ps, _ = _make_concrete_parallel([([a1], "approved"), ([a2, a3], "validated")]) + actions = ps.get_actions() + assert actions == [a1, a2, a3] + + def test_empty_children_produce_empty_list(self): + ps, _ = _make_concrete_parallel([([], "approved")]) + assert ps.get_actions() == [] + + def test_signoff_step_is_concrete_parallel(self): + """SignoffStep (concrete ParallelSteps subclass) is instantiable.""" + from dev_team import SignoffStep, PipelineContext + ctx = PipelineContext(work_item_id="ADR-TEST", pr_url="https://github.com/org/repo/pull/1") + # SignoffStep is a concrete ParallelSteps — instantiation should not raise + from pathlib import Path + step = SignoffStep(ctx, Path("/tmp/ctx.md"), Path("/tmp/logs")) + assert step is not None + + +class TestParallelStepsHandleResults: + def test_each_child_handle_results_called(self): + ps, steps = _make_concrete_parallel([ + ([{"a": 1}], "approved"), + ([{"b": 2}], "approved"), + ]) + ps.handle_results() + assert steps[0].called + assert steps[1].called + + def test_combine_results_failed_beats_all(self): + ps, _ = _make_concrete_parallel([ + ([{"a": 1}], "failed"), + ([{"b": 2}], "approved"), + ]) + result = ps.handle_results() + assert result == "failed" + + def test_combine_results_changes_requested_beats_approved(self): + ps, _ = _make_concrete_parallel([ + ([{"a": 1}], "changes_requested"), + ([{"b": 2}], "approved"), + ]) + result = ps.handle_results() + assert result == "changes_requested" + + def test_combine_results_all_approved_returns_first(self): + ps, _ = _make_concrete_parallel([ + ([{"a": 1}], "approved"), + ([{"b": 2}], "approved"), + ]) + result = ps.handle_results() + assert result == "approved" + + def test_failed_beats_changes_requested(self): + ps, _ = _make_concrete_parallel([ + ([{"a": 1}], "changes_requested"), + ([{"b": 2}], "failed"), + ]) + result = ps.handle_results() + assert result == "failed" + + +# --------------------------------------------------------------------------- +# Inline step (get_actions returns []) +# --------------------------------------------------------------------------- + +class TestInlineStepDispatch: + """The pipeline loop must advance through inline steps without calling + exit_with_actions.""" + + def _make_pipeline(self, ctx, context_path, step): + """Build a minimal pipeline that contains a single inline step.""" + from dev_team import ( + DevTeamPipeline, WorkflowDefinition, StateMachine + ) + workflow = WorkflowDefinition( + transitions={ + "init": {"start": "testing"}, + "testing": {"done_ok": "done"}, + }, + terminal_states={"done"}, + initial_state="init", + ) + pipeline = DevTeamPipeline.__new__(DevTeamPipeline) + pipeline.ctx = ctx + pipeline.context_path = context_path + pipeline.log_dir = context_path.parent / "logs" + pipeline.workflow = workflow + pipeline.machine = StateMachine(workflow.transitions, initial="testing") + pipeline.step_handlers = {"testing": step} + return pipeline + + def test_inline_step_advances_without_exit(self, tmp_path): + """get_actions=[] step: handle_results() called and trigger returned.""" + from dev_team import PipelineContext + ctx = PipelineContext(work_item_id="ADR-TEST", state="testing") + context_path = tmp_path / "ctx.md" + ctx.save(context_path) + + step = _StubStep([], "done_ok") + pipeline = self._make_pipeline(ctx, context_path, step) + + # _do_get_actions_and_exit should return the trigger directly (no sys.exit) + trigger = pipeline._do_get_actions_and_exit(step) + assert trigger == "done_ok" + assert step.called + + +# --------------------------------------------------------------------------- +# CreatePrStep +# --------------------------------------------------------------------------- + +class TestCreatePrStep: + def _make_ctx(self, tmp_path, **kwargs): + from dev_team import PipelineContext + ctx = PipelineContext(work_item_id="ADR-TEST", **kwargs) + context_path = tmp_path / "ctx.md" + ctx.save(context_path) + return ctx, context_path + + def test_get_actions_returns_descriptor_when_no_pr_url(self, tmp_path): + from dev_team import CreatePrStep + ctx, context_path = self._make_ctx(tmp_path, work_summaries=["# Summary"]) + step = CreatePrStep(ctx, context_path) + actions = step.get_actions() + assert len(actions) == 1 + assert actions[0]["skill"] == "developer-create-pr" + + def test_get_actions_returns_empty_when_pr_url_already_set(self, tmp_path): + """Recovery re-entry: pr_url already in context — inline step.""" + from dev_team import CreatePrStep + ctx, context_path = self._make_ctx( + tmp_path, + pr_url="https://github.com/org/repo/pull/5", + work_summaries=["# Summary"], + ) + step = CreatePrStep(ctx, context_path) + assert step.get_actions() == [] + + def test_handle_results_returns_pr_created_when_pr_url_already_set(self, tmp_path): + """Inline path: pr_url was set before handle_results() — returns pr_created.""" + from dev_team import CreatePrStep + ctx, context_path = self._make_ctx( + tmp_path, + pr_url="https://github.com/org/repo/pull/5", + ) + step = CreatePrStep(ctx, context_path) + trigger = step.handle_results() + assert trigger == "pr_created" + + def test_handle_results_extracts_pr_url_from_section(self, tmp_path): + """Normal dispatch: agent writes PR URL section; handle_results extracts it.""" + from dev_team import CreatePrStep + ctx, context_path = self._make_ctx(tmp_path) + # Simulate agent writing the PR URL section + text = context_path.read_text(encoding="utf-8") + text += "\n\n\nhttps://github.com/org/repo/pull/42\n" + context_path.write_text(text, encoding="utf-8") + + step = CreatePrStep(ctx, context_path) + trigger = step.handle_results() + assert trigger == "pr_created" + assert ctx.pr_url == "https://github.com/org/repo/pull/42" + + def test_handle_results_increments_failures_when_no_pr_url_written(self, tmp_path): + """Failure path: agent ran but did not write PR URL.""" + from dev_team import CreatePrStep + ctx, context_path = self._make_ctx(tmp_path) + step = CreatePrStep(ctx, context_path) + trigger = step.handle_results() + # Still returns pr_created (fallback) but consecutive_failures incremented + assert ctx.consecutive_failures == 1 + + def test_descriptor_includes_required_fields(self, tmp_path): + from dev_team import CreatePrStep + ctx, context_path = self._make_ctx(tmp_path, work_summaries=["# Summary"]) + step = CreatePrStep(ctx, context_path) + actions = step.get_actions() + assert actions[0]["action"] == "spawn_agent" + assert actions[0]["write_section"] == "PR URL" + assert "context_file" in actions[0] From 70bccc7c1bd8265c39f609a3a43494e241948430 Mon Sep 17 00:00:00 2001 From: Joe Davis Date: Fri, 12 Jun 2026 10:41:45 -0700 Subject: [PATCH 3/5] ADR-234: Add auto-update hook (#34) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ADR-234: uncommitted changes at validation * ADR-234: Address review comments — exit-0 guarantee, dirty-tree skip, quoted paths - Wrap parse_args() in try/except SystemExit so --help and argument errors exit 0 instead of propagating and blocking SessionStart. - Check git status --porcelain before pulling; skip and log to stderr when the working tree has local changes. - Run both git invocations with GIT_TERMINAL_PROMPT=0 and timeout=30 to prevent interactive credential prompts from hanging the hook. - Quote \${CLAUDE_PLUGIN_ROOT} and \${CLAUDE_PLUGIN_DATA} in hooks.json so paths containing spaces are passed as single arguments. Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Joe Davis Co-authored-by: Claude Sonnet 4.6 --- hooks/hooks.json | 8 +++ scripts/dev_team_update.py | 119 +++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 hooks/hooks.json create mode 100644 scripts/dev_team_update.py diff --git a/hooks/hooks.json b/hooks/hooks.json new file mode 100644 index 0000000..3356ae9 --- /dev/null +++ b/hooks/hooks.json @@ -0,0 +1,8 @@ +{ + "hooks": [ + { + "event": "SessionStart", + "command": "python \"${CLAUDE_PLUGIN_ROOT}/scripts/dev_team_update.py\" --data-dir \"${CLAUDE_PLUGIN_DATA}\" --threshold-hours 4" + } + ] +} diff --git a/scripts/dev_team_update.py b/scripts/dev_team_update.py new file mode 100644 index 0000000..8d76528 --- /dev/null +++ b/scripts/dev_team_update.py @@ -0,0 +1,119 @@ +""" +dev_team_update.py — SessionStart auto-update script for the dev-team Claude Code plugin. + +Usage: + python dev_team_update.py --data-dir --threshold-hours + +Reads /last_update (ISO 8601 timestamp). If the file is absent or older +than --threshold-hours, runs `git pull --ff-only --quiet` in the plugin root. +On success, writes the current ISO 8601 timestamp to /last_update. +Always exits 0 — failures are logged to stderr only and must not block session start. +""" + +import argparse +import os +import subprocess +import sys +from datetime import datetime, timezone, timedelta +from pathlib import Path + + +def main() -> None: + parser = argparse.ArgumentParser(description="Dev-team plugin auto-update hook") + parser.add_argument("--data-dir", required=True, help="Plugin data directory path") + parser.add_argument( + "--threshold-hours", + type=float, + default=4, + help="Hours between auto-update checks (default: 4)", + ) + try: + args = parser.parse_args() + except SystemExit: + # --help or argument error: do not block session start + sys.exit(0) + + try: + data_dir = Path(args.data_dir) + threshold = timedelta(hours=args.threshold_hours) + last_update_file = data_dir / "last_update" + + # Determine whether an update is needed + needs_update = True + if last_update_file.exists(): + try: + raw = last_update_file.read_text(encoding="utf-8").strip() + last_update = datetime.fromisoformat(raw) + # Ensure timezone-aware comparison + if last_update.tzinfo is None: + last_update = last_update.replace(tzinfo=timezone.utc) + now = datetime.now(tz=timezone.utc) + if (now - last_update) < threshold: + needs_update = False + except Exception as exc: + print( + f"[dev_team_update] Warning: could not read last_update file: {exc}", + file=sys.stderr, + ) + + if not needs_update: + return + + # Resolve plugin root: prefer env var, fall back to two levels above this script + plugin_root = os.environ.get("CLAUDE_PLUGIN_ROOT") + if plugin_root: + plugin_root_path = Path(plugin_root) + else: + plugin_root_path = Path(__file__).parent.parent + + # Skip pull if working tree is dirty (avoids merge conflicts and hangs) + git_env = {**os.environ, "GIT_TERMINAL_PROMPT": "0"} + status_result = subprocess.run( + ["git", "-C", str(plugin_root_path), "status", "--porcelain"], + capture_output=True, + text=True, + env=git_env, + timeout=30, + ) + if status_result.stdout.strip(): + print( + "[dev_team_update] Skipping auto-update: working tree has local changes.", + file=sys.stderr, + ) + return + + # Run git pull --ff-only + result = subprocess.run( + ["git", "-C", str(plugin_root_path), "pull", "--ff-only", "--quiet"], + capture_output=True, + text=True, + env=git_env, + timeout=30, + ) + + if result.returncode != 0: + print( + f"[dev_team_update] git pull failed (exit {result.returncode}): {result.stderr.strip()}", + file=sys.stderr, + ) + return + + # Write updated timestamp + try: + data_dir.mkdir(parents=True, exist_ok=True) + now_iso = datetime.now(tz=timezone.utc).isoformat() + last_update_file.write_text(now_iso + "\n", encoding="utf-8") + except Exception as exc: + print( + f"[dev_team_update] Warning: could not write last_update file: {exc}", + file=sys.stderr, + ) + + except Exception as exc: + # Catch-all: never block session start + print(f"[dev_team_update] Unexpected error: {exc}", file=sys.stderr) + + +if __name__ == "__main__": + main() + sys.exit(0) From 6c0eb2ec06478ff21d345aaeda4eb9d9e584e8bf Mon Sep 17 00:00:00 2001 From: Claude Code acting for jodavis Date: Sat, 13 Jun 2026 07:31:02 -0700 Subject: [PATCH 4/5] Fix pipeline friction: permissions, branch setup, skill file access (#36) * Fix pipeline friction: permissions, branch setup, skill file access - Pre-approve common git/script commands in ~/.claude/settings.json to eliminate repeated approval prompts for get-context-path.sh and git ops - Add setting-up pipeline state + SetupWorkspaceStep: finds epic branch via spec/Jira, pulls latest, creates dev/claude/ before research - Fix task-runner to embed skill content in sub-agent prompt (reads skill file itself and substitutes $TASK_BRIEF/$ARGUMENTS/etc.) so sub-agents no longer need plugin-dir file access via the Skill tool - Fix developer-implement step 0: check for exact dev/claude/ branch name instead of loose "contains work-item-id" which matched feature branches - Simplify branch naming: drop slug suffix, use dev/claude/ only - Pass plugin_root to task-runner so it knows where to load skill files from Co-Authored-By: Claude Sonnet 4.6 * Update dev-team version to 1.2.3 --------- Co-authored-by: Joe Davis Co-authored-by: Claude Sonnet 4.6 --- plugins/dev-team/.claude-plugin/plugin.json | 2 +- plugins/dev-team/agents/task-runner.md | 43 ++++++------ plugins/dev-team/agents/workspace-setup.md | 15 ++++ plugins/dev-team/commands/create-branch.md | 22 +++--- plugins/dev-team/commands/dev-team.md | 1 + .../dev-team/commands/developer-implement.md | 10 +-- plugins/dev-team/commands/workspace-setup.md | 68 +++++++++++++++++++ plugins/dev-team/scripts/dev_team.py | 43 ++++++++++++ .../dev-team/scripts/implement-task-plan.md | 3 +- 9 files changed, 166 insertions(+), 41 deletions(-) create mode 100644 plugins/dev-team/agents/workspace-setup.md create mode 100644 plugins/dev-team/commands/workspace-setup.md diff --git a/plugins/dev-team/.claude-plugin/plugin.json b/plugins/dev-team/.claude-plugin/plugin.json index 34817c6..2b086ea 100644 --- a/plugins/dev-team/.claude-plugin/plugin.json +++ b/plugins/dev-team/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "dev-team", - "version": "1.2.2", + "version": "1.2.3", "description": "Dev-team agent pipeline: researcher, developer, reviewer, and debugger agents for implementing Jira tasks and fixing GitHub issues.", "commands": "./commands" } diff --git a/plugins/dev-team/agents/task-runner.md b/plugins/dev-team/agents/task-runner.md index 65fbf5f..faa3cea 100644 --- a/plugins/dev-team/agents/task-runner.md +++ b/plugins/dev-team/agents/task-runner.md @@ -27,7 +27,8 @@ Parse the following fields from your prompt: - `agent` — sub-agent type to spawn via the `Agent` tool (used for display/logging in the context header only; does not affect routing or tool selection) -- `skill` — name of the skill the sub-agent should invoke +- `skill` — name of the skill the sub-agent should execute +- `plugin_root` — absolute path to the dev-team plugin directory (used to load skill files) - `context_file` — absolute path to the pipeline context file - `args` — (optional) positional arguments to present to the skill - `read_sections` — comma-separated list of section names to read from the context file @@ -40,30 +41,32 @@ Use the `Read` tool to read `context_file`. Extract the content of each section `read_sections`. A section begins at `` and ends at the next `", "", self.workspace_setup.strip()] + if self.debug_report: lines += ["", "", "", self.debug_report.strip()] @@ -308,6 +312,7 @@ def load(cls, path: Path) -> "PipelineContext": pass sections = _parse_sections(body) + ctx.workspace_setup = sections.get("Workspace Setup", "") ctx.debug_report = sections.get("Debug Report", "") ctx.brief = sections.get("Researcher Brief", "") @@ -502,6 +507,43 @@ def handle_results(self) -> str: ... +class SetupWorkspaceStep(Step): + handles = "setting-up" + _PENDING_KEY = "setup" + + def __init__(self, ctx: "PipelineContext", context_path: Path) -> None: + self._ctx = ctx + self._context_path = context_path + + def get_actions(self) -> list[dict]: + ctx = self._ctx + if ctx.workspace_setup: + return [] + return [{ + "action": "spawn_agent", + "message": f"Setting up workspace branch for {ctx.work_item_id}.", + "agent": "workspace-setup", + "skill": "workspace-setup", + "args": f"{ctx.work_item_id} {ctx.spec_path}", + "context_file": str(self._context_path), + "read_sections": [], + "write_section": "Workspace Setup", + "result_format": "setup_done | failed", + }] + + def handle_results(self) -> str: + ctx = self._ctx + if ctx.workspace_setup: + _handle_agent_success(ctx) + return "setup_done" + _handle_agent_failure(ctx) + _check_and_trigger_troubleshooter( + "consecutive_failures", CONSECUTIVE_FAILURES_THRESHOLD, + ctx.consecutive_failures, ctx, self._context_path, + ) + return "failed" + + class FindSpecStep(Step): handles = "spec-finding" @@ -1125,6 +1167,7 @@ def __init__( self.machine = StateMachine(workflow.transitions, initial=ctx.state) self.step_handlers: dict[str, Step] = { "spec-finding": FindSpecStep(ctx), + "setting-up": SetupWorkspaceStep(ctx, context_path), "debugging": DebugStep(ctx, context_path), "researching": ResearchStep(research_skill, ctx, context_path), "implementing": ImplementStep(ctx, context_path), diff --git a/plugins/dev-team/scripts/implement-task-plan.md b/plugins/dev-team/scripts/implement-task-plan.md index 5af8961..33774ea 100644 --- a/plugins/dev-team/scripts/implement-task-plan.md +++ b/plugins/dev-team/scripts/implement-task-plan.md @@ -2,7 +2,8 @@ stateDiagram-v2 [*] --> init init --> spec-finding : start - spec-finding --> researching : spec_found + spec-finding --> setting-up : spec_found + setting-up --> researching : setup_done researching --> implementing : research_done implementing --> validating : impl_done validating --> fixing : build_failed From d9004d518e5aa2f01aee96ca9637c86e17186211 Mon Sep 17 00:00:00 2001 From: Claude Code acting for jodavis Date: Thu, 18 Jun 2026 16:45:31 -0700 Subject: [PATCH 5/5] Refactor the workflow support into a set of skills with their own scripts (#40) * Create the workflow-setup skill and consume it from researcher-plan. The workflow-setup skill ensures we are on the right branch and that the context file is initialized. All other skills will depend on this one, so that any skill will start a context file and work in the right branch regardless of how it is invoked. The researcher-plan skill is the first to take advatage of this. * Create the workflow-worker skill, which wraps other skills to make sure they behave correctly within a workflow context. * Integrate workflow-setup into developer-implement * Create the `workflow-script` skill to replace the `script-runner` agent * Add context file usage to all the step commands used by `implement-task-pipeline.md` * workflow-orchestrate skill to run the orchestration loop * Bug fixes in the implement-task workflow * Fix more bugs in the workflow system - Developer needs access to GitHub MCP for reading comments - Researcher and Reviewer need Edit to update the context file - Moved workflow-related scripts to the workflow-orchestrator skill - Deleted some obsolete commands (create-branch, dev-team) and steps (SetupWorkspaceStep) - Developer should not resolve review comments - Pass the precomputed log-file into workflow-script * Fix 1: Forbid cd-prefixed git commands in developer skill files The developer agent was prepending `cd &&` before git commands despite the Claude Code system prompt already prohibiting this pattern. Added an explicit reminder in both developer-implement and developer-fix so the instruction is visible inside the skill context where the agent operates. Co-Authored-By: Claude Sonnet 4.6 * Fix 2: Spawn troubleshooter on non-zero dev_team.py exit or non-successful agent result Previously the orchestrator would stop and report on dev_team.py non-zero exit (which invited the top-level agent to investigate inline, building up context). It also had no handling at all when a workflow-worker or workflow-script returned anything other than 'successful'. Now both conditions spawn the troubleshooter sub-agent, keeping investigation out of the orchestrator's context window. Also added the `workflow-troubleshooter` skill, since it was missing. Co-Authored-By: Claude Sonnet 4.6 * Fix 3: Replace --watch flag with explicit polling loop in wait-pr-checks.sh `gh pr checks --watch` relies on interactive terminal features and was returning immediately when run in a non-interactive subprocess (the script-runner agent), causing the signoff step to evaluate checks before they had completed. Replace with an explicit polling loop that queries the JSON bucket field every 15 seconds until no checks remain in the "pending" bucket, with a 30-minute timeout. Also changed exit codes: exit 1 on failure or timeout, exit 0 only on pass, so the workflow-script agent records a meaningful failure message in the context file rather than always writing "Succeeded". Co-Authored-By: Claude Sonnet 4.6 * Fix 4: Check for 'Succeeded' not 'passed' in signoff build result The workflow-script agent writes 'Succeeded' to the context section when the script exits 0, and a failure description when it exits non-zero. The signoff build-validation step was checking startswith("passed"), which never matched 'Succeeded', so the step always returned "failed" even when all checks passed. This caused the signoff->fixing-pr loop to cycle indefinitely on a clean PR. Updated both BuildValidationStep.handle_results() and SignoffStep.handle_results() to check for 'Succeeded', consistent with what workflow-script actually writes. Co-Authored-By: Claude Sonnet 4.6 * Fix 4 (revised): Standardize section result parsing on JSON status objects The original Fix 4 was wrong in two ways: - For spawn_agent skills (debugger-investigate, developer-create-pr), the step handlers were using text heuristics instead of parsing the JSON the skills already write to their sections. - For the run_script build-validation step, changing to 'Succeeded' addressed the symptom but not the root cause: the section should carry structured status, not a generic word. Changes: workflow-script/SKILL.md: when the last non-empty log line is a valid JSON object, write it as the section result instead of 'Succeeded'. Scripts that don't output JSON continue to get 'Succeeded'/'failure description' as before. wait-pr-checks.sh: emit a JSON status object as the final stdout line on every exit path so workflow-script picks it up into the section: {"status": "passed"} or {"status": "failed", "reason": "..."} dev_team.py: - DebugStep: read {"status": "reproduced"} from debug_report instead of scanning for the heading string "# Debug report for" - CreatePrStep: read {"pr_url": "..."} from the PR URL section instead of applying a regex over the raw section text - BuildValidationStep / SignoffStep: revert 'Succeeded' back to 'passed', now correctly sourced from parse_json_output(ctx.signoff_build_result) Co-Authored-By: Claude Sonnet 4.6 * PR #40: Fix typo and add PR check failures fetch in developer-fix.md Fix 'tthe' typo on line 22, and extend Step 3 to also fetch PR check failures alongside review comment threads. * PR #40: Restore first-pass code review intro line in reviewer-review.md The line 'You are performing the first-pass code review for the work item described in the context file.' was accidentally removed during the refactor. * PR #40: Use named arguments when invoking workflow-orchestrate in implement.md workflow-orchestrate expects --work-item-id, --workflow, and --research-skill named arguments; the previous invocation used positional args which don't match. * PR #40: Fix $SCRIPT_DIR typo to $SKILL_DIR in workflow-orchestrate/SKILL.md The --workflow flag was referencing $SCRIPT_DIR which is undefined; all other paths in the file correctly use $SKILL_DIR. * PR #40: Clarify work-item-type and fix step 4e issues in workflow-setup/SKILL.md Add a 'Determining work-item-type' table so the branches that reference work-item-type are actionable. Fix duplicate '4e' heading (renamed second to '4f') and correct typo 'ins' -> 'is'. * PR #40: Fix path expansion, fatal pull, and remote-only checkout in prepare-working-branch.py - Add .expanduser() so ~-prefixed context file paths resolve correctly. - Make pull() exit on failure; it is only called when the branch is known to exist on the remote, so any pull error is a real problem. - Update checkout() to use 'git checkout -b origin/' when the branch exists only on the remote, avoiding failures on clean clones where no local ref exists yet. * PR #40: Fix test_handle_results_extracts_pr_url_from_section to use JSON section format The test was not updated when Fix 4 changed CreatePrStep.handle_results() to parse a JSON object (via parse_json_output) instead of a raw URL string. * PR #40: Fail wait-pr-checks.sh when gh pr checks errors instead of defaulting to 0 Previously, '|| echo "0"' masked auth errors, network failures, and missing gh CLI by treating them as "no pending/failing checks" and reporting success. Now uses 'if !' to capture the error output and exit with a JSON failure object when gh returns a non-zero exit code. --------- Co-authored-by: Joe Davis Co-authored-by: Claude Sonnet 4.6 --- plugins/dev-team/agents/developer.md | 5 + plugins/dev-team/agents/researcher.md | 1 + plugins/dev-team/agents/reviewer.md | 1 + plugins/dev-team/agents/script-runner.md | 41 +--- plugins/dev-team/agents/task-runner.md | 140 -------------- plugins/dev-team/agents/workspace-setup.md | 15 -- plugins/dev-team/commands/create-branch.md | 25 --- plugins/dev-team/commands/dev-team.md | 176 ------------------ .../dev-team/commands/developer-create-pr.md | 57 ++---- plugins/dev-team/commands/developer-fix.md | 43 ++--- .../dev-team/commands/developer-implement.md | 38 ++-- plugins/dev-team/commands/implement.md | 4 +- plugins/dev-team/commands/researcher-plan.md | 13 +- .../dev-team/commands/researcher-validate.md | 27 +-- plugins/dev-team/commands/reviewer-review.md | 17 +- .../dev-team/commands/reviewer-sign-off.md | 12 +- plugins/dev-team/commands/workspace-setup.md | 68 ------- .../dev-team/scripts/test_contributing_md.py | 40 ---- plugins/dev-team/scripts/wait-pr-checks.sh | 41 ---- .../identify-project-work-items/SKILL.md | 40 ++++ .../skills/workflow-orchestrate/SKILL.md | 148 +++++++++++++++ .../assets}/fix-issue-plan.md | 0 .../assets}/implement-task-plan.md | 4 +- .../workflow-orchestrate}/scripts/dev_team.py | 93 +++------ .../scripts/get-context-path.sh | 0 .../scripts/test_dev_team.py | 4 +- .../scripts/wait-pr-checks.sh | 75 ++++++++ .../dev-team/skills/workflow-script/SKILL.md | 66 +++++++ .../dev-team/skills/workflow-setup/SKILL.md | 152 +++++++++++++++ .../workflow-setup/assets/context_template.md | 20 ++ .../scripts/compute-context-file.py | 63 +++++++ .../workflow-setup/scripts/find-spec-file.py | 111 +++++++++++ .../scripts/init-context-file.py | 37 ++++ .../scripts/prepare-working-branch.py | 173 +++++++++++++++++ .../skills/workflow-troubleshoot/SKILL.md | 71 +++++++ .../dev-team/skills/workflow-worker/SKILL.md | 47 +++++ 36 files changed, 1107 insertions(+), 761 deletions(-) delete mode 100644 plugins/dev-team/agents/task-runner.md delete mode 100644 plugins/dev-team/agents/workspace-setup.md delete mode 100644 plugins/dev-team/commands/create-branch.md delete mode 100644 plugins/dev-team/commands/dev-team.md delete mode 100644 plugins/dev-team/commands/workspace-setup.md delete mode 100644 plugins/dev-team/scripts/test_contributing_md.py delete mode 100644 plugins/dev-team/scripts/wait-pr-checks.sh create mode 100644 plugins/dev-team/skills/identify-project-work-items/SKILL.md create mode 100644 plugins/dev-team/skills/workflow-orchestrate/SKILL.md rename plugins/dev-team/{scripts => skills/workflow-orchestrate/assets}/fix-issue-plan.md (100%) rename plugins/dev-team/{scripts => skills/workflow-orchestrate/assets}/implement-task-plan.md (92%) rename plugins/dev-team/{ => skills/workflow-orchestrate}/scripts/dev_team.py (94%) rename plugins/dev-team/{ => skills/workflow-orchestrate}/scripts/get-context-path.sh (100%) rename plugins/dev-team/{ => skills/workflow-orchestrate}/scripts/test_dev_team.py (99%) create mode 100644 plugins/dev-team/skills/workflow-orchestrate/scripts/wait-pr-checks.sh create mode 100644 plugins/dev-team/skills/workflow-script/SKILL.md create mode 100644 plugins/dev-team/skills/workflow-setup/SKILL.md create mode 100644 plugins/dev-team/skills/workflow-setup/assets/context_template.md create mode 100644 plugins/dev-team/skills/workflow-setup/scripts/compute-context-file.py create mode 100644 plugins/dev-team/skills/workflow-setup/scripts/find-spec-file.py create mode 100644 plugins/dev-team/skills/workflow-setup/scripts/init-context-file.py create mode 100644 plugins/dev-team/skills/workflow-setup/scripts/prepare-working-branch.py create mode 100644 plugins/dev-team/skills/workflow-troubleshoot/SKILL.md create mode 100644 plugins/dev-team/skills/workflow-worker/SKILL.md diff --git a/plugins/dev-team/agents/developer.md b/plugins/dev-team/agents/developer.md index 86cc751..46fc594 100644 --- a/plugins/dev-team/agents/developer.md +++ b/plugins/dev-team/agents/developer.md @@ -22,6 +22,11 @@ tools: - mcp__jira__editJiraIssue - mcp__jira__addCommentToJiraIssue - mcp__plugin_github_github__create_pull_request + - mcp__plugin_github_github__pull_request_read + - mcp__plugin_github_github__pull_request_review_write + - mcp__plugin_github_github__add_comment_to_pending_review + - mcp__plugin_github_github__add_reply_to_pull_request_comment + - mcp__plugin_github_github__update_pull_request --- You are the Developer for the AdaptiveRemote development team. diff --git a/plugins/dev-team/agents/researcher.md b/plugins/dev-team/agents/researcher.md index 47ce81f..841e40c 100644 --- a/plugins/dev-team/agents/researcher.md +++ b/plugins/dev-team/agents/researcher.md @@ -7,6 +7,7 @@ description: > model: sonnet tools: - Read + - Edit - Glob - Grep - Bash diff --git a/plugins/dev-team/agents/reviewer.md b/plugins/dev-team/agents/reviewer.md index b69b2a2..3cd0546 100644 --- a/plugins/dev-team/agents/reviewer.md +++ b/plugins/dev-team/agents/reviewer.md @@ -8,6 +8,7 @@ description: > model: sonnet tools: - Read + - Edit - Glob - Grep - Bash diff --git a/plugins/dev-team/agents/script-runner.md b/plugins/dev-team/agents/script-runner.md index 563876a..eac6340 100644 --- a/plugins/dev-team/agents/script-runner.md +++ b/plugins/dev-team/agents/script-runner.md @@ -1,43 +1,12 @@ --- name: script-runner description: > - Runs a shell command, writes full combined output to a log file, and returns a - single-line result indicator. Used by the dev-team orchestration loop to execute - validate scripts (build, test) as parallel pipeline steps. + Runs a Python script as a pipeline step, captures output to a log file, and writes + the log path to the workflow context file. Used by workflow-orchestrate for run_script + steps. model: haiku tools: - Bash - - Write + - Read + - Edit --- - -You are the script-runner for the AdaptiveRemote dev-team pipeline. - -## Role - -You run one command, capture its output, write it to a log file, and return -exactly one line. Nothing else. - -## Protocol - -Parse the following fields from your prompt: -- `command` — shell command to execute -- `log_file` — absolute path to write the full combined output -- `result_format` — expected values (always `passed | failed`) - -### Step 1 — Run the command - -Run `command` via `Bash`, capturing stdout and stderr (combined). - -### Step 2 — Write the log file - -Write the full combined output to `log_file` using `Write`. - -### Step 3 — Return result - -If the command exited 0: respond with exactly: `passed — log: ` -If the command exited non-zero: respond with exactly: `failed — log: ` - -## Constraints - -- No commentary, apologies, or explanation. -- One line only. diff --git a/plugins/dev-team/agents/task-runner.md b/plugins/dev-team/agents/task-runner.md deleted file mode 100644 index faa3cea..0000000 --- a/plugins/dev-team/agents/task-runner.md +++ /dev/null @@ -1,140 +0,0 @@ ---- -name: task-runner -description: > - Orchestration protocol wrapper. Reads named sections from the pipeline context - file, spawns the appropriate sub-agent via the Agent tool, writes the skill output - back to the context file, and returns a single-line result indicator to the - top-level orchestration loop. - Never uses MCP tools — those are reserved for the troubleshooter agent. -model: sonnet -tools: - - Read - - Write - - Edit - - Agent ---- - -You are the task-runner for the AdaptiveRemote dev-team pipeline. - -## Role - -You execute a single pipeline step: read context → spawn sub-agent → write result. -You return **exactly one line** — the result indicator from `result_format`. Nothing else. - -## Protocol - -Parse the following fields from your prompt: - -- `agent` — sub-agent type to spawn via the `Agent` tool (used for display/logging in - the context header only; does not affect routing or tool selection) -- `skill` — name of the skill the sub-agent should execute -- `plugin_root` — absolute path to the dev-team plugin directory (used to load skill files) -- `context_file` — absolute path to the pipeline context file -- `args` — (optional) positional arguments to present to the skill -- `read_sections` — comma-separated list of section names to read from the context file -- `write_section` — section name to overwrite with the skill output -- `result_format` — pipe-separated list of valid return values (e.g. `briefed | failed`) - -### Step 1 — Read context sections - -Use the `Read` tool to read `context_file`. Extract the content of each section in -`read_sections`. A section begins at `` and ends at the next -` - - -``` - -Use `Read` to read the current file content, then use **`Edit` only — never `Write`**. -Using `Write` would overwrite the entire file and erase sections written by other -concurrent agents. - -**If the sentinel `` already exists in the file:** - -Use `Edit` where: -- `old_string` = the sentinel line, the blank line after it, and all content up to - (but not including) the next `\n\n\n` - -**If the sentinel does not exist (new section):** - -Find the last `\n\n\n` - -**Overwrite the entire section — never append to it.** - -### Step 5 — Return result - -Determine which value from `result_format` best matches the skill's output. - -Respond with **exactly that one word or phrase** and nothing else. - -If the skill output cannot be mapped to any `result_format` value: -1. Append a parse-error note to the `` section - in `context_file` (create the section if it does not exist). - Format: `[task-runner] Could not map output for skill '' to result_format ''. Output excerpt: ` -2. Respond with exactly: `failed` - -## Constraints - -- Do not add commentary, apologies, or explanation to your response. -- Do not use MCP tools (Jira, GitHub, etc.) — those are for agent skills, not this wrapper. -- `write_section` overwrites the entire named section — no appending. -- The single-line result is the only output the top-level orchestration loop receives. diff --git a/plugins/dev-team/agents/workspace-setup.md b/plugins/dev-team/agents/workspace-setup.md deleted file mode 100644 index 2915c7f..0000000 --- a/plugins/dev-team/agents/workspace-setup.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -name: workspace-setup -description: > - Sets up the git workspace before a pipeline run. Finds the epic branch from - the spec or Jira, fetches latest, and creates the task branch. -model: haiku -tools: - - Read - - Bash - - mcp__08e9ccd3-4093-4425-adec-d98ea766a759__getJiraIssue ---- - -You are the workspace-setup agent for the dev-team pipeline. -Your only job is to set up the correct git branch before implementation begins. -Execute the instructions in `## Instructions` in your prompt. diff --git a/plugins/dev-team/commands/create-branch.md b/plugins/dev-team/commands/create-branch.md deleted file mode 100644 index c757193..0000000 --- a/plugins/dev-team/commands/create-branch.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -description: Ensure the current work runs on a dev/claude/ branch. -argument-hint: -user-invocable: false ---- - -## Inputs - -- Work item ID: `$ARGUMENTS` (e.g. `Issue-444`, `ADR-172`) - -If missing, stop and print: - -> Usage: `/create-branch ` - -## Steps - -1. Check the current branch: - ```bash - git branch --show-current - ``` -2. If already on `dev/claude/`, stop (nothing to do). -3. Create and switch: - ```bash - git checkout -b dev/claude/ - ``` diff --git a/plugins/dev-team/commands/dev-team.md b/plugins/dev-team/commands/dev-team.md deleted file mode 100644 index 5ff9380..0000000 --- a/plugins/dev-team/commands/dev-team.md +++ /dev/null @@ -1,176 +0,0 @@ ---- -description: > - Orchestration loop for the dev-team pipeline. Drives the step machine by repeatedly - invoking dev_team.py, parsing its JSON descriptor, and spawning the appropriate - agent via the Agent tool. Replaces run-workflow.md. -argument-hint: -user-invocable: false ---- - -## Arguments - -$ARGUMENTS - -Parse the three positional arguments from the line above: -- `work-item-id` — the resolved work item identifier (e.g. `ADR-123` or `Issue-444`) -- `workflow` — the pipeline filename stem (e.g. `implement-task-plan` or `fix-issue-plan`) -- `research-skill` — the researcher skill name (e.g. `researcher-plan` or `researcher-issue`) - -## Role - -You are the orchestration loop for the dev-team pipeline. You drive the step machine -by invoking `dev_team.py` repeatedly, parsing its JSON output, and spawning the -appropriate agent for each step. - -**Never attempt to:** -- Fix build errors, test failures, or code review comments yourself -- Invoke agent skills directly -- Edit source files or test files -- Take any action beyond what the JSON descriptor instructs - -## Steps - -### 1 — Compute context file path - -```bash -context_file=$(bash "${CLAUDE_PLUGIN_ROOT}/scripts/get-context-path.sh" "") -mkdir -p "$(dirname "$context_file")" -``` - -> **Note:** On Windows this runs via Git Bash, which ships with Git-for-Windows. No -> platform-detection branch is needed. - -### 2 — Orchestration loop - -Repeat the following until `action == "done"` or a terminal condition is reached: - -#### 2a — Run the step machine - -```bash -python -u ${CLAUDE_PLUGIN_ROOT}/scripts/dev_team.py \ - --workflow ${CLAUDE_PLUGIN_ROOT}/scripts/.md \ - --research-skill \ - --plugin-root ${CLAUDE_PLUGIN_ROOT} \ - --context-file -``` - -Capture all stdout. The last JSON array on stdout is the action descriptor list. - -#### 2b — Parse the descriptor array - -Display any non-JSON stdout lines as status updates to the user. - -Extract the last line from stdout that is a valid JSON array (starts with `[`). - -If the descriptors contain any `"message"` fields, use them to describe to the user -what work is being done before spawning the next agents. - -#### 2c — Branch on action - -Let `descriptors` be the parsed JSON array. The array always has at least one item. - -**If `descriptors` is a single-item array and `descriptors[0].action == "done"`:** -- If `result == "success"`: report success to the user and stop. -- If `result == "failed"`: report the failure reason to the user and stop. - -**If `descriptors` is a single-item array and `descriptors[0].skill == "troubleshooter"`:** - -Spawn the troubleshooter agent: -``` -Agent( - subagent_type="troubleshooter", - prompt=""" -context_file: -trigger: -cycle_count: -""" -) -``` - -Handle the outcome (a JSON object with `action` field): -- `"continue"` → continue the loop (the troubleshooter has edited the context file) -- `"terminate"` → report the reason to the user and stop -- `"needs_user_input"` → - 1. Ask the user the troubleshooter's question - 2. Write the user's answer to the `troubleshooter_input` frontmatter key in the - context file by passing the answer via stdin (avoids shell injection): - ```bash - python -c " - from pathlib import Path; import re, sys - path = Path('') - answer = sys.stdin.read().strip() - text = path.read_text(encoding='utf-8') - text = re.sub(r'troubleshooter_input:.*', lambda m: f'troubleshooter_input: {answer}', text) - path.write_text(text, encoding='utf-8') - " <<'ANSWER_HEREDOC' - - ANSWER_HEREDOC - ``` - 3. Continue the loop - -**All other lists (multiple items, a single `spawn_agent` item, or a single `run_script` item):** - -Dispatch all items in parallel — `spawn_agent` items via `Agent(subagent_type="task-runner")`, -`run_script` items via `Agent(subagent_type="script-runner")`: - -``` -results = await [ - Agent(subagent_type="task-runner", prompt=""" -agent: -skill: -plugin_root: -context_file: -args: -read_sections: -write_section: -result_format: -""") if item.action == "spawn_agent" else - - Agent(subagent_type="script-runner", prompt=""" -command: -log_file: -result_format: -""") if item.action == "run_script" - - for item in descriptors -] -``` - -Log each result: -``` -[] : -``` - -For each `run_script` item that has a `write_section` field, write the one-line result -to that section in the context file: -```bash -python -c " -from pathlib import Path; import sys -path = Path('') -result = '' # e.g. 'passed' or 'failed' -section = '' -sentinel = f'' -text = path.read_text(encoding='utf-8') -if sentinel in text: - import re - text = re.sub( - sentinel + r'.*?(?= init - init --> spec-finding : start - spec-finding --> setting-up : spec_found - setting-up --> researching : setup_done + init --> researching : setup_done researching --> implementing : research_done implementing --> validating : impl_done validating --> fixing : build_failed diff --git a/plugins/dev-team/scripts/dev_team.py b/plugins/dev-team/skills/workflow-orchestrate/scripts/dev_team.py similarity index 94% rename from plugins/dev-team/scripts/dev_team.py rename to plugins/dev-team/skills/workflow-orchestrate/scripts/dev_team.py index 54d3417..619b597 100644 --- a/plugins/dev-team/scripts/dev_team.py +++ b/plugins/dev-team/skills/workflow-orchestrate/scripts/dev_team.py @@ -507,43 +507,6 @@ def handle_results(self) -> str: ... -class SetupWorkspaceStep(Step): - handles = "setting-up" - _PENDING_KEY = "setup" - - def __init__(self, ctx: "PipelineContext", context_path: Path) -> None: - self._ctx = ctx - self._context_path = context_path - - def get_actions(self) -> list[dict]: - ctx = self._ctx - if ctx.workspace_setup: - return [] - return [{ - "action": "spawn_agent", - "message": f"Setting up workspace branch for {ctx.work_item_id}.", - "agent": "workspace-setup", - "skill": "workspace-setup", - "args": f"{ctx.work_item_id} {ctx.spec_path}", - "context_file": str(self._context_path), - "read_sections": [], - "write_section": "Workspace Setup", - "result_format": "setup_done | failed", - }] - - def handle_results(self) -> str: - ctx = self._ctx - if ctx.workspace_setup: - _handle_agent_success(ctx) - return "setup_done" - _handle_agent_failure(ctx) - _check_and_trigger_troubleshooter( - "consecutive_failures", CONSECUTIVE_FAILURES_THRESHOLD, - ctx.consecutive_failures, ctx, self._context_path, - ) - return "failed" - - class FindSpecStep(Step): handles = "spec-finding" @@ -580,7 +543,7 @@ def get_actions(self) -> list[dict]: return [{ "action": "spawn_agent", "message": f"Debugger is investigating {ctx.work_item_id}.", - "agent": "debugger", + "agent": "dev-team:debugger", "skill": "debugger-investigate", "context_file": str(self._context_path), "args": ctx.work_item_id, @@ -593,10 +556,11 @@ def handle_results(self) -> str: ctx = self._ctx if ctx.debug_report: _handle_agent_success(ctx) - if "# Debug report for" not in ctx.debug_report: - ctx.last_failure = f"Bug could not be reproduced.\n\n{ctx.debug_report}" - return "reproduction_failed" - return "debug_done" + status = parse_json_output(ctx.debug_report).get("status", "") + if status == "reproduced": + return "debug_done" + ctx.last_failure = f"Bug could not be reproduced.\n\n{ctx.debug_report}" + return "reproduction_failed" # Agent ran but wrote nothing _handle_agent_failure(ctx) _check_and_trigger_troubleshooter( @@ -625,7 +589,7 @@ def get_actions(self) -> list[dict]: return [{ "action": "spawn_agent", "message": f"Researcher is planning work for {ctx.work_item_id}.", - "agent": "researcher", + "agent": "dev-team:researcher", "skill": self._skill, "context_file": str(self._context_path), "args": f"{ctx.work_item_id} {ctx.spec_path}", @@ -663,7 +627,7 @@ def get_actions(self) -> list[dict]: return [{ "action": "spawn_agent", "message": "Researcher has written the task brief. Developer is now implementing.", - "agent": "developer", + "agent": "dev-team:developer", "skill": "developer-implement", "args": ctx.work_item_id, "context_file": str(self._context_path), @@ -703,9 +667,8 @@ def get_actions(self) -> list[dict]: timestamp = datetime.datetime.now().strftime("%Y%m%dT%H%M%S") log_path = self._log_dir / f"{ctx.work_item_id}-validate-{timestamp}.log" ctx.build_log = str(log_path) - ext = ".cmd" if sys.platform == "win32" else ".sh" - validate_script = REPO_ROOT / "scripts" / f"validate{ext}" - command = f'cmd /c "{validate_script}"' if sys.platform == "win32" else f'bash "{validate_script}"' + validate_script = REPO_ROOT / "scripts" / f"validate.sh" + command = str(validate_script) return [{ "action": "run_script", "message": "Running build and test validation.", @@ -721,7 +684,7 @@ def handle_results(self) -> str: result = ctx.validate_result.strip() ctx.validate_result = "" ctx.pending_agent = "" - if result == "passed": + if result.startswith("Succeeded"): ctx.last_failure = "" _commit_and_push(ctx.work_item_id) return "clean" @@ -757,7 +720,7 @@ def get_actions(self) -> list[dict]: return [{ "action": "spawn_agent", "message": "Implementation complete. Developer is creating a pull request.", - "agent": "developer", + "agent": "dev-team:developer", "skill": "developer-create-pr", "args": ctx.work_item_id, "context_file": str(self._context_path), @@ -772,15 +735,15 @@ def handle_results(self) -> str: # Inline path: already had pr_url _handle_agent_success(ctx) return "pr_created" - # Try to extract pr_url from the PR URL section written by the agent + # Extract pr_url from the JSON the skill wrote to the PR URL section text = self._context_path.read_text(encoding="utf-8") _, body = _parse_frontmatter(text) sections = _parse_sections(body) pr_url_section = sections.get("PR URL", "") if pr_url_section: - m = re.search(r"https://github\.com/[^\s]+/pull/\d+", pr_url_section) - if m: - ctx.pr_url = m.group(0) + pr_url = parse_json_output(pr_url_section).get("pr_url", "") + if pr_url: + ctx.pr_url = pr_url _handle_agent_success(ctx) ctx.save(self._context_path) return "pr_created" @@ -806,7 +769,7 @@ def get_actions(self) -> list[dict]: return [{ "action": "spawn_agent", "message": "Pull request created. Reviewer is reviewing the changes.", - "agent": "reviewer", + "agent": "dev-team:reviewer", "skill": "reviewer-review", "context_file": str(self._context_path), "read_sections": ["Researcher Brief"], @@ -869,7 +832,7 @@ def __init__(self, ctx: "PipelineContext", context_path: Path) -> None: def get_actions(self) -> list[dict]: return [{ "action": "spawn_agent", - "agent": "task-runner", + "agent": "dev-team:reviewer", "skill": "reviewer-sign-off", "context_file": str(self._context_path), "read_sections": ["Researcher Brief"], @@ -904,7 +867,7 @@ def get_actions(self) -> list[dict]: read_sections.append(f"Fix {i}") return [{ "action": "spawn_agent", - "agent": "task-runner", + "agent": "dev-team:researcher", "skill": "researcher-validate", "context_file": str(self._context_path), "read_sections": read_sections, @@ -954,7 +917,8 @@ def handle_results(self) -> str: ctx = self._ctx if ctx.signoff_build_result: _handle_agent_success(ctx) - return "approved" if ctx.signoff_build_result.strip().startswith("passed") else "failed" + status = parse_json_output(ctx.signoff_build_result).get("status", "") + return "approved" if status == "passed" else "failed" _handle_agent_failure(ctx) _check_and_trigger_troubleshooter( "consecutive_failures", CONSECUTIVE_FAILURES_THRESHOLD, @@ -988,7 +952,8 @@ def handle_results(self) -> str: # Build the failure summary for downstream steps failures: list[str] = [] - if not ctx.signoff_build_result.strip().startswith("passed"): + build_status = parse_json_output(ctx.signoff_build_result).get("status", "") + if build_status != "passed": if ctx.signoff_build_result: failures.append( f"Build/test validation failed. Log: {ctx.build_log}\n" @@ -1050,7 +1015,7 @@ def get_actions(self) -> list[dict]: f"Build or tests failed. Developer is fixing " f"(iteration {ctx.fix_iteration + 1} of {MAX_FIX_ITERATIONS})." ), - "agent": "developer", + "agent": "dev-team:developer", "skill": "developer-fix", "args": ctx.work_item_id, "context_file": str(self._context_path), @@ -1113,7 +1078,7 @@ def get_actions(self) -> list[dict]: f"Review requested changes. Developer is addressing review comments " f"(iteration {ctx.review_fix_iteration + 1} of {MAX_REVIEW_FIX_ITERATIONS})." ), - "agent": "developer", + "agent": "dev-team:developer", "skill": "developer-fix", "args": ctx.work_item_id, "context_file": str(self._context_path), @@ -1167,7 +1132,6 @@ def __init__( self.machine = StateMachine(workflow.transitions, initial=ctx.state) self.step_handlers: dict[str, Step] = { "spec-finding": FindSpecStep(ctx), - "setting-up": SetupWorkspaceStep(ctx, context_path), "debugging": DebugStep(ctx, context_path), "researching": ResearchStep(research_skill, ctx, context_path), "implementing": ImplementStep(ctx, context_path), @@ -1280,9 +1244,6 @@ def _find_repo_root() -> Path: REPO_ROOT = _find_repo_root() -# Resolved after argument parsing; default to the directory containing this script. -PLUGIN_ROOT: Path = Path(__file__).resolve().parent.parent - def _parse_frontmatter(text: str) -> tuple[dict, str]: """Split YAML frontmatter from body. Returns (metadata_dict, body).""" @@ -1402,10 +1363,6 @@ def main() -> None: if not args.context_file: parser.error("--context-file is required") - global PLUGIN_ROOT - if args.plugin_root: - PLUGIN_ROOT = Path(args.plugin_root).resolve() - work_item_id = args.work_item_id workflow_path = Path(args.workflow) if not workflow_path.is_absolute(): diff --git a/plugins/dev-team/scripts/get-context-path.sh b/plugins/dev-team/skills/workflow-orchestrate/scripts/get-context-path.sh similarity index 100% rename from plugins/dev-team/scripts/get-context-path.sh rename to plugins/dev-team/skills/workflow-orchestrate/scripts/get-context-path.sh diff --git a/plugins/dev-team/scripts/test_dev_team.py b/plugins/dev-team/skills/workflow-orchestrate/scripts/test_dev_team.py similarity index 99% rename from plugins/dev-team/scripts/test_dev_team.py rename to plugins/dev-team/skills/workflow-orchestrate/scripts/test_dev_team.py index 83d9da0..6b079e5 100644 --- a/plugins/dev-team/scripts/test_dev_team.py +++ b/plugins/dev-team/skills/workflow-orchestrate/scripts/test_dev_team.py @@ -988,9 +988,9 @@ def test_handle_results_extracts_pr_url_from_section(self, tmp_path): """Normal dispatch: agent writes PR URL section; handle_results extracts it.""" from dev_team import CreatePrStep ctx, context_path = self._make_ctx(tmp_path) - # Simulate agent writing the PR URL section + # Simulate agent writing the PR URL section as JSON (standardized format) text = context_path.read_text(encoding="utf-8") - text += "\n\n\nhttps://github.com/org/repo/pull/42\n" + text += '\n\n\n{"pr_url": "https://github.com/org/repo/pull/42"}\n' context_path.write_text(text, encoding="utf-8") step = CreatePrStep(ctx, context_path) diff --git a/plugins/dev-team/skills/workflow-orchestrate/scripts/wait-pr-checks.sh b/plugins/dev-team/skills/workflow-orchestrate/scripts/wait-pr-checks.sh new file mode 100644 index 0000000..42496e5 --- /dev/null +++ b/plugins/dev-team/skills/workflow-orchestrate/scripts/wait-pr-checks.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# wait-pr-checks.sh — Block until PR checks complete, then output pass/fail result. +# +# Usage: wait-pr-checks.sh +# +# Polls `gh pr checks` in a loop until no checks remain in the "pending" bucket, +# then inspects the final states. +# +# Outputs a human-readable summary line followed by a JSON status object: +# {"status": "passed"} +# {"status": "failed", "reason": ""} +# +# The JSON object is always the last stdout line so workflow-script can write it +# directly to the context file section. +# +# Exit code is 0 when all checks pass, 1 when checks fail or time out. + +set -euo pipefail + +if [[ $# -lt 1 ]]; then + echo "Usage: $(basename "$0") " >&2 + exit 1 +fi + +pr_url="$1" + +TIMEOUT_SECONDS=1800 # 30 minutes +POLL_INTERVAL=15 +elapsed=0 + +# Poll until no checks are pending (or timeout). +while [[ $elapsed -lt $TIMEOUT_SECONDS ]]; do + if ! pending=$(gh pr checks "$pr_url" --json bucket \ + --jq '[.[] | select(.bucket == "pending")] | length' 2>&1); then + reason="failed to query PR checks: $pending" + echo "failed - $reason" + echo "{\"status\": \"failed\", \"reason\": \"$reason\"}" + exit 1 + fi + + if [[ "$pending" -eq 0 ]]; then + break + fi + + echo "Waiting for $pending check(s) to complete... (${elapsed}s elapsed)" >&2 + sleep $POLL_INTERVAL + elapsed=$((elapsed + POLL_INTERVAL)) +done + +if [[ $elapsed -ge $TIMEOUT_SECONDS ]]; then + reason="checks still pending after ${TIMEOUT_SECONDS}s timeout" + echo "failed - $reason" + echo "{\"status\": \"failed\", \"reason\": \"$reason\"}" + exit 1 +fi + +# All checks have settled — inspect final states. +if ! failing=$(gh pr checks "$pr_url" --json bucket \ + --jq '[.[] | select(.bucket == "fail" or .bucket == "cancel")] | length' \ + 2>&1); then + reason="failed to query PR check results: $failing" + echo "failed - $reason" + echo "{\"status\": \"failed\", \"reason\": \"$reason\"}" + exit 1 +fi + +if [[ "$failing" -gt 0 ]]; then + reason="${failing} check(s) failed or were cancelled" + echo "failed - $reason" + echo "{\"status\": \"failed\", \"reason\": \"$reason\"}" + exit 1 +fi + +echo "passed - all checks passed" +echo '{"status": "passed"}' diff --git a/plugins/dev-team/skills/workflow-script/SKILL.md b/plugins/dev-team/skills/workflow-script/SKILL.md new file mode 100644 index 0000000..6cf1610 --- /dev/null +++ b/plugins/dev-team/skills/workflow-script/SKILL.md @@ -0,0 +1,66 @@ +--- +name: workflow-script +description: > + **Runs a Python script as part of a multi-agent orchestrated workflow.** + Use this skill when an agent is instructed to run a script step in an orchestrated workflow. +argument-hint: --context-file --write-section
--command --log-file +--- + +## Arguments + +- `--context-file` — absolute path to the workflow context file (e.g. `~/.dev-team/org/repo/ADR-123.md`) +- `--write-section` — name of the section to write the log file path to (e.g. `Build Result`) +- `--command` — the shell command to run (e.g. `python -u /path/to/validate.py ADR-123`) +- `--log-file` — a full path to a location where the script's output should be logged + +## Steps + +### 1 — Run the command + +Run the command via Bash, capturing combined stdout and stderr to the log file: + +```bash + > "" 2>&1 +``` + +### 2 — Write the log path to the context file + +Write the log file path to the `` section of ``. +Use `Edit`, never `Write` — concurrent agents share this file. +_Do not touch any other part of the file, and never modify the YAML +frontmatter unless explicitly instructed to do so._ + +The section format in the file is: + +``` + + + + +log: +``` + +Determine `` as follows: + +1. Read the last non-empty line of the log file. +2. If that line is a valid JSON object (starts with `{` and ends with `}`), use it verbatim as + `` — regardless of exit code. This lets scripts communicate structured status. +3. Otherwise: use `Succeeded` if the exit code is 0, or a short failure description (including + the exit code) if non-zero. + +**If the sentinel `` already exists:** use `Edit` to replace all +content between the sentinel and the next `` blocks +that hold agent output. Frontmatter fields relevant to troubleshooting: + +| Field | Description | +|---|---| +| `state` | Current pipeline state (e.g. `implementing`, `reviewing`). Edit this to resume at a different step. | +| `troubleshooter_input` | The user's answer if you previously returned `needs_user_input`. Empty on first call. | +| `pending_agent` | The last agent the pipeline attempted to spawn before failing. | +| `consecutive_failures` | Number of consecutive agent failures. Resets to 0 on success. | +| `signoff_cycle_count` | Number of completed sign-off rounds. | +| `review_cycle_count` | Number of completed review/fix rounds. | + +## Known triggers + +| Trigger | Meaning | What to look for | +|---|---|---| +| `consecutive_failures` | An agent has failed 3 times in a row | Check `pending_agent` and the section it should have written; look for missing output or error messages | +| `signoff_deadlock` | Sign-off has cycled twice without resolution | Read the `signoff_review` and `signoff_research` sections; determine what is blocking agreement | +| `review_loop` | Review/fix has iterated 3 times without approval | Read `review_notes` and `fix_summary` sections; identify what the reviewer keeps flagging | +| `unknown_state` | Pipeline entered a state with no handler | Check the `state` field; it may be a typo or a state that was removed — set it to a valid state | + +## Diagnosis steps + +1. Read the context file. Check `troubleshooter_input` — if non-empty, the user has answered a question + from a prior call; use that answer to decide what to do next. +2. Identify the trigger from `--problem` and note any relevant counter fields. +3. Read the `` blocks for the failing step to see what the agent produced (or failed to produce). +4. If needed, read plugin source files in the dev-team plugin directory to understand what a step expects. + +## Fix strategies + +- **Wrong or corrupted state** — edit the `state` frontmatter field to a valid pipeline state, then return `continue`. +- **Counter deadlock** — diagnose the root cause; if fixable, edit the relevant context section to break the cycle + and reset the counter to `0`; return `continue`. +- **Needs a user decision** — return `needs_user_input` with a single focused question; the orchestrator will + relay it to the user, write the answer to `troubleshooter_input`, and re-invoke this skill. +- **Cannot fix** — return `terminate` with a clear problem description and recommendation. + +## Output + +Return a JSON object — exactly one of these three shapes: + +```json +{ "action": "continue" } +``` +You applied a fix. The orchestrator resumes from whatever `state` is now set in the context file. + +```json +{ "action": "needs_user_input", "question": "" } +``` +You need the user to make a decision. The orchestrator asks the question, writes the answer to +`troubleshooter_input`, and re-invokes this skill. + +```json +{ "action": "terminate", "reason": "" } +``` +You could not fix the issue. The orchestrator reports the reason to the user and stops. diff --git a/plugins/dev-team/skills/workflow-worker/SKILL.md b/plugins/dev-team/skills/workflow-worker/SKILL.md new file mode 100644 index 0000000..a3ed454 --- /dev/null +++ b/plugins/dev-team/skills/workflow-worker/SKILL.md @@ -0,0 +1,47 @@ +--- +name: workflow-worker +description: > + **Defines the rules for working as a part of a multi-agent orchestrated workflow.** + Use this skill when an agent is instructed to run as an orchestrated worker. +argument-hint: --context-file --write-section
--skill [--skill-args ] +--- + +## Arguments + +- `--context-file` — absolute path to the workflow context file (e.g. `~/.dev-team/org/repo/ADR-123.md`) +- `--write-section` — name of the section to write output to (e.g. `Researcher Brief`) +- `--skill` — name of the skill to invoke +- `--skill-args` — (optional) arguments to pass to the skill + +## Steps + +### 1 — Invoke the skill + +Use the `Skill` tool to invoke `` with `` as arguments. Follow the skill's +instructions and complete all its steps. Capture the output — do not return it to the caller yet. + +### 2 — Write output to the context file + +Write the captured output to the `` section of ``. +Use `Edit`, never `Write` — concurrent agents share this file and `Write` would overwrite their sections. + +The section format is: +``` + + + +``` + +**If the sentinel `` already exists:** use `Edit` to replace all +content between the sentinel and the next `