U11: rank assigned work by lifecycle role (1 -> 0), plus two documented non-conversions (#2563)
Based on `main`. Continuing with unassigned work in my area (scheduling/ranking core). ## Measured (drift-review tracking) | file | comparisons before | after | |---|---:|---:| | `packages/core/src/assigned-task-ranking.ts` | **1** | **0** | ## What was wrong `tierForTask` identified the two **actionable** tiers by literal id — `in-progress` → `in_progress`, `todo` → `ready_todo` / `partial_blocked`. The file's own comment already recorded half of this: > Only treating default `todo`/`in-progress` as titled hid assigned work as a bare count But the fix that followed was a **floor, not a fix**: unrecognised columns fall to `other` so work stays *visible*, while a renamed hold column loses `ready_todo` and `partial_blocked` entirely. Work that is genuinely ready to start then ranks **below everything already in progress**, so an agent reading its Wake Delta sees ready work buried. Nothing errors and nothing disappears — the ordering is just wrong, which is how it survived a comment that noticed the adjacent problem. `partial_blocked` is the sharper loss: it's the **only** tier distinguishing "ready" from "waiting on a dependency" for hold-column cards, and it was unreachable for any renamed workflow. ## Two sibling files deliberately NOT converted Checked before assuming work existed: **`live-agent-count.ts` — already trait-driven.** Its literals are the else-branch of `flags ? traits : literals`, and the source says why: *"The literal fallback is fixture-only; board/store callers always supply flags/IR."* Converting a fixture-only fallback would be churn. **`task-priority.ts` → `sortTasksForDisplayColumn` — dead.** No production caller. The dashboard has its own independent implementation in `app/components/taskSorting.ts` with a richer signature (`doneSortMode`, `isArchivedColumn`), and that's the one `Lane.tsx` imports. Core's copy is reached only by its own tests and the barrel export. That's the **third dead export** this unit has found by checking reachability before converting (after the legacy dispatcher and `isRunnableQueuedOverlapCandidate`). Deletion is a separate concern from conversion and is not in this PR. ## Verification - **Mutation-verified:** not threading `roles` through to `tierForTask` fails **4 of 6** new tests - 13 tests green (6 new + the pre-existing ranking suite) - merge gate green (414 + 10 + 71), tsc clean, lint clean No changeset: `@fusion/core` is private. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-29-13:20 (U11 conversion — assigned-task ranking):
|
||||
|
||||
`tierForTask` ranks an agent's assigned work for the Wake Delta inventory, and it
|
||||
identified the two actionable tiers by literal id: `in-progress` -> `in_progress`,
|
||||
`todo` -> `ready_todo` / `partial_blocked`.
|
||||
|
||||
The file's own comment already documents the consequence for custom workflows —
|
||||
"Only treating default `todo`/`in-progress` as titled hid assigned work as a bare
|
||||
count" — and then fixes it only halfway: unrecognised columns fall to the `other`
|
||||
tier so they stay VISIBLE, but a renamed hold column loses `ready_todo` entirely.
|
||||
|
||||
That is the failure this converts: a card that is genuinely ready to start is
|
||||
ranked at the lowest tier, below every in-progress card, so an agent reading its
|
||||
Wake Delta sees ready work buried under work already underway. Nothing errors and
|
||||
nothing disappears — the ordering is just wrong, which is why it survived.
|
||||
|
||||
`partial_blocked` matters too: it is the ONLY tier that distinguishes "ready" from
|
||||
"waiting on a dependency" for hold-column cards, and it is unreachable for a
|
||||
renamed workflow.
|
||||
|
||||
Written against the literal implementation and observed FAILING first.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { rankAssignedTasksForWakeDelta } from "../assigned-task-ranking.js";
|
||||
|
||||
type Ranked = ReturnType<typeof rankAssignedTasksForWakeDelta>;
|
||||
|
||||
function task(over: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: "FN-1",
|
||||
title: "t",
|
||||
column: "drafting",
|
||||
paused: false,
|
||||
deletedAt: null,
|
||||
dependencies: [],
|
||||
...over,
|
||||
} as never;
|
||||
}
|
||||
|
||||
const RENAMED = { hold: "drafting", wip: "building" };
|
||||
|
||||
function tiersOf(result: Ranked): string[] {
|
||||
return result.ranked.map((line) => line.tier);
|
||||
}
|
||||
|
||||
describe("assigned-task ranking under a renamed column vocabulary", () => {
|
||||
it("ranks a card in the RENAMED hold column as ready_todo", () => {
|
||||
const result = rankAssignedTasksForWakeDelta([task()], { agentId: "a1", roles: RENAMED });
|
||||
expect(tiersOf(result)).toEqual(["ready_todo"]);
|
||||
});
|
||||
|
||||
it("ranks a dependency-carrying hold card as partial_blocked, not ready", () => {
|
||||
/* The tier that distinguishes "ready" from "waiting", and the one a renamed
|
||||
workflow could not reach at all. */
|
||||
const result = rankAssignedTasksForWakeDelta(
|
||||
[task({ dependencies: ["FN-DEP"] })],
|
||||
{ agentId: "a1", roles: RENAMED },
|
||||
);
|
||||
expect(tiersOf(result)).toEqual(["partial_blocked"]);
|
||||
});
|
||||
|
||||
it("ranks a card in the RENAMED wip column as in_progress", () => {
|
||||
const result = rankAssignedTasksForWakeDelta(
|
||||
[task({ column: "building" })],
|
||||
{ agentId: "a1", roles: RENAMED },
|
||||
);
|
||||
expect(tiersOf(result)).toEqual(["in_progress"]);
|
||||
});
|
||||
|
||||
it("orders ready hold work ABOVE nothing-but-other work", () => {
|
||||
/*
|
||||
The observable symptom. Without the conversion the hold card falls to `other`
|
||||
and sorts below in-progress, so an agent sees ready work buried.
|
||||
*/
|
||||
const result = rankAssignedTasksForWakeDelta(
|
||||
[task({ id: "FN-OTHER", column: "reviewing" }), task({ id: "FN-READY" })],
|
||||
{ agentId: "a1", roles: RENAMED },
|
||||
);
|
||||
const ids = result.ranked.map((l) => l.task.id);
|
||||
expect(ids.indexOf("FN-READY")).toBeLessThan(ids.indexOf("FN-OTHER"));
|
||||
});
|
||||
|
||||
it("keeps every other tier rule intact under the renamed vocabulary", () => {
|
||||
/* The conversion must change which id means "hold"/"wip", not which cards are
|
||||
actionable at all. */
|
||||
const paused = rankAssignedTasksForWakeDelta([task({ paused: true })], { agentId: "a1", roles: RENAMED });
|
||||
expect(paused.ranked).toEqual([]);
|
||||
expect(paused.notActionableCount).toBe(1);
|
||||
|
||||
const unknown = rankAssignedTasksForWakeDelta([task({ column: "reviewing" })], { agentId: "a1", roles: RENAMED });
|
||||
expect(tiersOf(unknown)).toEqual(["other"]);
|
||||
});
|
||||
|
||||
it("defaults to the legacy ids when no roles are supplied", () => {
|
||||
/* Byte-identical for every caller that cannot resolve a workflow. */
|
||||
const legacy = rankAssignedTasksForWakeDelta([task({ column: "todo" })], { agentId: "a1" });
|
||||
expect(tiersOf(legacy)).toEqual(["ready_todo"]);
|
||||
|
||||
const renamedWithoutRoles = rankAssignedTasksForWakeDelta([task({ column: "drafting" })], { agentId: "a1" });
|
||||
expect(tiersOf(renamedWithoutRoles)).toEqual(["other"]);
|
||||
});
|
||||
});
|
||||
@@ -65,10 +65,32 @@ Map known default columns for rank quality; treat all other non-terminal open
|
||||
columns (including custom workflow columns) as titled `other` so inventory stays
|
||||
visible. Paused stays count-only to avoid re-chase noise.
|
||||
*/
|
||||
function tierForTask(task: AssignedTaskLike): AssignedTaskRankTier | "not_actionable" {
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-29-13:35 (U11):
|
||||
The two ACTIONABLE lifecycle roles. `todo`/`in-progress` are only what the builtin
|
||||
coding workflow calls them. The comment above already records that unrecognised
|
||||
columns fall to `other` so assigned work stays visible — but that is a floor, not
|
||||
a fix: a renamed HOLD column loses `ready_todo` and `partial_blocked` entirely, so
|
||||
work that is genuinely ready to start ranks below everything already in progress.
|
||||
Nothing errors and nothing disappears; the ordering is just wrong, which is how it
|
||||
survived.
|
||||
|
||||
Defaults to the legacy ids so every unconverted caller is byte-identical.
|
||||
*/
|
||||
export interface AssignedTaskRankRoles {
|
||||
hold: string;
|
||||
wip: string;
|
||||
}
|
||||
|
||||
const LEGACY_RANK_ROLES: AssignedTaskRankRoles = { hold: "todo", wip: "in-progress" };
|
||||
|
||||
function tierForTask(
|
||||
task: AssignedTaskLike,
|
||||
roles: AssignedTaskRankRoles = LEGACY_RANK_ROLES,
|
||||
): AssignedTaskRankTier | "not_actionable" {
|
||||
if (task.paused) return "not_actionable";
|
||||
if (task.column === "in-progress") return "in_progress";
|
||||
if (task.column === "todo") {
|
||||
if (task.column === roles.wip) return "in_progress";
|
||||
if (task.column === roles.hold) {
|
||||
const deps = task.dependencies ?? [];
|
||||
if (deps.length === 0) return "ready_todo";
|
||||
// Coarse v1: non-empty deps ⇒ partial_blocked visibility (full dep hydrate deferred).
|
||||
@@ -96,6 +118,8 @@ export function rankAssignedTasksForWakeDelta(
|
||||
agentId: string;
|
||||
boundTaskId?: string | null;
|
||||
cap?: number;
|
||||
/** Resolved lifecycle roles; omitted keeps the legacy builtin ids. */
|
||||
roles?: AssignedTaskRankRoles;
|
||||
},
|
||||
): RankAssignedTasksForWakeDeltaResult {
|
||||
const cap = options.cap ?? WAKE_DELTA_ASSIGNED_TASKS_CAP;
|
||||
@@ -105,7 +129,7 @@ export function rankAssignedTasksForWakeDelta(
|
||||
let notActionableCount = 0;
|
||||
|
||||
for (const task of open) {
|
||||
const tierOrNa = tierForTask(task);
|
||||
const tierOrNa = tierForTask(task, options.roles);
|
||||
if (tierOrNa === "not_actionable") {
|
||||
notActionableCount += 1;
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user