fix width exceeding, release prep
This commit is contained in:
470
src/progress.ts
470
src/progress.ts
@@ -1,11 +1,11 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import type {
|
||||
ProgressState,
|
||||
PRDProgress,
|
||||
Task,
|
||||
Reflection,
|
||||
ToolUsage,
|
||||
ProgressState,
|
||||
PRDProgress,
|
||||
Task,
|
||||
Reflection,
|
||||
ToolUsage,
|
||||
} from "./types";
|
||||
import { ensureDir } from "./utils";
|
||||
|
||||
@@ -14,11 +14,11 @@ import { ensureDir } from "./utils";
|
||||
* e.g., "tasks/feature-x/README.md" → "tasks-feature-x-README"
|
||||
*/
|
||||
export function derivePRDKey(projectDir: string, sourcePath: string): string {
|
||||
const rel = path.relative(projectDir, sourcePath);
|
||||
return rel
|
||||
.replace(/[^a-zA-Z0-9_-]/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
const rel = path.relative(projectDir, sourcePath);
|
||||
return rel
|
||||
.replace(/[^a-zA-Z0-9_-]/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,250 +28,258 @@ export function derivePRDKey(projectDir: string, sourcePath: string): string {
|
||||
* Falls back to legacy flat format for backward compatibility.
|
||||
*/
|
||||
export class ProgressTracker {
|
||||
private statePath: string;
|
||||
private state: ProgressState;
|
||||
private prdKey: string;
|
||||
private statePath: string;
|
||||
private state: ProgressState;
|
||||
private prdKey: string;
|
||||
|
||||
constructor(projectDir: string, sourcePath: string, prdKey?: string) {
|
||||
const stateDir = path.join(projectDir, ".ralpi");
|
||||
ensureDir(stateDir);
|
||||
this.statePath = path.join(stateDir, "progress.json");
|
||||
this.prdKey = prdKey ?? derivePRDKey(projectDir, sourcePath);
|
||||
this.state = this.loadOrCreate(sourcePath);
|
||||
}
|
||||
constructor(projectDir: string, sourcePath: string, prdKey?: string) {
|
||||
const stateDir = path.join(projectDir, ".ralpi");
|
||||
ensureDir(stateDir);
|
||||
this.statePath = path.join(stateDir, "progress.json");
|
||||
this.prdKey = prdKey ?? derivePRDKey(projectDir, sourcePath);
|
||||
this.state = this.loadOrCreate(sourcePath);
|
||||
}
|
||||
|
||||
/** Load existing state or create a fresh one */
|
||||
private loadOrCreate(sourcePathHint: string): ProgressState {
|
||||
if (fs.existsSync(this.statePath)) {
|
||||
try {
|
||||
const raw = fs.readFileSync(this.statePath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as ProgressState;
|
||||
/** Load existing state or create a fresh one */
|
||||
private loadOrCreate(sourcePathHint: string): ProgressState {
|
||||
if (fs.existsSync(this.statePath)) {
|
||||
try {
|
||||
const raw = fs.readFileSync(this.statePath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as ProgressState;
|
||||
|
||||
// Multi-PRD mode: check if we have a PRD entry
|
||||
if (parsed.prds?.[this.prdKey]) {
|
||||
// Found PRD entry — use it, but keep legacy fields for compat
|
||||
return parsed;
|
||||
}
|
||||
// Multi-PRD mode: check if we have a PRD entry
|
||||
if (parsed.prds?.[this.prdKey]) {
|
||||
// Found PRD entry — use it, but keep legacy fields for compat
|
||||
return parsed;
|
||||
}
|
||||
|
||||
// Legacy flat mode: check if the source path matches
|
||||
if (path.resolve(parsed.sourcePath) === path.resolve(sourcePathHint)) {
|
||||
// Migrate legacy state to PRD mode
|
||||
parsed.prds = {
|
||||
[this.prdKey]: {
|
||||
sourcePath: parsed.sourcePath,
|
||||
tasks: parsed.tasks,
|
||||
startedAt: parsed.startedAt,
|
||||
lastUpdatedAt: parsed.lastUpdatedAt,
|
||||
paused: parsed.paused,
|
||||
},
|
||||
};
|
||||
return parsed;
|
||||
}
|
||||
// Legacy flat mode: check if the source path matches
|
||||
if (path.resolve(parsed.sourcePath) === path.resolve(sourcePathHint)) {
|
||||
// Migrate legacy state to PRD mode
|
||||
parsed.prds = {
|
||||
[this.prdKey]: {
|
||||
sourcePath: parsed.sourcePath,
|
||||
tasks: parsed.tasks,
|
||||
startedAt: parsed.startedAt,
|
||||
lastUpdatedAt: parsed.lastUpdatedAt,
|
||||
paused: parsed.paused,
|
||||
},
|
||||
};
|
||||
return parsed;
|
||||
}
|
||||
|
||||
// Different PRD — create new entry alongside existing ones
|
||||
if (parsed.prds) {
|
||||
parsed.prds[this.prdKey] = this.freshPRD(sourcePathHint);
|
||||
return parsed;
|
||||
}
|
||||
// Different PRD — create new entry alongside existing ones
|
||||
if (parsed.prds) {
|
||||
parsed.prds[this.prdKey] = this.freshPRD(sourcePathHint);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
// Legacy flat state exists but for a different source — promote it to PRD mode
|
||||
const legacyKey = derivePRDKey(
|
||||
path.dirname(this.statePath),
|
||||
parsed.sourcePath,
|
||||
);
|
||||
parsed.prds = {
|
||||
[legacyKey]: {
|
||||
sourcePath: parsed.sourcePath,
|
||||
tasks: parsed.tasks,
|
||||
startedAt: parsed.startedAt,
|
||||
lastUpdatedAt: parsed.lastUpdatedAt,
|
||||
paused: parsed.paused,
|
||||
},
|
||||
[this.prdKey]: this.freshPRD(sourcePathHint),
|
||||
};
|
||||
return parsed;
|
||||
} catch {
|
||||
// Fall through to create new
|
||||
}
|
||||
}
|
||||
// Legacy flat state exists but for a different source — promote it to PRD mode
|
||||
const legacyKey = derivePRDKey(
|
||||
path.dirname(this.statePath),
|
||||
parsed.sourcePath,
|
||||
);
|
||||
parsed.prds = {
|
||||
[legacyKey]: {
|
||||
sourcePath: parsed.sourcePath,
|
||||
tasks: parsed.tasks,
|
||||
startedAt: parsed.startedAt,
|
||||
lastUpdatedAt: parsed.lastUpdatedAt,
|
||||
paused: parsed.paused,
|
||||
},
|
||||
[this.prdKey]: this.freshPRD(sourcePathHint),
|
||||
};
|
||||
return parsed;
|
||||
} catch {
|
||||
// Fall through to create new
|
||||
}
|
||||
}
|
||||
|
||||
return this.freshState(sourcePathHint);
|
||||
}
|
||||
return this.freshState(sourcePathHint);
|
||||
}
|
||||
|
||||
private freshPRD(sourcePath: string): PRDProgress {
|
||||
return {
|
||||
sourcePath,
|
||||
tasks: {},
|
||||
startedAt: new Date().toISOString(),
|
||||
lastUpdatedAt: new Date().toISOString(),
|
||||
paused: false,
|
||||
};
|
||||
}
|
||||
private freshPRD(sourcePath: string): PRDProgress {
|
||||
return {
|
||||
sourcePath,
|
||||
tasks: {},
|
||||
startedAt: new Date().toISOString(),
|
||||
lastUpdatedAt: new Date().toISOString(),
|
||||
paused: false,
|
||||
};
|
||||
}
|
||||
|
||||
private freshState(sourcePath: string): ProgressState {
|
||||
return {
|
||||
sourcePath,
|
||||
tasks: {},
|
||||
startedAt: new Date().toISOString(),
|
||||
lastUpdatedAt: new Date().toISOString(),
|
||||
paused: false,
|
||||
prds: {
|
||||
[this.prdKey]: {
|
||||
sourcePath,
|
||||
tasks: {},
|
||||
startedAt: new Date().toISOString(),
|
||||
lastUpdatedAt: new Date().toISOString(),
|
||||
paused: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
private freshState(sourcePath: string): ProgressState {
|
||||
return {
|
||||
sourcePath,
|
||||
tasks: {},
|
||||
startedAt: new Date().toISOString(),
|
||||
lastUpdatedAt: new Date().toISOString(),
|
||||
paused: false,
|
||||
prds: {
|
||||
[this.prdKey]: {
|
||||
sourcePath,
|
||||
tasks: {},
|
||||
startedAt: new Date().toISOString(),
|
||||
lastUpdatedAt: new Date().toISOString(),
|
||||
paused: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Get the PRD-scoped progress entry */
|
||||
private getPRD(): PRDProgress {
|
||||
if (!this.state.prds) {
|
||||
// Should not happen after loadOrCreate, but guard anyway
|
||||
this.state.prds = { [this.prdKey]: this.freshPRD(this.state.sourcePath) };
|
||||
}
|
||||
if (!this.state.prds[this.prdKey]) {
|
||||
this.state.prds[this.prdKey] = this.freshPRD(this.state.sourcePath);
|
||||
}
|
||||
return this.state.prds[this.prdKey];
|
||||
}
|
||||
/** Get the PRD-scoped progress entry */
|
||||
private getPRD(): PRDProgress {
|
||||
if (!this.state.prds) {
|
||||
// Should not happen after loadOrCreate, but guard anyway
|
||||
this.state.prds = { [this.prdKey]: this.freshPRD(this.state.sourcePath) };
|
||||
}
|
||||
if (!this.state.prds[this.prdKey]) {
|
||||
this.state.prds[this.prdKey] = this.freshPRD(this.state.sourcePath);
|
||||
}
|
||||
return this.state.prds[this.prdKey];
|
||||
}
|
||||
|
||||
/** Save current state to disk */
|
||||
save(): void {
|
||||
const prd = this.getPRD();
|
||||
prd.lastUpdatedAt = new Date().toISOString();
|
||||
// Sync legacy flat fields with current PRD for backward compat
|
||||
this.state.sourcePath = prd.sourcePath;
|
||||
this.state.tasks = prd.tasks;
|
||||
this.state.startedAt = prd.startedAt;
|
||||
this.state.lastUpdatedAt = prd.lastUpdatedAt;
|
||||
this.state.paused = prd.paused;
|
||||
fs.writeFileSync(
|
||||
this.statePath,
|
||||
JSON.stringify(this.state, null, 2),
|
||||
"utf-8",
|
||||
);
|
||||
}
|
||||
/** Save current state to disk */
|
||||
save(): void {
|
||||
const prd = this.getPRD();
|
||||
prd.lastUpdatedAt = new Date().toISOString();
|
||||
// Sync legacy flat fields with current PRD for backward compat
|
||||
this.state.sourcePath = prd.sourcePath;
|
||||
this.state.tasks = prd.tasks;
|
||||
this.state.startedAt = prd.startedAt;
|
||||
this.state.lastUpdatedAt = prd.lastUpdatedAt;
|
||||
this.state.paused = prd.paused;
|
||||
fs.writeFileSync(
|
||||
this.statePath,
|
||||
JSON.stringify(this.state, null, 2),
|
||||
"utf-8",
|
||||
);
|
||||
}
|
||||
|
||||
/** Mark a task as in progress */
|
||||
markInProgress(taskId: string): void {
|
||||
const prd = this.getPRD();
|
||||
this.ensureTask(prd, taskId);
|
||||
prd.tasks[taskId].status = "in_progress";
|
||||
prd.tasks[taskId].startedAt = new Date().toISOString();
|
||||
this.save();
|
||||
}
|
||||
/** Mark a task as in progress */
|
||||
markInProgress(taskId: string): void {
|
||||
const prd = this.getPRD();
|
||||
this.ensureTask(prd, taskId);
|
||||
prd.tasks[taskId].status = "in_progress";
|
||||
prd.tasks[taskId].startedAt = new Date().toISOString();
|
||||
this.save();
|
||||
}
|
||||
|
||||
/** Mark a task as completed */
|
||||
markCompleted(
|
||||
taskId: string,
|
||||
durationMs: number,
|
||||
reflection?: Reflection,
|
||||
toolUsage?: ToolUsage,
|
||||
sessionFile?: string,
|
||||
outputPreview?: string,
|
||||
commitMessages?: string[],
|
||||
commitSummary?: string,
|
||||
): void {
|
||||
const prd = this.getPRD();
|
||||
this.ensureTask(prd, taskId);
|
||||
prd.tasks[taskId].status = "completed";
|
||||
prd.tasks[taskId].completedAt = new Date().toISOString();
|
||||
prd.tasks[taskId].durationMs = durationMs;
|
||||
if (reflection) prd.tasks[taskId].reflection = reflection;
|
||||
if (toolUsage) prd.tasks[taskId].toolUsage = toolUsage;
|
||||
if (sessionFile) prd.tasks[taskId].sessionFile = sessionFile;
|
||||
if (outputPreview) prd.tasks[taskId].outputPreview = outputPreview;
|
||||
if (commitMessages) prd.tasks[taskId].commitMessages = commitMessages;
|
||||
if (commitSummary) prd.tasks[taskId].commitSummary = commitSummary;
|
||||
this.save();
|
||||
}
|
||||
/** Mark a task as completed */
|
||||
markCompleted(
|
||||
taskId: string,
|
||||
durationMs: number,
|
||||
reflection?: Reflection,
|
||||
toolUsage?: ToolUsage,
|
||||
sessionFile?: string,
|
||||
outputPreview?: string,
|
||||
commitMessages?: string[],
|
||||
commitSummary?: string,
|
||||
): void {
|
||||
const prd = this.getPRD();
|
||||
this.ensureTask(prd, taskId);
|
||||
prd.tasks[taskId].status = "completed";
|
||||
prd.tasks[taskId].completedAt = new Date().toISOString();
|
||||
prd.tasks[taskId].durationMs = durationMs;
|
||||
if (reflection) prd.tasks[taskId].reflection = reflection;
|
||||
if (toolUsage) prd.tasks[taskId].toolUsage = toolUsage;
|
||||
if (sessionFile) prd.tasks[taskId].sessionFile = sessionFile;
|
||||
if (outputPreview) prd.tasks[taskId].outputPreview = outputPreview;
|
||||
if (commitMessages) prd.tasks[taskId].commitMessages = commitMessages;
|
||||
if (commitSummary) prd.tasks[taskId].commitSummary = commitSummary;
|
||||
this.save();
|
||||
}
|
||||
|
||||
/** Mark a task as failed */
|
||||
markFailed(taskId: string, error: string): void {
|
||||
const prd = this.getPRD();
|
||||
this.ensureTask(prd, taskId);
|
||||
prd.tasks[taskId].status = "failed";
|
||||
prd.tasks[taskId].error = error;
|
||||
this.save();
|
||||
}
|
||||
/** Mark a task as failed */
|
||||
markFailed(taskId: string, error: string): void {
|
||||
const prd = this.getPRD();
|
||||
this.ensureTask(prd, taskId);
|
||||
prd.tasks[taskId].status = "failed";
|
||||
prd.tasks[taskId].error = error;
|
||||
this.save();
|
||||
}
|
||||
|
||||
/** Get task status */
|
||||
getTaskStatus(taskId: string): Task["status"] {
|
||||
const prd = this.getPRD();
|
||||
return prd.tasks[taskId]?.status ?? "pending";
|
||||
}
|
||||
/** Get task status */
|
||||
getTaskStatus(taskId: string): Task["status"] {
|
||||
const prd = this.getPRD();
|
||||
return prd.tasks[taskId]?.status ?? "pending";
|
||||
}
|
||||
|
||||
/** Get IDs of all completed tasks */
|
||||
getCompletedTaskIds(): string[] {
|
||||
const prd = this.getPRD();
|
||||
return Object.entries(prd.tasks)
|
||||
.filter(([, info]) => info.status === "completed")
|
||||
.map(([id]) => id);
|
||||
}
|
||||
/** Get IDs of all completed tasks */
|
||||
getCompletedTaskIds(): string[] {
|
||||
const prd = this.getPRD();
|
||||
return Object.entries(prd.tasks)
|
||||
.filter(([, info]) => info.status === "completed")
|
||||
.map(([id]) => id);
|
||||
}
|
||||
|
||||
/** Get all reflections from completed tasks */
|
||||
getAllReflections(): Reflection[] {
|
||||
const prd = this.getPRD();
|
||||
const reflections: Reflection[] = [];
|
||||
for (const info of Object.values(prd.tasks)) {
|
||||
if (info.reflection) reflections.push(info.reflection);
|
||||
}
|
||||
return reflections;
|
||||
}
|
||||
/** Get IDs of all failed tasks */
|
||||
getFailedTaskIds(): string[] {
|
||||
const prd = this.getPRD();
|
||||
return Object.entries(prd.tasks)
|
||||
.filter(([, info]) => info.status === "failed")
|
||||
.map(([id]) => id);
|
||||
}
|
||||
|
||||
/** Get reflections for specific dependency tasks */
|
||||
getDependencyReflections(depIds: string[]): Reflection[] {
|
||||
const prd = this.getPRD();
|
||||
return depIds
|
||||
.map((id) => prd.tasks[id]?.reflection)
|
||||
.filter((r): r is Reflection => r !== undefined);
|
||||
}
|
||||
/** Get all reflections from completed tasks */
|
||||
getAllReflections(): Reflection[] {
|
||||
const prd = this.getPRD();
|
||||
const reflections: Reflection[] = [];
|
||||
for (const info of Object.values(prd.tasks)) {
|
||||
if (info.reflection) reflections.push(info.reflection);
|
||||
}
|
||||
return reflections;
|
||||
}
|
||||
|
||||
/** Increment retry count */
|
||||
incrementRetry(taskId: string): number {
|
||||
const prd = this.getPRD();
|
||||
this.ensureTask(prd, taskId);
|
||||
prd.tasks[taskId].retries++;
|
||||
this.save();
|
||||
return prd.tasks[taskId].retries;
|
||||
}
|
||||
/** Get reflections for specific dependency tasks */
|
||||
getDependencyReflections(depIds: string[]): Reflection[] {
|
||||
const prd = this.getPRD();
|
||||
return depIds
|
||||
.map((id) => prd.tasks[id]?.reflection)
|
||||
.filter((r): r is Reflection => r !== undefined);
|
||||
}
|
||||
|
||||
/** Set paused state */
|
||||
setPaused(paused: boolean): void {
|
||||
const prd = this.getPRD();
|
||||
prd.paused = paused;
|
||||
this.save();
|
||||
}
|
||||
/** Increment retry count */
|
||||
incrementRetry(taskId: string): number {
|
||||
const prd = this.getPRD();
|
||||
this.ensureTask(prd, taskId);
|
||||
prd.tasks[taskId].retries++;
|
||||
this.save();
|
||||
return prd.tasks[taskId].retries;
|
||||
}
|
||||
|
||||
/** Get the raw PRD state (for status display) */
|
||||
getState(): PRDProgress {
|
||||
return this.getPRD();
|
||||
}
|
||||
/** Set paused state */
|
||||
setPaused(paused: boolean): void {
|
||||
const prd = this.getPRD();
|
||||
prd.paused = paused;
|
||||
this.save();
|
||||
}
|
||||
|
||||
/** Get all PRDs (for multi-PRD status display) */
|
||||
getAllPRDs(): Record<string, PRDProgress> {
|
||||
return this.state.prds ?? {};
|
||||
}
|
||||
/** Get the raw PRD state (for status display) */
|
||||
getState(): PRDProgress {
|
||||
return this.getPRD();
|
||||
}
|
||||
|
||||
/** Get the PRD key for this tracker */
|
||||
getKey(): string {
|
||||
return this.prdKey;
|
||||
}
|
||||
/** Get all PRDs (for multi-PRD status display) */
|
||||
getAllPRDs(): Record<string, PRDProgress> {
|
||||
return this.state.prds ?? {};
|
||||
}
|
||||
|
||||
/** Reset all progress for this PRD */
|
||||
reset(): void {
|
||||
const prd = this.getPRD();
|
||||
Object.assign(prd, this.freshPRD(prd.sourcePath));
|
||||
this.save();
|
||||
}
|
||||
/** Get the PRD key for this tracker */
|
||||
getKey(): string {
|
||||
return this.prdKey;
|
||||
}
|
||||
|
||||
private ensureTask(prd: PRDProgress, taskId: string): void {
|
||||
if (!prd.tasks[taskId]) {
|
||||
prd.tasks[taskId] = { status: "pending", retries: 0 };
|
||||
}
|
||||
}
|
||||
/** Reset all progress for this PRD */
|
||||
reset(): void {
|
||||
const prd = this.getPRD();
|
||||
Object.assign(prd, this.freshPRD(prd.sourcePath));
|
||||
this.save();
|
||||
}
|
||||
|
||||
private ensureTask(prd: PRDProgress, taskId: string): void {
|
||||
if (!prd.tasks[taskId]) {
|
||||
prd.tasks[taskId] = { status: "pending", retries: 0 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user