diff --git a/automaton/dashboard/core/task.py b/automaton/dashboard/core/task.py index 51a42a3..547d793 100644 --- a/automaton/dashboard/core/task.py +++ b/automaton/dashboard/core/task.py @@ -176,6 +176,232 @@ class Task: return "No artifacts yet — not started" return f"In {self.state.value} phase" + @property + def blocked_action_items(self) -> list[str]: + """Extract actionable items from verdict for BLOCKED tasks.""" + if self.state != TaskState.BLOCKED: + return [] + verdict = self.artifacts.get("VERDICT.md") + if not verdict or not verdict.content: + return [] + content = verdict.content + items = [] + lines = content.splitlines() + in_section = False + section_items = [] + for line in lines: + low = line.strip().lower() + if any(kw in low for kw in ("tie-break", "tie break", "task for review", "remaining issue", "required action", "fix required")): + if low.startswith("##") or low.startswith("###") or low.startswith("- **"): + in_section = True + continue + if in_section and line.strip().startswith(("##", "###")) and not any(kw in line.strip().lower() for kw in ("tie-break", "tie break", "task for review", "remaining issue", "required action", "fix required")): + if section_items: + items.extend(section_items) + section_items = [] + in_section = False + if in_section and line.strip(): + section_items.append(line.strip().lstrip("- *").strip()) + if section_items: + items.extend(section_items) + return items if items else [] + + @property + def unblock_instructions(self) -> str: + """Return actionable instructions for unblocking a BLOCKED task.""" + if self.state != TaskState.BLOCKED: + return "" + verdict = self.artifacts.get("VERDICT.md") + if not verdict or not verdict.content: + return "The verdict file is empty. Use `status.py --transition referee --task " + self.name + "` to re-run the referee." + status = parse_verdict_status(verdict.content) + if status == VERDICT_FAIL: + return ( + "This task FAILED the referee review.\n\n" + "What to do:\n" + "1. Review the verdict below to understand what failed\n" + "2. Address the issues (fix bugs, improve tests, update docs)\n" + "3. To re-submit for review:\n" + " python ~/.automaton/scripts/status.py --transition referee --task " + self.name + "\n" + " (Then the agent will re-generate the verdict)" + ) + if status == VERDICT_NEEDS_REVIEW: + return ( + "This task requires MANUAL REVIEW.\n\n" + "What to do:\n" + "1. Read the verdict below — look for Tie-Breaks or issues the referee couldn't resolve\n" + "2. Make your decision on each Tie-Break\n" + "3. To approve (no changes needed):\n" + " python ~/.automaton/scripts/status.py --transition complete --task " + self.name + "\n" + "4. To reject (needs rework):\n" + " python ~/.automaton/scripts/status.py --transition human_intervention --task " + self.name + "\n" + " (Then the agent will address the issues and re-submit)" + ) + return ( + "This task is BLOCKED.\n\n" + "What to do:\n" + "1. Review the verdict content below\n" + "2. To re-run the referee:\n" + " python ~/.automaton/scripts/status.py --transition referee --task " + self.name + "\n" + "3. To mark complete:\n" + " python ~/.automaton/scripts/status.py --transition complete --task " + self.name + ) + + @property + def required_artifact_name(self) -> str: + phase_map = { + TaskState.BACKLOG: "", + TaskState.RESEARCH: "SPEC.md", + TaskState.DECOMPOSITION: "DECOMPOSITION.md", + TaskState.DESIGN: "DESIGN.md", + TaskState.TEST_DESIGN: "TEST_PLAN.md", + TaskState.IMPLEMENT: "IMPLEMENTATION.md", + TaskState.BUG_FIND: "BUG_REPORT.md", + TaskState.ADV_BUG_FIND: "ADVERSARIAL_BUG_REPORT.md", + TaskState.DOC_REVIEW: "DOC_REVIEW.md", + TaskState.REFEREE: "VERDICT.md", + TaskState.DONE: "", + TaskState.BLOCKED: "", + } + return phase_map.get(self.state, "") + + @property + def next_phase_name(self) -> str: + phase_map = { + TaskState.BACKLOG: "research", + TaskState.RESEARCH: "design, decomposition, or implement", + TaskState.DECOMPOSITION: "sub-task research", + TaskState.DESIGN: "test_design or implement", + TaskState.TEST_DESIGN: "implement", + TaskState.IMPLEMENT: "bug_find", + TaskState.BUG_FIND: "adversarial_bug_find", + TaskState.ADV_BUG_FIND: "doc_review", + TaskState.DOC_REVIEW: "referee", + TaskState.REFEREE: "complete or human_intervention", + TaskState.DONE: "", + TaskState.BLOCKED: "complete, referee, or human_intervention", + } + return phase_map.get(self.state, "") + + @property + def is_edit_phase(self) -> bool: + return self.state in (TaskState.IMPLEMENT, TaskState.DOC_REVIEW) + + @property + def is_approval_gated(self) -> bool: + return self.state in (TaskState.RESEARCH, TaskState.DECOMPOSITION, TaskState.DESIGN, TaskState.TEST_DESIGN) + + @property + def blocker(self) -> str: + """What's blocking this task from progressing to the next phase?""" + required = self.required_artifact_name + if required: + artifact = self.artifacts.get(required) + if not artifact or not artifact.exists: + return f"Missing required artifact: {required}" + if not artifact.content: + return f"Empty required artifact: {required}" + if self.is_approval_gated: + return "Awaiting user approval — use `status.py --approve` to approve" + if self.state == TaskState.BUG_FIND: + if "BUG_REPORT.md" not in self.artifacts or not self.artifacts["BUG_REPORT.md"].content: + return "Agent must generate BUG_REPORT.md" + if self.state == TaskState.ADV_BUG_FIND: + if "ADVERSARIAL_BUG_REPORT.md" not in self.artifacts or not self.artifacts["ADVERSARIAL_BUG_REPORT.md"].content: + return "Agent must generate ADVERSARIAL_BUG_REPORT.md" + if self.state == TaskState.DOC_REVIEW: + if "DOC_REVIEW.md" not in self.artifacts or not self.artifacts["DOC_REVIEW.md"].content: + return "Agent must generate DOC_REVIEW.md" + if self.state == TaskState.REFEREE: + if "VERDICT.md" not in self.artifacts or not self.artifacts["VERDICT.md"].content: + return "Agent must generate VERDICT.md" + if self.state == TaskState.BACKLOG: + return "No artifacts yet — needs SPEC.md to start research" + return "" + + @property + def phase_guidance(self) -> str: + """Actionable guidance for what to do in this phase.""" + if self.state == TaskState.BACKLOG: + return ( + "This task has not started.\n\n" + "To begin:\n" + "1. Transition to research:\n" + f" python ~/.automaton/scripts/status.py --transition research --task {self.name}\n" + "2. The agent will then produce a SPEC.md" + ) + if self.state == TaskState.RESEARCH: + return ( + "Agent should write SPEC.md defining the task scope, requirements, and acceptance criteria.\n\n" + "When ready for review:\n" + f" python ~/.automaton/scripts/status.py --transition research:awaiting_approval --task {self.name}\n\n" + "Or skip to design/implement directly:\n" + f" python ~/.automaton/scripts/status.py --transition design --task {self.name}\n" + f" python ~/.automaton/scripts/status.py --transition implement --task {self.name}" + ) + if self.state == TaskState.DECOMPOSITION: + return ( + "Agent should write DECOMPOSITION.md breaking the work into sub-tasks.\n\n" + "When ready for review:\n" + f" python ~/.automaton/scripts/status.py --transition decomposition:awaiting_approval --task {self.name}\n\n" + "After approval, sub-tasks will be created by the Orchestrator." + ) + if self.state == TaskState.DESIGN: + return ( + "Agent should write DESIGN.md describing architecture, data flow, and interfaces.\n\n" + "When ready for review:\n" + f" python ~/.automaton/scripts/status.py --transition design:awaiting_approval --task {self.name}\n\n" + "Or skip to test_design/implement:\n" + f" python ~/.automaton/scripts/status.py --transition test_design --task {self.name}" + ) + if self.state == TaskState.TEST_DESIGN: + return ( + "Agent should write TEST_PLAN.md describing test strategy and test cases.\n\n" + "When ready for review:\n" + f" python ~/.automaton/scripts/status.py --transition test_design:awaiting_approval --task {self.name}\n\n" + "Or skip to implementation:\n" + f" python ~/.automaton/scripts/status.py --transition implement --task {self.name}" + ) + if self.state == TaskState.IMPLEMENT: + return ( + "EDIT ALLOWED — agent can modify code, write tests, create IMPLEMENTATION.md.\n\n" + "When implementation is done:\n" + f" python ~/.automaton/scripts/status.py --transition bug_find --task {self.name}" + ) + if self.state == TaskState.BUG_FIND: + return ( + "Agent should review the code and write BUG_REPORT.md.\n\n" + "When done:\n" + f" python ~/.automaton/scripts/status.py --transition adversarial_bug_find --task {self.name}" + ) + if self.state == TaskState.ADV_BUG_FIND: + return ( + "Agent should perform adversarial review and write ADVERSARIAL_BUG_REPORT.md.\n\n" + "When done:\n" + f" python ~/.automaton/scripts/status.py --transition doc_review --task {self.name}" + ) + if self.state == TaskState.DOC_REVIEW: + return ( + "EDIT ALLOWED — agent can review/update documentation and write DOC_REVIEW.md.\n\n" + "When done:\n" + f" python ~/.automaton/scripts/status.py --transition referee --task {self.name}" + ) + if self.state == TaskState.REFEREE: + return ( + "Agent should review all artifacts and write VERDICT.md.\n\n" + "When verdict is complete:\n" + f" python ~/.automaton/scripts/status.py --transition complete --task {self.name} (if PASS)\n" + f" python ~/.automaton/scripts/status.py --transition human_intervention --task {self.name} (if FAIL or NEEDS_REVIEW)" + ) + if self.state == TaskState.DONE: + return ( + "This task is complete — all phases passed.\n\n" + "To archive: no action needed. The task will remain in the Done column." + ) + if self.state == TaskState.BLOCKED: + return self.unblock_instructions + return f"Task is in {self.state.value} phase." + @property def has_verdict(self) -> bool: return self.state in (TaskState.REFEREE, TaskState.DONE, TaskState.BLOCKED) diff --git a/automaton/dashboard/html/dashboard.js b/automaton/dashboard/html/dashboard.js index edca33c..306927a 100644 --- a/automaton/dashboard/html/dashboard.js +++ b/automaton/dashboard/html/dashboard.js @@ -184,7 +184,7 @@ function renderTaskCard(task) { return `
${escapeHtml(task.blocker)}
${escapeHtml(task.phase_guidance)}${escapeHtml(task.spec_content)}${escapeHtml(task.decomposition_content)}${escapeHtml(task.parent_spec_content)}${escapeHtml(task.vram_config_content)}${escapeHtml(task.verdict_content)}${escapeHtml(task.spec_content)}${escapeHtml(task.decomposition_content)}${escapeHtml(task.parent_spec_content)}${escapeHtml(task.vram_config_content)}${escapeHtml(task.verdict_content)}${escapeHtml(task.bug_report_content)}