## What Plan Review, planning, and the replan loop move from the implementation column into the **planning lane** (`todo`), so a task under specification never holds a WIP slot. The card crosses into `in-progress` exactly once, at `parse`, released by the scheduler. Operators also finally see a **Plan Review** badge while the gate runs — it was previously invisible on the default workflow. ## The part that made it possible Moving the node is ten lines. It was attempted three times and reverted each time, because a graph run with no durable continuation replayed from `start` and dragged an in-progress card *backward* out of the WIP column, firing `abort-on-exit` and stranding it in a pre-WIP column with no releaser. So this PR adds the graph **entry contract** — `resolveColumnResumeNode`: | Card is in | Resumes at | |---|---| | `triage` | `start` | | `todo` | `plan` | | `in-progress` | `parse` — never re-plans, never moves backward | | `in-review` | first review node — gates are not skipped | `ir.columns` is ordered and that order is the lifecycle order; rework and failure edges are excluded so the entry point is always the main path. The proof it's the right fix: **`executor-task-done-invariant` passes unmodified** after failing every previous attempt. ## Also in here - **Release gate narrowed twice.** `isUnplannedForExecution` applies its pre-release plan-review gate only when the node's column equals the card's column *and* the group is enabled for the task. The enablement check fixes a real deadlock — a task with Plan Review toggled off was held forever waiting for evidence nothing would ever write. - **Badge cleanup.** Gate badge reads "Plan Review" instead of the ambiguous "Reviewing" and no longer hides behind a lane restriction; the status badge stops duplicating it; `planning` renders as "Planning" instead of the raw engine token. - **Coding (Ideas)** renames its planner column to "Planning" (id `todo` unchanged) and loses its private planning-node re-home — the graph it clones is already plan-in-place. - **New sweep** `reconcileUndeclaredTaskColumns` re-homes a row whose column its workflow no longer declares. Written for a follow-up, kept because it makes any column edit survivable. ## Test changes Scheduler and release fixtures now model a card whose Plan Review passed — the state every real card is in when the capacity sweep sees it. A held unreviewed card is the gate working, and that path stays owned by `pre-release-plan-review.test.ts`. New `workflow-graph-entry-contract.test.ts` covers the invariant at every lifecycle position, plus the gap-column and remediation-node cases. ## Verification Gate 299 + 70 + 10, dashboard badge suites 672, engine workflow/entry/executor suites 147, core 122. Lint and typecheck clean. Full engine suite sits at the pre-existing baseline (notifier / plugin-runner / notification-service, untouched by this). ## Follow-up Removing the Todo column entirely is a separate ~207-site lifecycle-vocabulary refactor — planned in `docs/plans/2026-07-26-001-refactor-workflow-owned-lifecycle-plan.md` (companion docs PR). 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Plan Review now runs in the Planning lane before implementation begins. * Cards resume from their current workflow column without replaying earlier steps. * Added automatic recovery for cards stranded in outdated workflow columns. * **Improvements** * Renamed the Coding (Ideas) planner column to “Planning.” * Refined Plan Review gating to respect enabled settings and the card’s current column. * Updated planning and Plan Review badges for clearer, consistent labels across cards and lists. * **Bug Fixes** * Improved workflow transitions and release behavior around planning, review, and execution. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
76 lines
2.9 KiB
TypeScript
76 lines
2.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { TFunction } from "i18next";
|
|
import { getTaskStatusBadgeLabel, hasTaskStatusBadge } from "../taskStatusBadgeLabel";
|
|
|
|
const t = ((key: string, fallback?: string) => fallback ?? key) as TFunction<"app">;
|
|
|
|
describe("hasTaskStatusBadge", () => {
|
|
it.each([
|
|
"planning",
|
|
"executing",
|
|
"reviewing",
|
|
"merging",
|
|
"failed",
|
|
"needs-replan",
|
|
"done",
|
|
])("keeps a real status visible regardless of column placement: %s", (status) => {
|
|
expect(hasTaskStatusBadge(status)).toBe(true);
|
|
});
|
|
|
|
it("leaves null, undefined, and empty status badge-free", () => {
|
|
expect(hasTaskStatusBadge(null)).toBe(false);
|
|
expect(hasTaskStatusBadge(undefined)).toBe(false);
|
|
expect(hasTaskStatusBadge(" ")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("getTaskStatusBadgeLabel", () => {
|
|
it("maps the full AI merge pipeline to Merging…", () => {
|
|
for (const status of ["merging", "merging-pr", "reviewing", "landing"]) {
|
|
expect(getTaskStatusBadgeLabel(status, t)).toBe("Merging…");
|
|
}
|
|
});
|
|
|
|
it("keeps merging-fix distinct", () => {
|
|
expect(getTaskStatusBadgeLabel("merging-fix", t)).toBe("Merging fixes…");
|
|
});
|
|
|
|
it("keeps merging-fix over a still-running workflow-step label", () => {
|
|
// A pre-merge step's running state can survive into a merge-fix retry; the badge must not regress to the step name.
|
|
expect(getTaskStatusBadgeLabel("merging-fix", t, "Plan Review")).toBe("Merging fixes…");
|
|
});
|
|
|
|
it("keeps every active-merge status over a still-running workflow-step label", () => {
|
|
// The same stale startedAt-without-completedAt step state can survive into the whole merge pipeline.
|
|
for (const status of ["merging", "merging-pr", "reviewing", "landing"]) {
|
|
expect(getTaskStatusBadgeLabel(status, t, "Code Review")).toBe("Merging…");
|
|
}
|
|
});
|
|
|
|
it("lets a running workflow-step label override other statuses", () => {
|
|
expect(getTaskStatusBadgeLabel("planning", t, "Plan Review")).toBe("Plan Review");
|
|
expect(getTaskStatusBadgeLabel("needs-replan", t, "Plan Review")).toBe("Plan Review");
|
|
});
|
|
|
|
it("maps needs-replan to the operator-facing Revising label", () => {
|
|
const label = getTaskStatusBadgeLabel("needs-replan", t);
|
|
expect(label).toBe("Revising");
|
|
expect(label).not.toBe("Replan");
|
|
});
|
|
|
|
/*
|
|
FNXC:TaskStatusBadge 2026-07-26-14:05:
|
|
With the Plan Review gate badge naming itself, callers drop the workflow-step override while it
|
|
renders — so this branch is now what a planning card actually reads, and it must be operator copy
|
|
rather than the raw engine token it used to expose.
|
|
*/
|
|
it("maps the planning status to operator copy, not the engine token", () => {
|
|
expect(getTaskStatusBadgeLabel("planning", t)).toBe("Planning");
|
|
});
|
|
|
|
it("passes through non-merge statuses", () => {
|
|
expect(getTaskStatusBadgeLabel("failed", t)).toBe("failed");
|
|
expect(getTaskStatusBadgeLabel(null, t)).toBe("");
|
|
});
|
|
});
|