fix(chat): narrate task-chat step numbers 1-based to match the card's step count

The proactive status narration printed the internal 0-based step index, so the
final step of a 13-step task announced "Starting Step 12" next to a card
showing "12/13". Display now uses index + 1 in both the engine builders and
the store-side updateStep narration; the 0-based tool/PROMPT.md contract is
unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-23 21:43:58 -07:00
parent 345dccbe7d
commit 049c36c9ae
5 changed files with 52 additions and 20 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Task chat step narration now shows 1-based step numbers matching the task card's step count.
category: fix
dev: Display-only change in proactive-status.ts builders and merge-queue-ops proactiveStepStatusMessage; the 0-based step-index contract (tools, PROMPT.md headings, run-audit) is unchanged.

View File

@@ -36,8 +36,9 @@ pgTest("proactive step-status chat entries (PostgreSQL)", () => {
const statuses = (await harness.store.getAgentLogs(task.id, { type: "status" })).map((entry) => entry.text); const statuses = (await harness.store.getAgentLogs(task.id, { type: "status" })).map((entry) => entry.text);
expect(statuses).toEqual([ expect(statuses).toEqual([
"Step 0 finished — Implement the change.", // Narration displays 1-based numbers (0-based index 0 → "Step 1") to match the task card.
"Step 0 was returned to pending — Implement the change.", "Step 1 finished — Implement the change.",
"Step 1 was returned to pending — Implement the change.",
]); ]);
}); });
}); });

View File

@@ -41,16 +41,20 @@ function proactiveStepStatusMessage(
status: import("../types.js").StepStatus, status: import("../types.js").StepStatus,
): string | null { ): string | null {
if (previousStatus === status) return null; if (previousStatus === status) return null;
const label = stepName.trim() || `Step ${stepIndex}`; // FNXC:ProactiveChatStatus 2026-07-23-10:30:
// Chat narration must display 1-based step numbers to match the task card's "N/M steps"
// counting; stepIndex stays 0-based in the store/tool contract (see proactive-status.ts).
const display = stepIndex + 1;
const label = stepName.trim() || `Step ${display}`;
switch (status) { switch (status) {
case "in-progress": case "in-progress":
return `Starting Step ${stepIndex}: ${label}`; return `Starting Step ${display}: ${label}`;
case "done": case "done":
return `Step ${stepIndex} finished — ${label}.`; return `Step ${display} finished — ${label}.`;
case "skipped": case "skipped":
return `Step ${stepIndex} was skipped — ${label}.`; return `Step ${display} was skipped — ${label}.`;
case "pending": case "pending":
return `Step ${stepIndex} was returned to pending — ${label}.`; return `Step ${display} was returned to pending — ${label}.`;
} }
} }

View File

