Files
fusion/packages/core/src/mesh-task-replication.ts
gsxdsm beebd270bd fix: make Queued to plan / Ready badges agree with the planning lane
TaskCard inferred "unplanned" from steps.length === 0 while triage's
todo-discovery and the scheduler's dispatch filter both decide from
PROMPT.md seed-ness, so the badges disagreed with the engine in both
directions: a real spec that parsed to zero steps read as "Queued to
plan" while the scheduler already treated it as a WIP-slot candidate, and
a re-seeded card still carrying old steps read as "Ready" while triage
was about to plan it. Either way the badge sent operators to the wrong
cap.

Adds the shared isTaskAwaitingPlanning predicate (replan park, missing
spec, seed-vs-real content) used by both triage's discovery and a new
best-effort `awaitingPlanning` enrichment on GET /api/tasks. TaskCard
derives both badges from that one value — strict complements — and keeps
the step count only as a fallback for SSE payloads and older servers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 17:48:46 -07:00

107 lines
5.6 KiB
TypeScript

/*
FNXC:PostgresCutover 2026-07-12:
Mesh task replication is REMOVED — all replication is handled at the
PostgreSQL level (nodes share the database). This module used to carry the
replicated-create payload builders/matchers; only buildBootstrapPrompt
survives because task creation, comments, and title/description sync use it
to write the human-visible PROMPT.md stub.
*/
export function buildBootstrapPrompt(taskId: string, title: string | undefined, description: string): string {
const heading = title ? `${taskId}: ${title}` : taskId;
return `# ${heading}\n\n${description}\n`;
}
/*
FNXC:TaskRefinementWorkflow 2026-07-13-12:00:
The single source of truth for the refinement seed shape. TaskStore.refineTask writes this
exact content and isUnplannedSeedPrompt detects it by byte-equality — keep both on this
builder or the detector silently stops matching when the seed format changes, and unplanned
refinements release into execution again.
*/
export function buildRefinementSeedPrompt(title: string, description: string): string {
return `# ${title}\n\n${description}\n`;
}
/*
FNXC:WorkflowScheduling 2026-07-25-11:20:
Seed detection compares NORMALIZED text, not raw bytes. Symptom it fixes: a Coding (Ideas) card
promoted to Todo was never planned — triage's todo-discovery only admits a card whose PROMPT.md
still reads as a seed, and any byte-level drift from the builder output silently reclassified the
card as "already planned", so it sat in Todo forever with no log line. Drift sources are all
benign and outside the writer's control: a CRLF checkout/editor round-trip, an editor that adds or
strips the trailing newline, or trailing spaces. Normalization is line-ending + trailing-whitespace
only — the heading and body text still must match exactly, so a REAL spec can never normalize into
a seed (it carries Mission/Steps/File Scope sections the seed does not have).
*/
function normalizeSeedText(content: string): string {
return content.replace(/\r\n/g, "\n").replace(/[ \t]+$/gm, "").trimEnd();
}
/*
FNXC:WorkflowScheduling 2026-07-12-22:55:
"Unplanned" detection must recognize BOTH seed-prompt shapes or unplanned cards slip into
execution with a non-spec prompt:
1. The createTask bootstrap stub (`# {id}: {title}\n\n{description}\n`).
2. The refineTask seed (buildRefinementSeedPrompt — no task-id prefix), which previously
failed the strict stub-equality check, so a refinement promoted out of a manual intake
column (Coding (Ideas)) was treated as already planned and released straight into
execution carrying only the operator's feedback text.
FNXC:WorkflowScheduling 2026-07-25-11:20:
This is the SINGLE seed predicate for every scheduling decision. The dispatch filter in
scheduler.ts used to open-code strict `content === buildBootstrapPrompt(...)`, which disagreed
with this function on the refinement seed: triage called such a card unplanned (so it planned it)
while the scheduler called it planned (so it was also a dispatch candidate), leaving hold-release
as the only thing between a refinement seed and an executor running on the operator's feedback
text. Callers: triage todo-discovery (plan-in-place workflows), the scheduler dispatch filter, and
hold-release's isUnplannedForExecution guard. Do not re-open-code either equality check.
*/
export function isUnplannedSeedPrompt(
content: string,
taskId: string,
title: string | undefined,
description: string,
): boolean {
const normalized = normalizeSeedText(content);
if (normalized === normalizeSeedText(buildBootstrapPrompt(taskId, title, description))) return true;
return title !== undefined
&& normalized === normalizeSeedText(buildRefinementSeedPrompt(title, description));
}
/**
* Durable statuses that park a plan-in-place card for another planning pass regardless of what its
* PROMPT.md currently says. Mirrors triage todo-discovery's `needs-replan` branch — Plan Review
* rejected the current spec, so the real (rejected) prompt must not read as "already planned".
*/
const AWAITING_PLANNING_STATUSES = new Set(["needs-replan"]);
/*
FNXC:CodingIdeasWorkflow 2026-07-26-15:30:
Whether a plan-in-place (Todo) card is waiting for a PLANNING slot rather than a WIP slot — the
question the "Queued to plan" / "Ready" badge pair answers.
Requirement: the badge must not contradict the engine. It used to infer "unplanned" from
`steps.length === 0`, while every engine lane decides from PROMPT.md seed-ness, so the two disagreed
in both directions: a card with a real spec but no parsed steps was labelled "Queued to plan" while
the scheduler was actually treating it as a dispatch candidate (waiting for a WIP slot, i.e. Ready),
and a re-seeded card still carrying steps from a previous pass was labelled "Ready" while triage was
about to plan it. The badge exists to make the FN-8600 planning-capacity wait legible, so a wrong
label sends operators to the wrong cap.
`promptContent === null` means PROMPT.md is missing, which triage treats as unplanned (it
regenerates the spec) rather than as planned. Kept pure — the fs read belongs to the caller, so
this stays usable from the API layer, the engine, and tests alike.
Callers: the `GET /api/tasks` board enrichment (`awaitingPlanning`) and triage's todo-discovery
content branch. Do not re-open-code the status set or the seed check.
*/
export function isTaskAwaitingPlanning(
task: { id: string; title?: string; description: string; status?: string | null },
promptContent: string | null,
): boolean {
if (task.status != null && AWAITING_PLANNING_STATUSES.has(task.status)) return true;
if (promptContent === null) return true;
return isUnplannedSeedPrompt(promptContent, task.id, task.title, task.description);
}