From cd3a20f627c96bb0af0203853965fce696102f6b Mon Sep 17 00:00:00 2001 From: Frontend Developer 1 Date: Sun, 27 Sep 2026 23:05:22 +0000 Subject: [PATCH 1/2] [99f61c8c] feat(tasks): read-only stuck indicator driven by wedge-ledger contract fields --- .../components/kanban/core/kanban-card.tsx | 4 + .../tasks/__tests__/stuck-indicator.test.tsx | 128 ++++++++++++++++++ panel/src/components/tasks/index.ts | 1 + .../src/components/tasks/stuck-indicator.tsx | 60 ++++++++ .../tasks/task-detail/task-header.tsx | 5 + .../hooks/__tests__/use-stuck-state.test.ts | 118 ++++++++++++++++ panel/src/hooks/index.ts | 1 + panel/src/hooks/use-stuck-state.ts | 80 +++++++++++ panel/src/types/index.ts | 15 ++ 9 files changed, 412 insertions(+) create mode 100644 panel/src/components/tasks/__tests__/stuck-indicator.test.tsx create mode 100644 panel/src/components/tasks/stuck-indicator.tsx create mode 100644 panel/src/hooks/__tests__/use-stuck-state.test.ts create mode 100644 panel/src/hooks/use-stuck-state.ts diff --git a/panel/src/components/kanban/core/kanban-card.tsx b/panel/src/components/kanban/core/kanban-card.tsx index 04feb1cd0..1134f6b06 100644 --- a/panel/src/components/kanban/core/kanban-card.tsx +++ b/panel/src/components/kanban/core/kanban-card.tsx @@ -23,6 +23,7 @@ import { BlockedBadge } from "../shared/blocked-badge"; import { AssigneeAvatar } from "../shared/assignee-avatar"; import { AgentSelector } from "@/components/agents/agent-selector"; import { TaskTypeBadge } from "@/components/tasks/task-type-badge"; +import { StuckIndicator } from "@/components/tasks/stuck-indicator"; import { GripVertical, ArrowRight, @@ -175,6 +176,9 @@ function KanbanCardImpl({ )} {isBlocked && } + {/* Stuck chip — read-only wedge-ledger surfacing (strikes or + blocked-for-human); nothing renders on a healthy task. */} + {isBacklog && ( diff --git a/panel/src/components/tasks/__tests__/stuck-indicator.test.tsx b/panel/src/components/tasks/__tests__/stuck-indicator.test.tsx new file mode 100644 index 000000000..6d475b195 --- /dev/null +++ b/panel/src/components/tasks/__tests__/stuck-indicator.test.tsx @@ -0,0 +1,128 @@ +import { describe, it, expect } from "vitest"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { TaskStatus, type Task } from "@/types"; +import { StuckIndicator } from "../stuck-indicator"; + +// The backend slice ships in parallel — contract fields are mocked here, per +// the task brief. Only the fields this component reads are set. +function baseTask(overrides: Partial = {}): Task { + return { + id: "task-1", + title: "Task", + description: "", + acceptance_criteria: [], + status: TaskStatus.IN_PROGRESS, + priority: 2, + sequence: 0, + team: "frontend" as Task["team"], + created_by: "ceo", + assigned_to: null, + parent_task_id: null, + dependency_ids: [], + blocker_ids: [], + created_at: "2026-01-01T00:00:00Z", + updated_at: null, + claimed_at: null, + started_at: null, + completed_at: null, + target_date: null, + estimated_complexity: "medium" as Task["estimated_complexity"], + nature: "technical" as Task["nature"], + task_type: "code" as Task["task_type"], + project_id: null, + docs_complete: false, + pr_created: false, + pm_approvals: {}, + plan: null, + checkpoints: [], + progress_updates: [], + commits: [], + dev_notes: null, + qa_notes: null, + auditor_notes: null, + quick_context: null, + self_verified: false, + qa_verified: null, + branch_name: null, + pr_number: null, + pr_url: null, + ...overrides, + }; +} + +describe("StuckIndicator", () => { + it("renders nothing on a healthy task", () => { + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); + + it("shows active time since progress when wedge strikes are open", () => { + render( + , + ); + + const chip = screen.getByTestId("stuck-indicator"); + expect(chip).toHaveTextContent("stuck · 2h 5m since progress"); + }); + + it("renders without the time segment when no active time is present", () => { + render( + , + ); + expect(screen.getByTestId("stuck-indicator")).toHaveTextContent(/^stuck$/); + }); + + it("shows stuck for a blocked-for-human task without strikes", () => { + render( + , + ); + expect(screen.getByTestId("stuck-indicator")).toBeInTheDocument(); + }); + + it("does not show for a blocked-for-agent task", () => { + const { container } = render( + , + ); + expect(container).toBeEmptyDOMElement(); + }); + + it("tooltip lists the wedge cycle and states it is read-only", async () => { + const user = userEvent.setup(); + render( + , + ); + + await user.hover(screen.getByTestId("stuck-indicator")); + const tooltip = await screen.findByRole("tooltip"); + expect(tooltip).toHaveTextContent("1 open wedge strike"); + expect(tooltip).toHaveTextContent("main-pm → respawn"); + expect(tooltip).toHaveTextContent("Read-only"); + }); +}); diff --git a/panel/src/components/tasks/index.ts b/panel/src/components/tasks/index.ts index cff4d4548..985abd24d 100644 --- a/panel/src/components/tasks/index.ts +++ b/panel/src/components/tasks/index.ts @@ -1,4 +1,5 @@ export { TaskStatusBadge } from "./task-status-badge"; +export { StuckIndicator } from "./stuck-indicator"; export { CreateTaskDialog } from "./create-task-dialog"; export { TaskActions } from "./task-actions"; export { TaskFilters } from "./task-filters"; diff --git a/panel/src/components/tasks/stuck-indicator.tsx b/panel/src/components/tasks/stuck-indicator.tsx new file mode 100644 index 000000000..8dcdf5df1 --- /dev/null +++ b/panel/src/components/tasks/stuck-indicator.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { Hourglass } from "lucide-react"; + +import { + formatActiveTime, + useStuckState, +} from "@/hooks/use-stuck-state"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import type { Task } from "@/types"; + +/** + * Read-only stuck indicator driven by the wedge ledger: active time since the + * last progress-fingerprint movement plus the recorded actor/verb cycle. + * Renders nothing on a healthy task. Surfacing only — remediation decisions + * stay with the PM/CEO, so there are deliberately no actions here. + */ +export function StuckIndicator({ task }: { task: Task }) { + const { isStuck, activeTimeSinceProgress, wedgeCycle, reasons } = + useStuckState(task); + + if (!isStuck) return null; + + const activeTime = formatActiveTime(activeTimeSinceProgress); + + return ( + + + + + stuck + {activeTime ? ` · ${activeTime} since progress` : ""} + + + +

+ {reasons.join(" · ")} + {activeTime ? ` — active time since last progress movement: ${activeTime}` : ""} + . +

+ {wedgeCycle.length > 0 && ( +
+

Wedge cycle:

+ {wedgeCycle.map((entry, i) => ( +

+ {entry.actor} → {entry.verb} · {entry.timestamp} +

+ ))} +
+ )} +

+ Read-only — resolution is a PM/CEO decision. +

+
+
+ ); +} diff --git a/panel/src/components/tasks/task-detail/task-header.tsx b/panel/src/components/tasks/task-detail/task-header.tsx index 73c3dc628..3703d1cf5 100644 --- a/panel/src/components/tasks/task-detail/task-header.tsx +++ b/panel/src/components/tasks/task-detail/task-header.tsx @@ -57,6 +57,7 @@ import { } from "lucide-react"; import { toast } from "sonner"; import { TaskTypeBadge } from "../task-type-badge"; +import { StuckIndicator } from "../stuck-indicator"; import { CopyButton } from "@/components/ui/copy-button"; import { HelpTip } from "@/components/ui/help-tip"; import { @@ -709,6 +710,10 @@ export function TaskHeader({ task, onAction, nav }: TaskHeaderProps) { {/* Stalled chip - only when this task carries the backend's durable stalled marker (task.stalled_reason). */} + {/* Stuck chip — read-only wedge-ledger surfacing (strikes or + blocked-for-human); nothing renders on a healthy task. */} + + {stalledReason && ( diff --git a/panel/src/hooks/__tests__/use-stuck-state.test.ts b/panel/src/hooks/__tests__/use-stuck-state.test.ts new file mode 100644 index 000000000..17aa600a1 --- /dev/null +++ b/panel/src/hooks/__tests__/use-stuck-state.test.ts @@ -0,0 +1,118 @@ +import { describe, it, expect } from "vitest"; +import { TaskStatus, type Task } from "@/types"; +import { deriveStuckState, formatActiveTime } from "../use-stuck-state"; + +// Minimal task stub — the derivation only reads the wedge-contract fields +// plus status/blocker_resolver_type. +function baseTask(overrides: Partial = {}): Task { + return { + id: "task-1", + title: "Task", + description: "", + acceptance_criteria: [], + status: TaskStatus.IN_PROGRESS, + priority: 2, + sequence: 0, + team: "frontend" as Task["team"], + created_by: "ceo", + assigned_to: null, + parent_task_id: null, + dependency_ids: [], + blocker_ids: [], + created_at: "2026-01-01T00:00:00Z", + updated_at: null, + claimed_at: null, + started_at: null, + completed_at: null, + target_date: null, + estimated_complexity: "medium" as Task["estimated_complexity"], + nature: "technical" as Task["nature"], + task_type: "code" as Task["task_type"], + project_id: null, + docs_complete: false, + pr_created: false, + pm_approvals: {}, + plan: null, + checkpoints: [], + progress_updates: [], + commits: [], + dev_notes: null, + qa_notes: null, + auditor_notes: null, + quick_context: null, + self_verified: false, + qa_verified: null, + branch_name: null, + pr_number: null, + pr_url: null, + ...overrides, + }; +} + +describe("deriveStuckState", () => { + it("is not stuck on a healthy task", () => { + const state = deriveStuckState(baseTask()); + expect(state.isStuck).toBe(false); + expect(state.reasons).toEqual([]); + }); + + it("is stuck when open wedge strikes are present", () => { + const state = deriveStuckState( + baseTask({ open_wedge_strikes: 2, wedge_cycle: [] }), + ); + expect(state.isStuck).toBe(true); + expect(state.reasons).toEqual(["2 open wedge strikes"]); + }); + + it("is stuck when blocked for human resolution", () => { + const state = deriveStuckState( + baseTask({ + status: TaskStatus.BLOCKED, + blocker_resolver_type: "human", + }), + ); + expect(state.isStuck).toBe(true); + expect(state.reasons).toEqual(["blocked for human resolution"]); + }); + + it("is not stuck when blocked for an agent", () => { + const state = deriveStuckState( + baseTask({ + status: TaskStatus.BLOCKED, + blocker_resolver_type: "agent", + }), + ); + expect(state.isStuck).toBe(false); + }); + + it("returns the wedge cycle entries verbatim", () => { + const cycle = [{ actor: "main-pm", verb: "respawn", timestamp: "2026-09-27T00:00:00Z" }]; + const state = deriveStuckState( + baseTask({ open_wedge_strikes: 1, wedge_cycle: cycle }), + ); + expect(state.wedgeCycle).toEqual(cycle); + }); + + it("handles a null/undefined task", () => { + expect(deriveStuckState(null).isStuck).toBe(false); + expect(deriveStuckState(undefined).isStuck).toBe(false); + }); +}); + +describe("formatActiveTime", () => { + it("passes pre-formatted strings through", () => { + expect(formatActiveTime("2 days")).toBe("2 days"); + }); + + it("formats seconds into a coarse clock", () => { + expect(formatActiveTime(3600 * 26 + 60 * 5)).toBe("1d 2h"); + expect(formatActiveTime(60 * 14)).toBe("14m"); + expect(formatActiveTime(30)).toBe("30s"); + }); + + it("returns null for empty values", () => { + expect(formatActiveTime(null)).toBeNull(); + expect(formatActiveTime(undefined)).toBeNull(); + expect(formatActiveTime("")).toBeNull(); + }); +}); diff --git a/panel/src/hooks/index.ts b/panel/src/hooks/index.ts index a218e9bf6..75e3ccfac 100644 --- a/panel/src/hooks/index.ts +++ b/panel/src/hooks/index.ts @@ -1,3 +1,4 @@ +export * from "./use-stuck-state"; export * from "./use-tasks"; export * from "./use-verification"; export * from "./use-page-refresh"; diff --git a/panel/src/hooks/use-stuck-state.ts b/panel/src/hooks/use-stuck-state.ts new file mode 100644 index 000000000..2127f8912 --- /dev/null +++ b/panel/src/hooks/use-stuck-state.ts @@ -0,0 +1,80 @@ +"use client"; + +import { TaskStatus, type Task, type WedgeCycleEntry } from "@/types"; + +export interface StuckState { + /** True when the task carries open wedge strikes OR is blocked for a human. */ + isStuck: boolean; + /** Active time since the last progress-fingerprint movement (contract field). */ + activeTimeSinceProgress: string | number | null; + openWedgeStrikes: number; + wedgeCycle: WedgeCycleEntry[]; + /** Why the task is stuck — drives the indicator's tooltip. */ + reasons: string[]; +} + +// "Blocked for human" = the dispatcher must skip this task because only a +// human can resolve it (blocker_resolver_type="human" on a blocked status). +export function isTaskBlockedForHuman(task: Task): boolean { + return ( + task.status === TaskStatus.BLOCKED && task.blocker_resolver_type === "human" + ); +} + +// Pure stuck-state derivation from the task payload's wedge-ledger fields. +// Read-only by design: this surfaces state for the PM/CEO, it never acts. +export function deriveStuckState(task: Task | null | undefined): StuckState { + const openWedgeStrikes = task?.open_wedge_strikes ?? 0; + const wedgeCycle = task?.wedge_cycle ?? []; + const activeTimeSinceProgress = task?.active_time_since_progress ?? null; + const blockedForHuman = task ? isTaskBlockedForHuman(task) : false; + + const reasons: string[] = []; + if (openWedgeStrikes > 0) { + reasons.push( + `${openWedgeStrikes} open wedge strike${openWedgeStrikes === 1 ? "" : "s"}`, + ); + } + if (blockedForHuman) { + reasons.push("blocked for human resolution"); + } + + return { + isStuck: openWedgeStrikes > 0 || blockedForHuman, + activeTimeSinceProgress, + openWedgeStrikes, + wedgeCycle, + reasons, + }; +} + +// Active time since last progress movement. The backend may send seconds +// (number) or a pre-formatted string; display strings verbatim, format +// seconds as a coarse "1d 2h 3m" clock. +export function formatActiveTime( + value: string | number | null | undefined, +): string | null { + if (value == null || value === "") return null; + if (typeof value === "string") return value; + let seconds = Math.max(0, Math.floor(value)); + const days = Math.floor(seconds / 86400); + seconds -= days * 86400; + const hours = Math.floor(seconds / 3600); + seconds -= hours * 3600; + const minutes = Math.floor(seconds / 60); + seconds -= minutes * 60; + const parts: string[] = []; + if (days) parts.push(`${days}d`); + if (hours) parts.push(`${hours}h`); + if (minutes) parts.push(`${minutes}m`); + if (!parts.length) parts.push(`${seconds}s`); + return parts.slice(0, 2).join(" "); +} + +/** + * Stuck state for a task payload already fetched by the list/detail queries — + * no second fetch. Feeds the read-only StuckIndicator on cards and detail. + */ +export function useStuckState(task: Task | null | undefined): StuckState { + return deriveStuckState(task); +} diff --git a/panel/src/types/index.ts b/panel/src/types/index.ts index 58ba5fd92..3c32b4015 100644 --- a/panel/src/types/index.ts +++ b/panel/src/types/index.ts @@ -204,6 +204,14 @@ export interface SubTask { notes: string | null; } +// One recorded loop iteration in a task's wedge cycle: which actor ran +// which verb, and when. Rendered read-only by the stuck indicator. +export interface WedgeCycleEntry { + actor: string; + verb: string; + timestamp: string; +} + export interface TaskPlan { approach: string; sub_tasks: SubTask[]; @@ -242,6 +250,13 @@ export interface Task { // blocked. stalled_reason?: string | null; stalled_since?: string | null; + // Wedge-ledger stuck state (frozen cross-cell contract, additive on the + // task payload): active time since the last progress-fingerprint movement, + // count of open wedge strikes, and the recorded actor/verb/timestamp + // cycle. Read-only on the panel — surfaced, never acted on here. + active_time_since_progress?: string | number | null; + open_wedge_strikes?: number; + wedge_cycle?: WedgeCycleEntry[]; priority: number; // 0=P0(highest), 1=P1, 2=P2, 3=P3(lowest) // Cost cap (ROBOCO_TASK_BUDGETS_ENABLED). null = use the task-type default. budget_usd?: number | null; From bd5360e70827f0fbc3ced5e1d42fb7cf13730a3b Mon Sep 17 00:00:00 2001 From: Frontend Developer 1 Date: Sun, 27 Sep 2026 23:06:27 +0000 Subject: [PATCH 2/2] [99f61c8c] regenerate generated artifacts --- agents/prompts/_generated/auditor.md | 1 - agents/prompts/_generated/cell_pm.md | 2 - agents/prompts/_generated/developer.md | 3 -- agents/prompts/_generated/head_marketing.md | 1 - .../prompts/_generated/lifecycle-cell_pm.md | 1 - .../prompts/_generated/lifecycle-main_pm.md | 1 - agents/prompts/_generated/main_pm.md | 2 - agents/prompts/_generated/pr_reviewer.md | 1 - agents/prompts/_generated/product_owner.md | 1 - agents/prompts/_generated/qa.md | 2 - agents/prompts/_generated/verbs.md | 46 ---------------- docs/rag/lifecycle/intent-verbs.md | 44 +++++---------- docs/rag/lifecycle/status-transitions.md | 6 +-- panel/lib/lifecycle.json | 53 +++---------------- 14 files changed, 23 insertions(+), 141 deletions(-) diff --git a/agents/prompts/_generated/auditor.md b/agents/prompts/_generated/auditor.md index 5e64719ae..2fdc2699c 100644 --- a/agents/prompts/_generated/auditor.md +++ b/agents/prompts/_generated/auditor.md @@ -19,7 +19,6 @@ | `evidence` | `evidence(task_id: UUID)` | | `dm` | `dm(recipient: str, text: str, task_id: UUID | None = None, skill: str | None = None)` | | `read_a2a` | `read_a2a(see do_server)` | -| `task_time` | `task_time(task_id: UUID)` | | `approve_playbook` | `approve_playbook(playbook_id: UUID)` | | `reject_playbook` | `reject_playbook(playbook_id: UUID, reason: str)` | | `archive_playbook` | `archive_playbook(playbook_id: UUID)` | diff --git a/agents/prompts/_generated/cell_pm.md b/agents/prompts/_generated/cell_pm.md index 46a82ada5..5c54bd5a2 100644 --- a/agents/prompts/_generated/cell_pm.md +++ b/agents/prompts/_generated/cell_pm.md @@ -7,7 +7,6 @@ | Verb | Body schema | |------|-------------| -| `cancel_leaf` | `cancel_leaf(task_id: UUID, reason: str)` | | `complete` | `complete(task_id: UUID, notes: str)` | | `declare_coverage` | `declare_coverage(task_id: UUID, criteria: list[str])` | | `delegate` | `delegate(parent_task_id: UUID, title: str, description: str, assigned_to: str, team: str, task_type: str, nature: str, estimated_complexity: Complexity, acceptance_criteria: list[str], project_id: UUID | None = None, covers_parent_criteria: list[str] | None = None, intends_to_touch: list[str] | None = None, adds_migration: bool = False, touches_shared: bool = False, depends_on: list[UUID] | None = None)` | @@ -33,7 +32,6 @@ | `evidence` | `evidence(task_id: UUID)` | | `pr_update` | `pr_update(see do_server)` | | `draft_playbook` | `draft_playbook(title: str, problem: str, procedure: str, tags: list[str] = PydanticUndefined, source_task_id: UUID | None = None)` | -| `task_time` | `task_time(task_id: UUID)` | | `notify_list` | `notify_list(unread_only: bool = True, pending_ack_only: bool = False, limit: int = 20)` | | `notify_get` | `notify_get(notification_id: UUID)` | | `notify_ack` | `notify_ack(notification_id: UUID)` | diff --git a/agents/prompts/_generated/developer.md b/agents/prompts/_generated/developer.md index 62a341624..9b53c3fa3 100644 --- a/agents/prompts/_generated/developer.md +++ b/agents/prompts/_generated/developer.md @@ -30,10 +30,7 @@ | `draft_playbook` | `draft_playbook(title: str, problem: str, procedure: str, tags: list[str] = PydanticUndefined, source_task_id: UUID | None = None)` | | `propose_video` | `propose_video(composition_id: str, x_caption: str, tiktok_caption: str, platforms: list[str], input_props: str | Any | None = None)` | | `request_sandbox` | `request_sandbox(services: list[str] | None = None, extensions: str | list[str] | None = None)` | -| `run_sandbox_tests` | `run_sandbox_tests(command: str, image: str | None = None, timeout_seconds: int | None = None)` | | `request_render` | `request_render(composition_id: str | None = None, orientation: str = 'vertical', frame_count: int = 8, input_props: str | Any | None = None)` | -| `preflight_diff` | `preflight_diff(task_id: UUID)` | -| `triage_failure` | `triage_failure(task_id: UUID, test_name: str, error_excerpt: str = '')` | | `notify_list` | `notify_list(unread_only: bool = True, pending_ack_only: bool = False, limit: int = 20)` | | `notify_get` | `notify_get(notification_id: UUID)` | | `notify_ack` | `notify_ack(notification_id: UUID)` | diff --git a/agents/prompts/_generated/head_marketing.md b/agents/prompts/_generated/head_marketing.md index 8e4b9e9a4..85aa12688 100644 --- a/agents/prompts/_generated/head_marketing.md +++ b/agents/prompts/_generated/head_marketing.md @@ -21,7 +21,6 @@ | `notify` | `notify(target: str, text: str, priority: str = 'normal', task_id: UUID | None = None)` | | `evidence` | `evidence(task_id: UUID)` | | `nothing_to_propose` | `nothing_to_propose(task_id: UUID, reason: str)` | -| `task_time` | `task_time(task_id: UUID)` | | `notify_list` | `notify_list(unread_only: bool = True, pending_ack_only: bool = False, limit: int = 20)` | | `notify_get` | `notify_get(notification_id: UUID)` | | `notify_ack` | `notify_ack(notification_id: UUID)` | diff --git a/agents/prompts/_generated/lifecycle-cell_pm.md b/agents/prompts/_generated/lifecycle-cell_pm.md index e2230c94a..f88d4f6f2 100644 --- a/agents/prompts/_generated/lifecycle-cell_pm.md +++ b/agents/prompts/_generated/lifecycle-cell_pm.md @@ -3,7 +3,6 @@ These are the only verbs the gateway will accept from you. Calling any other verb will be rejected with a Decision telling you the right one. -- **cancel_leaf**: Close a zero-diff leaf: a delegated child whose findings a merged sibling already fixed, so no legitimate diff remains and i_am_done/complete can never accept it. Refuses a target with any children of its own (not a leaf), any commit ahead of its base, or an open PR - those go through the normal review path, not this. `reason` is recorded as your journal:decision and on the task's audit trail. Cell PM: only your own coordination task's children. Main PM: any root's descendant. - **complete**: Cell PM merges the PR (leaf into the cell branch, or the gated cell→root PR into the root branch) + transitions to completed; Main PM escalates the root to the CEO (who merges root→master). The merge runs BEFORE the complete transition: TaskService.complete asserts the PR is already merged, so the choreographer verb body (cell_pm_complete / main_pm_complete) owns the merge-first ordering — no trailing pr_merge side_effect is declared here. - **declare_coverage**: Stamp parent acceptance criteria onto an existing child's parent_ac_refs after the fact — for a replacement child whose delegate omitted covers_parent_criteria. Or, targeting your OWN root/coordination task, declare criteria as root-owned (only your own machinery satisfies them — never push these into a cell). No status change; the verb body owns ownership + criterion validation. - **delegate**: Create a subtask under the current task. Validates the delegation chain (main_pm->cell_pm; cell_pm->its team's devs) and the assignee-vs-task_type rule (Cell PMs get planning-typed tasks; devs get code/research, UX devs also design). documentation is NOT delegatable — the lifecycle auto-creates the doc phase after the code subtask passes QA. diff --git a/agents/prompts/_generated/lifecycle-main_pm.md b/agents/prompts/_generated/lifecycle-main_pm.md index 4d8233d4a..256201e0e 100644 --- a/agents/prompts/_generated/lifecycle-main_pm.md +++ b/agents/prompts/_generated/lifecycle-main_pm.md @@ -3,7 +3,6 @@ These are the only verbs the gateway will accept from you. Calling any other verb will be rejected with a Decision telling you the right one. -- **cancel_leaf**: Close a zero-diff leaf: a delegated child whose findings a merged sibling already fixed, so no legitimate diff remains and i_am_done/complete can never accept it. Refuses a target with any children of its own (not a leaf), any commit ahead of its base, or an open PR - those go through the normal review path, not this. `reason` is recorded as your journal:decision and on the task's audit trail. Cell PM: only your own coordination task's children. Main PM: any root's descendant. - **complete**: Cell PM merges the PR (leaf into the cell branch, or the gated cell→root PR into the root branch) + transitions to completed; Main PM escalates the root to the CEO (who merges root→master). The merge runs BEFORE the complete transition: TaskService.complete asserts the PR is already merged, so the choreographer verb body (cell_pm_complete / main_pm_complete) owns the merge-first ordering — no trailing pr_merge side_effect is declared here. - **declare_coverage**: Stamp parent acceptance criteria onto an existing child's parent_ac_refs after the fact — for a replacement child whose delegate omitted covers_parent_criteria. Or, targeting your OWN root/coordination task, declare criteria as root-owned (only your own machinery satisfies them — never push these into a cell). No status change; the verb body owns ownership + criterion validation. - **delegate**: Create a subtask under the current task. Validates the delegation chain (main_pm->cell_pm; cell_pm->its team's devs) and the assignee-vs-task_type rule (Cell PMs get planning-typed tasks; devs get code/research, UX devs also design). documentation is NOT delegatable — the lifecycle auto-creates the doc phase after the code subtask passes QA. diff --git a/agents/prompts/_generated/main_pm.md b/agents/prompts/_generated/main_pm.md index ae06e4a98..1e93b940f 100644 --- a/agents/prompts/_generated/main_pm.md +++ b/agents/prompts/_generated/main_pm.md @@ -7,7 +7,6 @@ | Verb | Body schema | |------|-------------| -| `cancel_leaf` | `cancel_leaf(task_id: UUID, reason: str)` | | `complete` | `complete(task_id: UUID, notes: str)` | | `declare_coverage` | `declare_coverage(task_id: UUID, criteria: list[str])` | | `delegate` | `delegate(parent_task_id: UUID, title: str, description: str, assigned_to: str, team: str, task_type: str, nature: str, estimated_complexity: Complexity, acceptance_criteria: list[str], project_id: UUID | None = None, covers_parent_criteria: list[str] | None = None, intends_to_touch: list[str] | None = None, adds_migration: bool = False, touches_shared: bool = False, depends_on: list[UUID] | None = None)` | @@ -34,7 +33,6 @@ | `evidence` | `evidence(task_id: UUID)` | | `pr_update` | `pr_update(see do_server)` | | `draft_playbook` | `draft_playbook(title: str, problem: str, procedure: str, tags: list[str] = PydanticUndefined, source_task_id: UUID | None = None)` | -| `task_time` | `task_time(task_id: UUID)` | | `notify_list` | `notify_list(unread_only: bool = True, pending_ack_only: bool = False, limit: int = 20)` | | `notify_get` | `notify_get(notification_id: UUID)` | | `notify_ack` | `notify_ack(notification_id: UUID)` | diff --git a/agents/prompts/_generated/pr_reviewer.md b/agents/prompts/_generated/pr_reviewer.md index 5bb211314..7bdae032f 100644 --- a/agents/prompts/_generated/pr_reviewer.md +++ b/agents/prompts/_generated/pr_reviewer.md @@ -26,4 +26,3 @@ | `read_a2a` | `read_a2a(see do_server)` | | `notify_list` | `notify_list(unread_only: bool = True, pending_ack_only: bool = False, limit: int = 20)` | | `notify_get` | `notify_get(notification_id: UUID)` | -| `task_time` | `task_time(task_id: UUID)` | diff --git a/agents/prompts/_generated/product_owner.md b/agents/prompts/_generated/product_owner.md index 11a09c207..bc0f086e0 100644 --- a/agents/prompts/_generated/product_owner.md +++ b/agents/prompts/_generated/product_owner.md @@ -21,7 +21,6 @@ | `notify` | `notify(target: str, text: str, priority: str = 'normal', task_id: UUID | None = None)` | | `evidence` | `evidence(task_id: UUID)` | | `nothing_to_propose` | `nothing_to_propose(task_id: UUID, reason: str)` | -| `task_time` | `task_time(task_id: UUID)` | | `notify_list` | `notify_list(unread_only: bool = True, pending_ack_only: bool = False, limit: int = 20)` | | `notify_get` | `notify_get(notification_id: UUID)` | | `notify_ack` | `notify_ack(notification_id: UUID)` | diff --git a/agents/prompts/_generated/qa.md b/agents/prompts/_generated/qa.md index 3110368a0..e45f876cb 100644 --- a/agents/prompts/_generated/qa.md +++ b/agents/prompts/_generated/qa.md @@ -25,9 +25,7 @@ | `evidence` | `evidence(task_id: UUID)` | | `draft_playbook` | `draft_playbook(title: str, problem: str, procedure: str, tags: list[str] = PydanticUndefined, source_task_id: UUID | None = None)` | | `request_sandbox` | `request_sandbox(services: list[str] | None = None, extensions: str | list[str] | None = None)` | -| `run_sandbox_tests` | `run_sandbox_tests(command: str, image: str | None = None, timeout_seconds: int | None = None)` | | `request_render` | `request_render(composition_id: str | None = None, orientation: str = 'vertical', frame_count: int = 8, input_props: str | Any | None = None)` | -| `triage_failure` | `triage_failure(task_id: UUID, test_name: str, error_excerpt: str = '')` | | `notify_list` | `notify_list(unread_only: bool = True, pending_ack_only: bool = False, limit: int = 20)` | | `notify_get` | `notify_get(notification_id: UUID)` | | `notify_ack` | `notify_ack(notification_id: UUID)` | diff --git a/agents/prompts/_generated/verbs.md b/agents/prompts/_generated/verbs.md index d7fc44563..c3870cb31 100644 --- a/agents/prompts/_generated/verbs.md +++ b/agents/prompts/_generated/verbs.md @@ -39,10 +39,7 @@ real tools live in their agent_sdk drivers, not role_config. | `draft_playbook` | `draft_playbook(title: str, problem: str, procedure: str, tags: list[str] = PydanticUndefined, source_task_id: UUID | None = None)` | | `propose_video` | `propose_video(composition_id: str, x_caption: str, tiktok_caption: str, platforms: list[str], input_props: str | Any | None = None)` | | `request_sandbox` | `request_sandbox(services: list[str] | None = None, extensions: str | list[str] | None = None)` | -| `run_sandbox_tests` | `run_sandbox_tests(command: str, image: str | None = None, timeout_seconds: int | None = None)` | | `request_render` | `request_render(composition_id: str | None = None, orientation: str = 'vertical', frame_count: int = 8, input_props: str | Any | None = None)` | -| `preflight_diff` | `preflight_diff(task_id: UUID)` | -| `triage_failure` | `triage_failure(task_id: UUID, test_name: str, error_excerpt: str = '')` | | `notify_list` | `notify_list(unread_only: bool = True, pending_ack_only: bool = False, limit: int = 20)` | | `notify_get` | `notify_get(notification_id: UUID)` | | `notify_ack` | `notify_ack(notification_id: UUID)` | @@ -73,9 +70,7 @@ real tools live in their agent_sdk drivers, not role_config. | `evidence` | `evidence(task_id: UUID)` | | `draft_playbook` | `draft_playbook(title: str, problem: str, procedure: str, tags: list[str] = PydanticUndefined, source_task_id: UUID | None = None)` | | `request_sandbox` | `request_sandbox(services: list[str] | None = None, extensions: str | list[str] | None = None)` | -| `run_sandbox_tests` | `run_sandbox_tests(command: str, image: str | None = None, timeout_seconds: int | None = None)` | | `request_render` | `request_render(composition_id: str | None = None, orientation: str = 'vertical', frame_count: int = 8, input_props: str | Any | None = None)` | -| `triage_failure` | `triage_failure(task_id: UUID, test_name: str, error_excerpt: str = '')` | | `notify_list` | `notify_list(unread_only: bool = True, pending_ack_only: bool = False, limit: int = 20)` | | `notify_get` | `notify_get(notification_id: UUID)` | | `notify_ack` | `notify_ack(notification_id: UUID)` | @@ -119,7 +114,6 @@ real tools live in their agent_sdk drivers, not role_config. | Verb | Body schema | |------|-------------| -| `cancel_leaf` | `cancel_leaf(task_id: UUID, reason: str)` | | `complete` | `complete(task_id: UUID, notes: str)` | | `declare_coverage` | `declare_coverage(task_id: UUID, criteria: list[str])` | | `delegate` | `delegate(parent_task_id: UUID, title: str, description: str, assigned_to: str, team: str, task_type: str, nature: str, estimated_complexity: Complexity, acceptance_criteria: list[str], project_id: UUID | None = None, covers_parent_criteria: list[str] | None = None, intends_to_touch: list[str] | None = None, adds_migration: bool = False, touches_shared: bool = False, depends_on: list[UUID] | None = None)` | @@ -145,7 +139,6 @@ real tools live in their agent_sdk drivers, not role_config. | `evidence` | `evidence(task_id: UUID)` | | `pr_update` | `pr_update(see do_server)` | | `draft_playbook` | `draft_playbook(title: str, problem: str, procedure: str, tags: list[str] = PydanticUndefined, source_task_id: UUID | None = None)` | -| `task_time` | `task_time(task_id: UUID)` | | `notify_list` | `notify_list(unread_only: bool = True, pending_ack_only: bool = False, limit: int = 20)` | | `notify_get` | `notify_get(notification_id: UUID)` | | `notify_ack` | `notify_ack(notification_id: UUID)` | @@ -158,7 +151,6 @@ real tools live in their agent_sdk drivers, not role_config. | Verb | Body schema | |------|-------------| -| `cancel_leaf` | `cancel_leaf(task_id: UUID, reason: str)` | | `complete` | `complete(task_id: UUID, notes: str)` | | `declare_coverage` | `declare_coverage(task_id: UUID, criteria: list[str])` | | `delegate` | `delegate(parent_task_id: UUID, title: str, description: str, assigned_to: str, team: str, task_type: str, nature: str, estimated_complexity: Complexity, acceptance_criteria: list[str], project_id: UUID | None = None, covers_parent_criteria: list[str] | None = None, intends_to_touch: list[str] | None = None, adds_migration: bool = False, touches_shared: bool = False, depends_on: list[UUID] | None = None)` | @@ -185,7 +177,6 @@ real tools live in their agent_sdk drivers, not role_config. | `evidence` | `evidence(task_id: UUID)` | | `pr_update` | `pr_update(see do_server)` | | `draft_playbook` | `draft_playbook(title: str, problem: str, procedure: str, tags: list[str] = PydanticUndefined, source_task_id: UUID | None = None)` | -| `task_time` | `task_time(task_id: UUID)` | | `notify_list` | `notify_list(unread_only: bool = True, pending_ack_only: bool = False, limit: int = 20)` | | `notify_get` | `notify_get(notification_id: UUID)` | | `notify_ack` | `notify_ack(notification_id: UUID)` | @@ -212,7 +203,6 @@ real tools live in their agent_sdk drivers, not role_config. | `notify` | `notify(target: str, text: str, priority: str = 'normal', task_id: UUID | None = None)` | | `evidence` | `evidence(task_id: UUID)` | | `nothing_to_propose` | `nothing_to_propose(task_id: UUID, reason: str)` | -| `task_time` | `task_time(task_id: UUID)` | | `notify_list` | `notify_list(unread_only: bool = True, pending_ack_only: bool = False, limit: int = 20)` | | `notify_get` | `notify_get(notification_id: UUID)` | | `notify_ack` | `notify_ack(notification_id: UUID)` | @@ -244,7 +234,6 @@ real tools live in their agent_sdk drivers, not role_config. | `notify` | `notify(target: str, text: str, priority: str = 'normal', task_id: UUID | None = None)` | | `evidence` | `evidence(task_id: UUID)` | | `nothing_to_propose` | `nothing_to_propose(task_id: UUID, reason: str)` | -| `task_time` | `task_time(task_id: UUID)` | | `notify_list` | `notify_list(unread_only: bool = True, pending_ack_only: bool = False, limit: int = 20)` | | `notify_get` | `notify_get(notification_id: UUID)` | | `notify_ack` | `notify_ack(notification_id: UUID)` | @@ -275,7 +264,6 @@ real tools live in their agent_sdk drivers, not role_config. | `evidence` | `evidence(task_id: UUID)` | | `dm` | `dm(recipient: str, text: str, task_id: UUID | None = None, skill: str | None = None)` | | `read_a2a` | `read_a2a(see do_server)` | -| `task_time` | `task_time(task_id: UUID)` | | `approve_playbook` | `approve_playbook(playbook_id: UUID)` | | `reject_playbook` | `reject_playbook(playbook_id: UUID, reason: str)` | | `archive_playbook` | `archive_playbook(playbook_id: UUID)` | @@ -312,38 +300,4 @@ real tools live in their agent_sdk drivers, not role_config. | `read_a2a` | `read_a2a(see do_server)` | | `notify_list` | `notify_list(unread_only: bool = True, pending_ack_only: bool = False, limit: int = 20)` | | `notify_get` | `notify_get(notification_id: UUID)` | -| `task_time` | `task_time(task_id: UUID)` | - -## devops - -### Flow verbs - -| Verb | Body schema | -|------|-------------| -| `claim_gate_review` | `claim_gate_review(task_id: UUID)` | -| `give_me_work` | `give_me_work()` | -| `i_am_done` | `i_am_done(task_id: UUID, notes: str = '', resolved_findings: list[ResolvedFindingInput] = PydanticUndefined)` | -| `i_am_idle` | `i_am_idle()` | -| `i_will_work_on` | `i_will_work_on(task_id: UUID, plan: str | None = None, steps: list[str | str] = PydanticUndefined, technical_considerations: list[str] = PydanticUndefined, risks: list[str | str] = PydanticUndefined, open_questions: list[str | str | bool] = PydanticUndefined)` | -| `open_pr` | `open_pr(task_id: UUID)` | -| `pr_fail` | `pr_fail(task_id: UUID, issues: list[str] = PydanticUndefined, findings: list[str | Any] = PydanticUndefined)` | -| `pr_pass` | `pr_pass(task_id: UUID, notes: str)` | -| `record_devops_review` | `record_devops_review(task_id: UUID, notes: str)` | -| `sync_branch` | `sync_branch(task_id: UUID, stash: bool = False)` | -| `unclaim` | `unclaim(task_id: UUID)` | - -### Content (do) tools - -| Tool | Body schema | -|------|-------------| -| `commit` | `commit(message: str, files: list[str] | None = None)` | -| `note` | `note(text: str, scope: str = 'note', task_id: UUID | None = None, title: str | None = None, context: str = '', options: list[str | str] | None = None, chosen: str = '', rationale: str = '', consequences: list[str] | None = None, what_done: str = '', what_learned: str = '', what_struggled: str = '', next_steps: list[str] | None = None, section: str | Any | None = None, done: str = '', next: str = '', where_to_look: list[str] | None = None)` | -| `evidence` | `evidence(task_id: UUID)` | -| `dm` | `dm(recipient: str, text: str, task_id: UUID | None = None, skill: str | None = None)` | -| `read_messages` | `read_messages()` | -| `read_a2a` | `read_a2a(see do_server)` | -| `notify_list` | `notify_list(unread_only: bool = True, pending_ack_only: bool = False, limit: int = 20)` | -| `notify_get` | `notify_get(notification_id: UUID)` | -| `notify_ack` | `notify_ack(notification_id: UUID)` | -| `task_time` | `task_time(task_id: UUID)` | diff --git a/docs/rag/lifecycle/intent-verbs.md b/docs/rag/lifecycle/intent-verbs.md index ed72bfb19..d7558b979 100644 --- a/docs/rag/lifecycle/intent-verbs.md +++ b/docs/rag/lifecycle/intent-verbs.md @@ -1,16 +1,5 @@ # Intent Verbs (gateway-facing surface) -## cancel_leaf - -Close a zero-diff leaf: a delegated child whose findings a merged sibling already fixed, so no legitimate diff remains and i_am_done/complete can never accept it. Refuses a target with any children of its own (not a leaf), any commit ahead of its base, or an open PR - those go through the normal review path, not this. `reason` is recorded as your journal:decision and on the task's audit trail. Cell PM: only your own coordination task's children. Main PM: any root's descendant. - -**Allowed roles:** cell_pm, main_pm - -**Composes:** (no atomic actions) - -**Preconditions:** non_terminal - - ## claim_doc_task Claim awaiting_documentation. Returns evidence inline. @@ -24,7 +13,7 @@ Claim awaiting_documentation. Returns evidence inline. Claim an assembled-PR review task (awaiting_pr_review) WITHOUT transitioning it — mirrors QA's claim_review. The assembled diff and the parent task's acceptance criteria are returned inline. -**Allowed roles:** devops, pr_reviewer +**Allowed roles:** pr_reviewer **Composes:** (no atomic actions) @@ -109,7 +98,7 @@ Fail QA with concrete issues. Transitions to needs_revision. Return your most-actionable task or signal idle. -**Allowed roles:** cell_pm, developer, devops, documenter, main_pm, pr_reviewer, qa +**Allowed roles:** cell_pm, developer, documenter, main_pm, pr_reviewer, qa **Composes:** (no atomic actions) @@ -127,7 +116,7 @@ Escalate to PM. Logs a struggle journal entry. Submit work for QA. Auto-runs in_progress->verifying then verifying->awaiting_qa. Strict - PR must be open (call open_pr first) and >=1 commit. -**Allowed roles:** developer, devops +**Allowed roles:** developer **Composes:** submit_verification → submit_qa @@ -138,7 +127,7 @@ Submit work for QA. Auto-runs in_progress->verifying then verifying->awaiting_qa Signal you have no active work. PMs auto-pause owned in_progress tasks. -**Allowed roles:** auditor, cell_pm, developer, devops, documenter, head_marketing, main_pm, pr_reviewer, product_owner, prompter, qa, secretary +**Allowed roles:** auditor, cell_pm, developer, documenter, head_marketing, main_pm, pr_reviewer, product_owner, prompter, qa, secretary **Composes:** (no atomic actions) @@ -158,7 +147,7 @@ PM mirror of i_will_work_on for parent tasks. Claim, plan, transition to in_prog **Allowed roles:** cell_pm, main_pm -**Composes:** claim → set_plan +**Composes:** claim → set_plan → start **Preconditions:** plan @@ -167,9 +156,9 @@ PM mirror of i_will_work_on for parent tasks. Claim, plan, transition to in_prog Claim a task, set the plan, and transition to in_progress. Atomic - preconditions checked before any state mutation. -**Allowed roles:** developer, devops +**Allowed roles:** developer -**Composes:** claim → set_plan +**Composes:** claim → set_plan → start **Preconditions:** plan @@ -178,7 +167,7 @@ Claim a task, set the plan, and transition to in_progress. Atomic - precondition Push the branch and open a PR. Atomic - preconditions (assignee, >=1 commit, no prior PR) checked BEFORE any git operation. After success, call i_am_done. -**Allowed roles:** developer, devops +**Allowed roles:** developer **Composes:** (no atomic actions) @@ -209,7 +198,7 @@ Post one complete change-request to the external PR and finish the review task. Fail the assembled-PR review with concrete issues. Transitions awaiting_pr_review -> needs_revision, routed back like a QA fail. -**Allowed roles:** devops, pr_reviewer +**Allowed roles:** pr_reviewer **Composes:** pr_fail @@ -218,7 +207,7 @@ Fail the assembled-PR review with concrete issues. Transitions awaiting_pr_revie Pass the assembled-PR review. Transitions awaiting_pr_review -> awaiting_pm_review so the PM can merge. -**Allowed roles:** devops, pr_reviewer +**Allowed roles:** pr_reviewer **Composes:** pr_pass @@ -232,15 +221,6 @@ Hand a claimed/in_progress task to another developer in your own cell. The branc **Composes:** (no atomic actions) -## record_devops_review - -Record the DevOps infra-review verdict (pass) on an assembled-PR gate task WITHOUT transitioning it: the primary reviewer's pr_pass then composes. Use pr_fail instead to reject the PR. - -**Allowed roles:** devops - -**Composes:** (no atomic actions) - - ## request_changes Reject the merge review with concrete issues. Transitions awaiting_pm_review -> needs_revision, routed back like a QA fail (original developer for a leaf, revision PM for an assembled task). Use this for an AC/scope violation caught at merge review — never i_am_blocked/escalate, which have no revision routing. @@ -287,7 +267,7 @@ Cell PM opens the cell→root PR and moves the cell task into the PR-review gate Rebase your task's branch onto its current base THROUGH the gate (raw git is denied). Use when your branch has fallen behind its base — e.g. a sibling task's PR merged into the parent branch while you worked. Fetches origin, rebases head onto base, and force-pushes (with-lease). No DB state change. On conflicts the rebase is aborted and the conflicted files are returned — resolve by hand, commit, then sync_branch again. Pass stash=True to auto-stash uncommitted changes instead of refusing DIRTY_WORKSPACE; they are restored after the rebase. -**Allowed roles:** developer, devops +**Allowed roles:** developer **Composes:** (no atomic actions) @@ -325,7 +305,7 @@ PM unblocks a blocked task; restores pre-block state. Voluntarily release a claim back to pending. The work-in-progress branch is preserved. A PR reviewer who claimed an external review (in_progress) or a gate review (awaiting_pr_review) and cannot finish releases the claim here rather than wedging the lane until the stale-claim reaper. -**Allowed roles:** cell_pm, developer, devops, documenter, main_pm, pr_reviewer, qa +**Allowed roles:** cell_pm, developer, documenter, main_pm, pr_reviewer, qa **Composes:** (no atomic actions) diff --git a/docs/rag/lifecycle/status-transitions.md b/docs/rag/lifecycle/status-transitions.md index 766bdba3d..d7f39fdf1 100644 --- a/docs/rag/lifecycle/status-transitions.md +++ b/docs/rag/lifecycle/status-transitions.md @@ -13,10 +13,10 @@ | awaiting_pm_review | cancelled | cancel | cell_pm, ceo, main_pm | | awaiting_pm_review | completed | complete | cell_pm, main_pm | | awaiting_pm_review | needs_revision | request_changes | cell_pm, main_pm | -| awaiting_pr_review | awaiting_pm_review | pr_pass | devops, pr_reviewer | +| awaiting_pr_review | awaiting_pm_review | pr_pass | pr_reviewer | | awaiting_pr_review | cancelled | cancel | cell_pm, ceo, main_pm | -| awaiting_pr_review | claimed | claim | devops, pr_reviewer | -| awaiting_pr_review | needs_revision | pr_fail | devops, pr_reviewer | +| awaiting_pr_review | claimed | claim | pr_reviewer | +| awaiting_pr_review | needs_revision | pr_fail | pr_reviewer | | awaiting_qa | awaiting_documentation | qa_pass | qa | | awaiting_qa | cancelled | cancel | cell_pm, ceo, main_pm | | awaiting_qa | claimed | claim | qa | diff --git a/panel/lib/lifecycle.json b/panel/lib/lifecycle.json index e6d93f12f..c673a92ee 100644 --- a/panel/lib/lifecycle.json +++ b/panel/lib/lifecycle.json @@ -10,11 +10,6 @@ "needs_revision", "pending" ], - "devops": [ - "awaiting_pr_review", - "needs_revision", - "pending" - ], "documenter": [ "awaiting_documentation", "pending" @@ -34,17 +29,6 @@ ] }, "intents": [ - { - "allowed_roles": [ - "cell_pm", - "main_pm" - ], - "composes": [], - "description": "Close a zero-diff leaf: a delegated child whose findings a merged sibling already fixed, so no legitimate diff remains and i_am_done/complete can never accept it. Refuses a target with any children of its own (not a leaf), any commit ahead of its base, or an open PR - those go through the normal review path, not this. `reason` is recorded as your journal:decision and on the task's audit trail. Cell PM: only your own coordination task's children. Main PM: any root's descendant.", - "name": "cancel_leaf", - "pre_side_effects": [], - "side_effects": [] - }, { "allowed_roles": [ "documenter" @@ -57,7 +41,6 @@ }, { "allowed_roles": [ - "devops", "pr_reviewer" ], "composes": [], @@ -167,7 +150,6 @@ "allowed_roles": [ "cell_pm", "developer", - "devops", "documenter", "main_pm", "pr_reviewer", @@ -195,8 +177,7 @@ }, { "allowed_roles": [ - "developer", - "devops" + "developer" ], "composes": [ "submit_verification", @@ -212,7 +193,6 @@ "auditor", "cell_pm", "developer", - "devops", "documenter", "head_marketing", "main_pm", @@ -247,7 +227,8 @@ ], "composes": [ "claim", - "set_plan" + "set_plan", + "start" ], "description": "PM mirror of i_will_work_on for parent tasks. Claim, plan, transition to in_progress; from there delegate subtasks.", "name": "i_will_plan", @@ -256,12 +237,12 @@ }, { "allowed_roles": [ - "developer", - "devops" + "developer" ], "composes": [ "claim", - "set_plan" + "set_plan", + "start" ], "description": "Claim a task, set the plan, and transition to in_progress. Atomic - preconditions checked before any state mutation.", "name": "i_will_work_on", @@ -270,8 +251,7 @@ }, { "allowed_roles": [ - "developer", - "devops" + "developer" ], "composes": [], "description": "Push the branch and open a PR. Atomic - preconditions (assignee, >=1 commit, no prior PR) checked BEFORE any git operation. After success, call i_am_done.", @@ -308,7 +288,6 @@ }, { "allowed_roles": [ - "devops", "pr_reviewer" ], "composes": [ @@ -321,7 +300,6 @@ }, { "allowed_roles": [ - "devops", "pr_reviewer" ], "composes": [ @@ -342,16 +320,6 @@ "pre_side_effects": [], "side_effects": [] }, - { - "allowed_roles": [ - "devops" - ], - "composes": [], - "description": "Record the DevOps infra-review verdict (pass) on an assembled-PR gate task WITHOUT transitioning it: the primary reviewer's pr_pass then composes. Use pr_fail instead to reject the PR.", - "name": "record_devops_review", - "pre_side_effects": [], - "side_effects": [] - }, { "allowed_roles": [ "cell_pm", @@ -411,8 +379,7 @@ }, { "allowed_roles": [ - "developer", - "devops" + "developer" ], "composes": [], "description": "Rebase your task's branch onto its current base THROUGH the gate (raw git is denied). Use when your branch has fallen behind its base \u2014 e.g. a sibling task's PR merged into the parent branch while you worked. Fetches origin, rebases head onto base, and force-pushes (with-lease). No DB state change. On conflicts the rebase is aborted and the conflicted files are returned \u2014 resolve by hand, commit, then sync_branch again. Pass stash=True to auto-stash uncommitted changes instead of refusing DIRTY_WORKSPACE; they are restored after the rebase.", @@ -461,7 +428,6 @@ "allowed_roles": [ "cell_pm", "developer", - "devops", "documenter", "main_pm", "pr_reviewer", @@ -584,7 +550,6 @@ { "action": "pr_pass", "roles": [ - "devops", "pr_reviewer" ], "source": "awaiting_pr_review", @@ -603,7 +568,6 @@ { "action": "claim", "roles": [ - "devops", "pr_reviewer" ], "source": "awaiting_pr_review", @@ -612,7 +576,6 @@ { "action": "pr_fail", "roles": [ - "devops", "pr_reviewer" ], "source": "awaiting_pr_review",