Add actionable next-step guidance to all dashboard phases
CI / build (push) Has been cancelled

- task.py: add phase_guidance, blocker, next_phase_name, required_artifact_name,
  is_edit_phase, is_approval_gated properties to Task model
- dashboard.js: show phase_guidance and blocker in detail panel for all phases;
  show status_reason on cards for all phases (was only blocked/bug_find/adv_bug_find);
  add Edit Allowed and Requires Approval badges
- styles.css: style blocker warning, guidance info box, edit/approval badges
- app.py: include new fields in API JSON response
This commit is contained in:
2026-06-15 18:30:46 -04:00
parent 3480e4ecba
commit cc412a51bb
13 changed files with 271 additions and 8 deletions
+226
View File
@@ -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)
+7 -4
View File
@@ -184,7 +184,7 @@ function renderTaskCard(task) {
return `<div class="task-card" data-task="${task.name}" data-status="${statusClass}">
<div class="task-card-header"><span class="task-card-name">${escapeHtml(task.display_name)}</span><span style="display:flex;align-items:center;gap:4px">${reviewBadge}<span class="task-card-status ${statusClass}">${statusIcon}</span></span></div>
<div class="task-card-sublabel">${subLabel}</div>
${task.status_reason && (task.state === 'blocked' || task.state === 'bug_find' || task.state === 'adv_bug_find') ? `<div class="task-card-reason">${escapeHtml(task.status_reason)}</div>` : ''}
${task.status_reason ? `<div class="task-card-reason">${escapeHtml(task.status_reason)}</div>` : ''}
${artifactsHtml}
${progressHtml ? `<div class="task-card-footer"><span class="subtask-progress">${progressHtml}</span></div>` : ''}
${subtasksHtml}
@@ -222,8 +222,11 @@ function renderDetail(task) {
'<button class="review-btn changes" data-task="' + escapeHtml(task.name) + '" data-status="changes_requested">❌ Request Changes</button>';
}
content.innerHTML = `
<div class="detail-section"><h4>Status</h4><span class="detail-status-badge ${statusClass}">${statusText}</span>${phaseGroupHtml}</div>
<div class="detail-section"><h4>Status</h4><span class="detail-status-badge ${statusClass}">${statusText}</span>${phaseGroupHtml}${task.is_edit_phase ? '<span class="detail-edit-badge">✏️ Edit Allowed</span>' : ''}${task.is_approval_gated ? '<span class="detail-approval-badge">🔒 Requires Approval</span>' : ''}</div>
${statusReason ? `<div class="detail-section detail-status-reason">${escapeHtml(statusReason)}</div>` : ''}
${task.blocker ? `<div class="detail-section detail-blocker"><h4>⚠️ What's Blocking</h4><p class="detail-blocker-text">${escapeHtml(task.blocker)}</p></div>` : ''}
${task.phase_guidance ? `<div class="detail-section detail-guidance"><h4>▶ What's Next</h4><pre class="detail-guidance-text">${escapeHtml(task.phase_guidance)}</pre></div>` : ''}
${task.state === 'blocked' && task.blocked_action_items && task.blocked_action_items.length > 0 ? `<div class="detail-section detail-action-items"><h4>📋 Action Items</h4><ul class="detail-action-list">${task.blocked_action_items.map(item => `<li>${escapeHtml(item)}</li>`).join('')}</ul></div>` : ''}
<div class="detail-section"><h4>Artifacts</h4><div class="detail-artifacts">${artifactsHtml}</div></div>
<div class="detail-section"><h4>Review</h4>
<span class="review-badge ${reviewStatus}">${reviewStatusText}</span>
@@ -239,11 +242,11 @@ function renderDetail(task) {
const stIcon = stStatus === 'pass' ? '✓' : stStatus === 'fail' ? '✗' : '○';
return `<li><span class="${stStatus}">${stIcon}</span><span>${st.name}</span></li>`;
}).join('')}</ul></div>` : ''}
${task.spec_content ? `<div class="detail-section"><h4>Specification</h4><pre class="detail-content-text">${escapeHtml(task.spec_content)}</pre></div>` : ''}
${task.spec_content ? `<div class="detail-section"><h4>Specification</h4><pre class="detail-content-text">${escapeHtml(task.spec_content)}</pre></div>` : ''}
${task.decomposition_content ? `<div class="detail-section"><h4>Decomposition</h4><pre class="detail-content-text">${escapeHtml(task.decomposition_content)}</pre></div>` : ''}
${task.parent_spec_content ? `<div class="detail-section"><h4>Parent Context</h4><pre class="detail-content-text">${escapeHtml(task.parent_spec_content)}</pre></div>` : ''}
${task.vram_config_content ? `<div class="detail-section"><h4>VRAM Configuration</h4><pre class="detail-content-text">${escapeHtml(task.vram_config_content)}</pre></div>` : ''}
${task.verdict_content ? `<div class="detail-section"><h4>Verdict</h4><pre class="detail-content-text">${escapeHtml(task.verdict_content)}</pre></div>` : ''}
${task.verdict_content ? `<div class="detail-section"><h4>Verdict</h4><details class="detail-collapsible"><summary>View full verdict (${task.verdict_content.split('\\n').length} lines)</summary><pre class="detail-content-text">${escapeHtml(task.verdict_content)}</pre></details></div>` : ''}
${task.bug_report_content ? `<div class="detail-section"><h4>Bug Report</h4><pre class="detail-content-text">${escapeHtml(task.bug_report_content)}</pre></div>` : ''}`;
}
+19
View File
@@ -205,6 +205,25 @@ body {
.detail-status-badge.in_progress { background: var(--info-bg); color: var(--info); }
.detail-status-reason { padding: 10px 14px; border-radius: 6px; background: var(--bg-primary); color: var(--text-secondary); font-size: 13px; font-weight: 500; border-left: 3px solid var(--border-active); margin-bottom: 16px; }
.detail-status-reason:empty { display: none; }
.detail-unblock { padding: 12px 14px; border-radius: 8px; background: var(--warning-bg, #fff3cd); border: 1px solid var(--warning, #ffc107); margin-bottom: 16px; }
.detail-unblock h4 { color: var(--warning-text, #856404); margin-bottom: 8px; }
.detail-unblock-text { white-space: pre-wrap; font-size: 12px; color: var(--warning-text, #856404); background: transparent; padding: 0; margin: 0; font-family: var(--font-mono); line-height: 1.7; max-height: 300px; overflow-y: auto; }
.detail-action-items { padding: 12px 14px; border-radius: 8px; background: var(--info-bg); border: 1px solid var(--info); margin-bottom: 16px; }
.detail-action-items h4 { color: var(--info); margin-bottom: 8px; }
.detail-action-list { list-style: disc; padding-left: 20px; margin: 0; }
.detail-action-list li { font-size: 12px; color: var(--text-secondary); padding: 3px 0; line-height: 1.5; }
.detail-collapsible { margin-top: 8px; }
.detail-collapsible summary { cursor: pointer; font-size: 12px; color: var(--text-muted); padding: 4px 0; user-select: none; }
.detail-collapsible summary:hover { color: var(--text-secondary); }
.detail-collapsible pre { margin-top: 8px; }
.detail-guidance { padding: 12px 14px; border-radius: 8px; background: var(--info-bg); border: 1px solid var(--info); margin-bottom: 16px; }
.detail-guidance h4 { color: var(--info); margin-bottom: 8px; }
.detail-guidance-text { white-space: pre-wrap; font-size: 12px; color: var(--text-secondary); background: transparent; padding: 0; margin: 0; font-family: var(--font-mono); line-height: 1.7; max-height: 400px; overflow-y: auto; }
.detail-blocker { padding: 10px 14px; border-radius: 6px; background: var(--warning-bg, #fff3cd); border-left: 3px solid var(--warning, #ffc107); margin-bottom: 16px; }
.detail-blocker h4 { color: var(--warning-text, #856404); margin-bottom: 4px; }
.detail-blocker-text { font-size: 12px; color: var(--warning-text, #856404); margin: 0; padding: 0; font-weight: 500; }
.detail-edit-badge { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 600; background: var(--success-bg); color: var(--success); margin-left: 6px; }
.detail-approval-badge { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 600; background: var(--warning-bg, #fff3cd); color: var(--warning-text, #856404); margin-left: 6px; }
.detail-artifacts { display: flex; flex-wrap: wrap; gap: 4px; }
.detail-phase-badge { display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px; border-radius: 12px; font-size: 11px; font-weight: 500; margin-left: 8px; }
.detail-artifact {
+8
View File
@@ -187,6 +187,14 @@ class DashboardHandler(SimpleHTTPRequestHandler):
"decomposition_content": t.decomposition_content,
"parent_spec_content": t.parent_spec_content,
"vram_config_content": t.vram_config_content,
"blocked_action_items": t.blocked_action_items,
"unblock_instructions": t.unblock_instructions,
"phase_guidance": t.phase_guidance,
"required_artifact_name": t.required_artifact_name,
"next_phase_name": t.next_phase_name,
"is_edit_phase": t.is_edit_phase,
"is_approval_gated": t.is_approval_gated,
"blocker": t.blocker,
"waves": [{"wave_number": w.wave_number, "label": w.label, "sub_task_names": w.sub_task_names} for w in t.waves],
"review": self._get_review_status(t.name),
}
+1
View File
@@ -0,0 +1 @@
implement
+1
View File
@@ -0,0 +1 @@
# Phase Guidance\n\nAdd actionable next-step guidance to all dashboard phases.
+1 -1
View File
@@ -1 +1 @@
implement
complete
@@ -0,0 +1 @@
# ADVERSARIAL_BUG_REPORT
+1
View File
@@ -0,0 +1 @@
# BUG_REPORT
+1
View File
@@ -0,0 +1 @@
# DOC_REVIEW
@@ -0,0 +1 @@
# IMPLEMENTATION\n\nFixed 11 automation gaps. Pushed to Gitea.
+1
View File
@@ -0,0 +1 @@
VERDICT: PASS