Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions frontend/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,12 @@ export async function postProjectDirective(id: string, content: string) {
})
}

export async function dismissProjectDirective(id: string) {
return request<{ ok: boolean }>(`/projects/${id}/directive/dismiss`, {
method: "POST",
})
}

// ---------------------------------------------------------------------------
// Snapshots, Proposals, Human Requests
// ---------------------------------------------------------------------------
Expand Down
35 changes: 35 additions & 0 deletions frontend/src/pages/ProjectDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
reviewAutopilotCycle,
fetchProjectChat,
postProjectChat,
dismissProjectDirective,
} from "@/lib/api"
import type { Snapshot } from "@/lib/api"
import type {
Expand Down Expand Up @@ -1180,6 +1181,40 @@ export default function ProjectDetail() {
New directive
</Button>
</div>
{(() => {
const activeDir = directives.find((d) => d.sk === project.active_directive_sk)
const stuck =
project.active_directive_sk &&
!project.awaiting_next_directive &&
activeDir &&
activeDir.task_ids.length === 0
if (!stuck) return null
return (
<div className="mb-4 flex items-center gap-3 rounded-lg border border-amber-500/30 bg-amber-500/5 px-4 py-3 text-[13px] text-amber-300">
<AlertTriangle className="size-4 shrink-0 text-amber-400" />
<span className="flex-1">
Directive decomposition failed — no tasks were created. Autopilot is blocked.
</span>
<Button
variant="outline"
size="sm"
className="h-7 shrink-0 gap-1.5 border-amber-500/30 text-[12px] text-amber-300 hover:bg-amber-500/10"
onClick={async () => {
try {
await dismissProjectDirective(project.id)
toast.success("Directive dismissed — autopilot unblocked")
await load()
} catch {
toast.error("Failed to dismiss directive")
}
}}
>
<XCircle className="size-3.5" />
Dismiss
</Button>
</div>
)
})()}
{directives.length === 0 ? (
<p className="text-sm text-zinc-500">No directives yet.</p>
) : (
Expand Down
13 changes: 13 additions & 0 deletions infra/packages/api/src/routes/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,19 @@ projects.post("/projects/:id/directive", async (c) => {
return c.json({ ok: true, directive });
});

// POST /projects/:id/directive/dismiss — clear stuck directive so autopilot can proceed
projects.post("/projects/:id/directive/dismiss", async (c) => {
const p = await db.getProject(c.req.param("id"));
if (!p) return c.json({ error: "not found" }, 404);
if (!p.active_directive_sk) {
return c.json({ error: "no active directive to dismiss" }, 400);
}
await db.updateProject(p.id, {
awaiting_next_directive: true,
});
return c.json({ ok: true });
});

// ---------------------------------------------------------------------------
// Snapshots
// ---------------------------------------------------------------------------
Expand Down
21 changes: 21 additions & 0 deletions src/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,19 @@ def run_plan_only(store, task):
return True


def _clear_directive_on_failure(project_id, directive_sk):
# type: (str, str) -> None
"""Reset project directive flags so autopilot isn't permanently blocked by a failed decomposition."""
try:
from .projects_dynamo import update_project

update_project(project_id, {"awaiting_next_directive": True})
log.info("run_directive: cleared directive flags for project %s after failure", project_id)
plog(project_id, "directive_failed", "plan", "Decomposition failed for %s" % directive_sk)
except Exception:
log.exception("run_directive: failed to clear directive flags for project %s", project_id)


def run_directive(store, project_id, directive_sk):
# type: (Any, str, str) -> bool
"""Decompose a project directive into top-level tasks (plan-only style), then dispatch runners."""
Expand All @@ -483,10 +496,12 @@ def run_directive(store, project_id, directive_sk):
proj = get_project(project_id)
if not proj:
log.error("run_directive: project %s not found", project_id)
_clear_directive_on_failure(project_id, directive_sk)
return False
ditem = get_directive_item(project_id, directive_sk)
if not ditem:
log.error("run_directive: directive %s not found for project %s", directive_sk, project_id)
_clear_directive_on_failure(project_id, directive_sk)
return False

spec = (proj.get("spec") or "").strip() or "(no spec)"
Expand Down Expand Up @@ -518,31 +533,37 @@ def run_directive(store, project_id, directive_sk):
)
except subprocess.TimeoutExpired:
log.warning("run_directive: timed out for project %s", project_id)
_clear_directive_on_failure(project_id, directive_sk)
return False
except Exception:
log.exception("run_directive: agent error for project %s", project_id)
_clear_directive_on_failure(project_id, directive_sk)
return False

if result.returncode != 0:
log.warning(
"run_directive: agent failed project %s (exit %d)", project_id, result.returncode
)
_clear_directive_on_failure(project_id, directive_sk)
return False

text = _extract_agent_text(result.stdout)
json_match = re.search(r"\[.*\]", text, re.DOTALL)
if not json_match:
log.warning("run_directive: no JSON for project %s", project_id)
_clear_directive_on_failure(project_id, directive_sk)
return False

try:
steps = _json.loads(json_match.group())
except _json.JSONDecodeError:
log.exception("run_directive: JSON parse error for project %s", project_id)
_clear_directive_on_failure(project_id, directive_sk)
return False

if not isinstance(steps, list) or not steps:
log.warning("run_directive: empty plan for project %s", project_id)
_clear_directive_on_failure(project_id, directive_sk)
return False

spec_excerpt = spec if len(spec) <= 2000 else spec[:2000] + "… (truncated)"
Expand Down
Loading