fix: resume selection under-reports task totals with multiple loops
The resume prompt counted total/completed from progress.tasks, which only records TOUCHED tasks (started/completed/failed) — never-started tasks were silently missing, and file-checkbox completions weren't counted unless markCompleted had run. With e.g. 10 tasks where 3 completed and 5 untouched, it showed 3/5 done instead of 3/10 done. - add countPRDResumeStats (src/utils.ts): total from parseTaskFile of the PRD source; completed = progress completions ∪ PRD checkbox completions, deduped by task id; failed from progress. Falls back to touched-task counts when the source file is missing/unparseable. - selectPRDToResume (index.ts) uses the helper instead of Object.keys(). - tests/resume-stats.test.ts: 4 regression tests. Includes pre-existing uncommitted changes: index.ts tab-reformat (matches src/ style) and package.json version bump 0.4.2 -> 0.4.3.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mikefreno/ralpi",
|
"name": "@mikefreno/ralpi",
|
||||||
"version": "0.4.2",
|
"version": "0.4.3",
|
||||||
"description": "Execute tasks from task files/PRD's using DAG-based dependency resolution with persistent progress tracking",
|
"description": "Execute tasks from task files/PRD's using DAG-based dependency resolution with persistent progress tracking",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"pi-package",
|
"pi-package",
|
||||||
|
|||||||
44
src/utils.ts
44
src/utils.ts
@@ -7,6 +7,7 @@ import type {
|
|||||||
ToolUsage,
|
ToolUsage,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { DEFAULT_CONFIG } from "./types";
|
import { DEFAULT_CONFIG } from "./types";
|
||||||
|
import { parseTaskFile } from "./parser";
|
||||||
import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
||||||
import {
|
import {
|
||||||
createAgentSession,
|
createAgentSession,
|
||||||
@@ -198,6 +199,49 @@ export function listPRDsSorted(
|
|||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PRDResumeSummary {
|
||||||
|
total: number;
|
||||||
|
completed: number;
|
||||||
|
failed: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count tasks for the resume-selection display.
|
||||||
|
*
|
||||||
|
* The progress tracker only records tasks that were TOUCHED (started,
|
||||||
|
* completed, or failed) — never-started tasks are absent from `prd.tasks`,
|
||||||
|
* so a naive Object.keys(prd.tasks).length under-reports the real total.
|
||||||
|
* The true total comes from parsing the PRD source file. Completed counts
|
||||||
|
* both progress-marked completions and PRD checkbox completions (a task
|
||||||
|
* checked off in the file is done even if the loop was interrupted before
|
||||||
|
* markCompleted), deduped by task id. Falls back to touched-task counts
|
||||||
|
* when the source file is missing or unparseable.
|
||||||
|
*/
|
||||||
|
export function countPRDResumeStats(
|
||||||
|
prd: PRDProgress,
|
||||||
|
sourcePath: string,
|
||||||
|
): PRDResumeSummary {
|
||||||
|
const touched = Object.entries(prd.tasks);
|
||||||
|
const failed = touched.filter(([, t]) => t.status === "failed").length;
|
||||||
|
const completedIds = new Set(
|
||||||
|
touched.filter(([, t]) => t.status === "completed").map(([id]) => id),
|
||||||
|
);
|
||||||
|
|
||||||
|
let total: number;
|
||||||
|
try {
|
||||||
|
const project = parseTaskFile(sourcePath);
|
||||||
|
total = project.tasks.length;
|
||||||
|
for (const task of project.tasks) {
|
||||||
|
if (task.status === "completed") completedIds.add(task.id);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// PRD file missing/unparseable — fall back to touched-task counts
|
||||||
|
total = touched.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { total, completed: completedIds.size, failed };
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Model Resolution ───────────────────────────────────────────────────────
|
// ─── Model Resolution ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
87
tests/resume-stats.test.ts
Normal file
87
tests/resume-stats.test.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
/// <reference types="bun-types" />
|
||||||
|
import { describe, it, expect, beforeEach } from "bun:test";
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as os from "node:os";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import { ProgressTracker } from "../src/progress";
|
||||||
|
import { countPRDResumeStats } from "../src/utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regression test: the resume-selection prompt under-reported task totals
|
||||||
|
* when multiple loop histories existed. The progress tracker only records
|
||||||
|
* TOUCHED tasks (started/completed/failed) — never-started tasks are absent
|
||||||
|
* from prd.tasks, so a naive Object.keys() count missed them entirely, and
|
||||||
|
* file-checked completions were ignored unless markCompleted had run.
|
||||||
|
* countPRDResumeStats derives the true total from the parsed PRD file and
|
||||||
|
* counts checkbox completions too.
|
||||||
|
*/
|
||||||
|
let root: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
root = fs.mkdtempSync(path.join(os.tmpdir(), "ralpi-stats-test-"));
|
||||||
|
});
|
||||||
|
|
||||||
|
const PRD_CONTENT = `# Test PRD
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
- [ ] Task one
|
||||||
|
- [x] Task two
|
||||||
|
- [ ] Task three
|
||||||
|
- [ ] Task four
|
||||||
|
`;
|
||||||
|
|
||||||
|
function writePRD(rel: string): string {
|
||||||
|
const p = path.join(root, rel);
|
||||||
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
||||||
|
fs.writeFileSync(p, PRD_CONTENT, "utf-8");
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("countPRDResumeStats", () => {
|
||||||
|
it("reports the full task total from the PRD file, not just touched tasks", () => {
|
||||||
|
const sourcePath = writePRD("tasks/a/README.md");
|
||||||
|
const progress = new ProgressTracker(root, sourcePath);
|
||||||
|
|
||||||
|
// Simple-checkbox format assigns sequential ids 00-03. Only 00
|
||||||
|
// (completed) and 02 (failed) were touched by the loop; 01 is checked
|
||||||
|
// off in the file; 03 was never started.
|
||||||
|
progress.markCompleted("00", 1000);
|
||||||
|
progress.markFailed("02", "boom");
|
||||||
|
|
||||||
|
const stats = countPRDResumeStats(progress.getState(), sourcePath);
|
||||||
|
expect(stats.total).toBe(4); // old code reported 2
|
||||||
|
expect(stats.completed).toBe(2); // 00 via progress + 01 via checkbox
|
||||||
|
expect(stats.failed).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not double-count a task that is both progress-completed and file-checked", () => {
|
||||||
|
const sourcePath = writePRD("tasks/b/README.md");
|
||||||
|
const progress = new ProgressTracker(root, sourcePath);
|
||||||
|
|
||||||
|
progress.markCompleted("01", 500); // 01 already [x] in the file
|
||||||
|
|
||||||
|
const stats = countPRDResumeStats(progress.getState(), sourcePath);
|
||||||
|
expect(stats.completed).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to touched-task counts when the PRD file is missing", () => {
|
||||||
|
const missing = path.join(root, "tasks/gone/README.md");
|
||||||
|
const progress = new ProgressTracker(root, missing);
|
||||||
|
progress.markCompleted("01", 1000);
|
||||||
|
progress.markFailed("02", "nope");
|
||||||
|
|
||||||
|
const stats = countPRDResumeStats(progress.getState(), missing);
|
||||||
|
expect(stats.total).toBe(2);
|
||||||
|
expect(stats.completed).toBe(1);
|
||||||
|
expect(stats.failed).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports zero for a never-touched PRD with no file", () => {
|
||||||
|
const missing = path.join(root, "tasks/none/README.md");
|
||||||
|
const progress = new ProgressTracker(root, missing);
|
||||||
|
const stats = countPRDResumeStats(progress.getState(), missing);
|
||||||
|
expect(stats.total).toBe(0);
|
||||||
|
expect(stats.completed).toBe(0);
|
||||||
|
expect(stats.failed).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user