@@ -33,10 +33,12 @@ describe("proactive status narration", () => {
}); });
it("builds complete status messages and safely reports unavailable reviews", () => { it("builds complete status messages and safely reports unavailable reviews", () => {
expect(buildStepStartMessage(2, "Ship it")).toBe("Starting Step 2: Ship it"); // Narration displays 1-based step numbers (0-based index 2 → "Step 3") to match the task card.
expect(buildStepSuccessMessage(2, "Ship it")).toBe("Step 2 finished — Ship it."); expect(buildStepStartMessage(2, "Ship it")).toBe("Starting Step 3: Ship it");
expect(buildStepSkippedMessage(2, "No code change needed")).toBe("Step 2 was skipped — No code change needed."); expect(buildStepSuccessMessage(2, "Ship it")).toBe("Step 3 finished — Ship it.");
expect(buildStepSkippedMessage(2, "Step 2")).toBe("Step 2 was skipped."); expect(buildStepSkippedMessage(2, "No code change needed")).toBe("Step 3 was skipped — No code change needed.");
// A caller-defaulted name of "Step <0-based index>" is treated as unnamed, not echoed.
expect(buildStepSkippedMessage(2, "Step 2")).toBe("Step 3 was skipped.");
expect(buildStepFailureMessage(2, "Ship it", sanitizeFailureReason(undefined))).toContain("No failure reason"); expect(buildStepFailureMessage(2, "Ship it", sanitizeFailureReason(undefined))).toContain("No failure reason");
expect(buildPlanVerifiedMessage()).toBe("The plan was written and verified."); expect(buildPlanVerifiedMessage()).toBe("The plan was written and verified.");
expect(buildReviewVerdictMessage("UNAVAILABLE", "nope")).toBeNull(); expect(buildReviewVerdictMessage("UNAVAILABLE", "nope")).toBeNull();

View File

@@ -39,18 +39,33 @@ export function sanitizeFailureReason(rawError: unknown): string {
return sanitized; return sanitized;
} }
function stepLabel(stepIndex: number, stepName?: string): string { /*
const fallback = `Step ${stepIndex}`; FNXC:ProactiveChatStatus 2026-07-23-10:30:
return stepName?.trim() || fallback; Step indices are 0-based everywhere in the agent-facing contract (fn_task_update, PROMPT.md
"### Step 0:" headings, run-audit rows), but the task card counts steps 1-based ("12/13").
Chat narration is read next to the card, so it must display stepIndex + 1 or the final step of a
13-step task announces itself as "Step 12" while the card shows 13 steps. Only the display number
converts; callers keep passing the 0-based index, and a fallback name of "Step <0-based index>"
(produced by callers that default the name from the index) is treated as "unnamed".
*/
function stepDisplayNumber(stepIndex: number): number {
return stepIndex + 1;
}
function stepDisplayName(stepIndex: number, stepName?: string): string | undefined {
const name = stepName?.trim();
return name && name !== `Step ${stepIndex}` ? name : undefined;
} }
export function buildStepStartMessage(stepIndex: number, stepName?: string): string { export function buildStepStartMessage(stepIndex: number, stepName?: string): string {
return `Starting Step ${stepIndex}: ${stepLabel(stepIndex, stepName)}`; const display = stepDisplayNumber(stepIndex);
return `Starting Step ${display}: ${stepDisplayName(stepIndex, stepName) ?? `Step ${display}`}`;
} }
export function buildStepSuccessMessage(stepIndex: number, stepName?: string): string { export function buildStepSuccessMessage(stepIndex: number, stepName?: string): string {
const name = stepName?.trim(); const name = stepDisplayName(stepIndex, stepName);
return name && name !== `Step ${stepIndex}` ? `Step ${stepIndex} finished — ${name}.` : `Step ${stepIndex} finished.`; const display = stepDisplayNumber(stepIndex);
return name ? `Step ${display} finished — ${name}.` : `Step ${display} finished.`;
} }
/** /**
@@ -59,12 +74,15 @@ export function buildStepSuccessMessage(stepIndex: number, stepName?: string): s
* preflight and intentional no-op flows remain visible without fabricating a failure reason. * preflight and intentional no-op flows remain visible without fabricating a failure reason.
*/ */
export function buildStepSkippedMessage(stepIndex: number, stepName?: string): string { export function buildStepSkippedMessage(stepIndex: number, stepName?: string): string {
const name = stepName?.trim(); const name = stepDisplayName(stepIndex, stepName);
return name && name !== `Step ${stepIndex}` ? `Step ${stepIndex} was skipped — ${name}.` : `Step ${stepIndex} was skipped.`; const display = stepDisplayNumber(stepIndex);
return name ? `Step ${display} was skipped — ${name}.` : `Step ${display} was skipped.`;
} }
export function buildStepFailureMessage(stepIndex: number, stepName: string | undefined, safeReason: string): string { export function buildStepFailureMessage(stepIndex: number, stepName: string | undefined, safeReason: string): string {
const prefix = stepName?.trim() && stepName.trim() !== `Step ${stepIndex}` ? `Step ${stepIndex} (${stepName.trim()})` : `Step ${stepIndex}`; const name = stepDisplayName(stepIndex, stepName);
const display = stepDisplayNumber(stepIndex);
const prefix = name ? `Step ${display} (${name})` : `Step ${display}`;
return `${prefix} did not complete: ${safeReason}`; return `${prefix} did not complete: ${safeReason}`;
} }