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/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/plugins/dev-team/.claude-plugin/plugin.json b/plugins/dev-team/.claude-plugin/plugin.json index 82b741b..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.0", + "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/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 65fbf5f..0000000 --- a/plugins/dev-team/agents/task-runner.md +++ /dev/null @@ -1,137 +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 invoke -- `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/commands/create-branch.md b/plugins/dev-team/commands/create-branch.md deleted file mode 100644 index 94b8a42..0000000 --- a/plugins/dev-team/commands/create-branch.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -description: Ensure the current work runs on a feature branch derived from a work-item ID and title/brief text. -argument-hint: "" -user-invocable: false ---- - -## Inputs - -- Work item ID: first token in `$ARGUMENTS` (e.g. `Issue-444`, `ADR-172`) -- Slug source text: remaining argument text (quoted when it includes spaces) - -If either input is 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. Derive a slug from the source text: - - lowercase - - replace spaces and underscores with hyphens - - remove non-alphanumeric/non-hyphen characters - - trim to 40 characters -4. 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 de59640..0000000 --- a/plugins/dev-team/commands/dev-team.md +++ /dev/null @@ -1,175 +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: -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'.*?(?= 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/skills/workflow-orchestrate/assets/implement-task-plan.md similarity index 91% rename from plugins/dev-team/scripts/implement-task-plan.md rename to plugins/dev-team/skills/workflow-orchestrate/assets/implement-task-plan.md index f3432f0..1eaeaad 100644 --- a/plugins/dev-team/scripts/implement-task-plan.md +++ b/plugins/dev-team/skills/workflow-orchestrate/assets/implement-task-plan.md @@ -1,13 +1,13 @@ ```mermaid stateDiagram-v2 [*] --> init - init --> spec-finding : start - spec-finding --> researching : spec_found + init --> researching : setup_done researching --> implementing : research_done 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/dev_team.py b/plugins/dev-team/skills/workflow-orchestrate/scripts/dev_team.py similarity index 68% rename from plugins/dev-team/scripts/dev_team.py rename to plugins/dev-team/skills/workflow-orchestrate/scripts/dev_team.py index 27d31dc..619b597 100644 --- a/plugins/dev-team/scripts/dev_team.py +++ b/plugins/dev-team/skills/workflow-orchestrate/scripts/dev_team.py @@ -199,6 +199,7 @@ class PipelineContext: build_log: str = "" test_log: str = "" debug_report: str = "" + workspace_setup: str = "" signoff_review: str = "" signoff_research: str = "" signoff_build_result: str = "" @@ -239,6 +240,9 @@ def save(self, path: Path) -> None: f"# {self.work_item_id} Dev Team Context", ] + if self.workspace_setup: + lines += ["", "", "", 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", "") @@ -492,22 +497,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,39 +531,44 @@ 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", + "agent": "dev-team:debugger", "skill": "debugger-investigate", "context_file": str(self._context_path), "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) + 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( + "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,38 +576,39 @@ 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", + "agent": "dev-team:researcher", "skill": self._skill, "context_file": str(self._context_path), "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,36 +616,37 @@ 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", + "agent": "dev-team:developer", "skill": "developer-implement", "args": ctx.work_item_id, "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 +654,317 @@ 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) + validate_script = REPO_ROOT / "scripts" / f"validate.sh" + command = str(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) + if result.startswith("Succeeded"): 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) - - 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", - }]) +class CreatePrStep(Step): + handles = "creating-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 + 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": "dev-team: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" + # 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: + 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" + _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": "dev-team: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": "dev-team:reviewer", + "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": "dev-team:researcher", + "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, - ) + "result_format": "success | failed", + }] - 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", - }]) - - # 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) + 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, + 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}") + 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" + 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,122 +972,99 @@ 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 " 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), "read_sections": read_sections, "write_section": write_section, - "result_format": "fixed | failed", - }]) - - -class FixPrStep(Step): - handles = "fixing-pr" - - def __init__(self, context_path: Path) -> None: - self._context_path = context_path + "result_format": "success | failed", + }] - 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,23 +1072,42 @@ 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 " 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), "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 +1131,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 +1179,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 +1214,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 # --------------------------------------------------------------------------- @@ -1187,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).""" @@ -1309,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(): @@ -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/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 75% rename from plugins/dev-team/scripts/test_dev_team.py rename to plugins/dev-team/skills/workflow-orchestrate/scripts/test_dev_team.py index 5e1314c..6b079e5 100644 --- a/plugins/dev-team/scripts/test_dev_team.py +++ b/plugins/dev-team/skills/workflow-orchestrate/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 as JSON (standardized format) + text = context_path.read_text(encoding="utf-8") + 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) + 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] 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 `