Clean pi-plan references, add judge timeout, fix heading format

- Rename spec doc to 2026-06-15_pi-goals.md, update title
- Update review.md spec reference
- Rename piPlanExtension -> piGoalsExtension in src/index.ts
- Add 120s timeout to judge subprocess (was unbounded, caused hang)
- Change planInjection heading from 'Goals (goals.md):' to '.pi/goals.md:'
- Add FIXMEs for tool label, progress visibility, heading format
This commit is contained in:
wassname
2026-06-17 18:09:03 +08:00
parent 0a1503dc04
commit 489f9b8c35
4 changed files with 40 additions and 7 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
Code review against spec `docs/spec/2026-06-15_pi-plan.md`. Code review against spec `docs/spec/2026-06-15_pi-goals.md`.
--- ---
@@ -1,4 +1,4 @@
# pi-plan — design spec # pi-goals — design spec
Working title. A pi extension: set up goals (with subtasks and evidence) through plan mode, work them autonomously, and sign a goal off only when a check passes. One markdown file holds everything. The form guides a process; it does not police one. Successor to `pi-lgtm`, deliberately smaller. Working title. A pi extension: set up goals (with subtasks and evidence) through plan mode, work them autonomously, and sign a goal off only when a check passes. One markdown file holds everything. The form guides a process; it does not police one. Successor to `pi-lgtm`, deliberately smaller.
+34 -3
View File
@@ -84,7 +84,7 @@ interface PlanState {
judgeModel: string | null; judgeModel: string | null;
} }
export default function piPlanExtension(pi: ExtensionAPI): void { export default function piGoalsExtension(pi: ExtensionAPI): void {
let state: PlanState = { isPlanMode: false, objective: null, judgeModel: null }; let state: PlanState = { isPlanMode: false, objective: null, judgeModel: null };
// Reminder cadence: fire when an active goal exists but goals.md was not touched since last turn. // Reminder cadence: fire when an active goal exists but goals.md was not touched since last turn.
let lastInjectedPlan = ""; let lastInjectedPlan = "";
@@ -270,6 +270,12 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
pi.registerTool({ pi.registerTool({
name: "CompleteGoal", name: "CompleteGoal",
// FIXME(label): "Complete goal" is ambiguous in the TUI — the user sees it running with no
// hint that it spawns a read-only subagent. Change to "Subagent goal signoff" or similar so the
// running tool reads as an action, not just a checkbox flip.
// FIXME(progress): while the judge runs (potentially 10-120s), the user only sees "Working". Surface
// judge progress — e.g. set the tool label to "Subagent goal signoff (judging...)" via onUpdate,
// or emit a status notification so the user knows what's happening and can tell a hang from slowness.
label: "Complete goal", label: "Complete goal",
description: completeGoalDescription, description: completeGoalDescription,
parameters: Type.Object({ parameters: Type.Object({
@@ -451,13 +457,38 @@ async function runJudge(
args.push(task); args.push(task);
const inv = getPiInvocation(args); const inv = getPiInvocation(args);
// FIXME(hang): no timeout — if the judge subprocess stalls, this promise never resolves and the
// user sees "Working" indefinitely with no way to know what's happening. Add a setTimeout that
// resolves with a reject verdict after, say, 120s, and surface progress (e.g. stderr lines) so
// the user can tell the judge is still alive. See also: no progress indication in TUI while judging.
const JUDGE_TIMEOUT_MS = 120_000;
const output = await new Promise<string>((resolve) => { const output = await new Promise<string>((resolve) => {
let settled = false;
const timer = setTimeout(() => {
if (!settled) {
settled = true;
proc.kill();
resolve(`VERDICT: reject\nmissing: judge timed out after ${JUDGE_TIMEOUT_MS / 1000}s — the subagent did not return a verdict`);
}
}, JUDGE_TIMEOUT_MS);
const proc = spawn(inv.command, inv.args, { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"], signal }); const proc = spawn(inv.command, inv.args, { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"], signal });
let out = ""; let out = "";
proc.stdout.on("data", (d) => (out += d)); proc.stdout.on("data", (d) => (out += d));
proc.stderr.on("data", (d) => (out += d)); proc.stderr.on("data", (d) => (out += d));
proc.on("close", () => resolve(out)); proc.on("close", () => {
proc.on("error", (e) => resolve(`VERDICT: reject\nmissing: judge subprocess failed: ${e.message}`)); if (!settled) {
settled = true;
clearTimeout(timer);
resolve(out);
}
});
proc.on("error", (e) => {
if (!settled) {
settled = true;
clearTimeout(timer);
resolve(`VERDICT: reject\nmissing: judge subprocess failed: ${e.message}`);
}
});
}); });
// The subprocess emits ANSI/CSI control codes in -p mode; strip them so they don't leak into `missing`. // The subprocess emits ANSI/CSI control codes in -p mode; strip them so they don't leak into `missing`.
+4 -2
View File
@@ -117,14 +117,16 @@ export function planInjection(p: {
counts: { done: number; open: number }; counts: { done: number; open: number };
}): string { }): string {
if (!p.activeGoal) { if (!p.activeGoal) {
return `Goals (goals.md): ${p.title}\nNo active goal. ${p.counts.open} open, ${p.counts.done} done. Pick the next goal (set its checkbox to [/]) or run /goals.`; // FIXME(heading): user wants the heading to show ".pi/goals.md: <title>" so the filename is explicit
// even in the injection. Currently says "Goals (goals.md):" which is close but not the same.
return `.pi/goals.md: ${p.title}\nNo active goal. ${p.counts.open} open, ${p.counts.done} done. Pick the next goal (set its checkbox to [/]) or run /goals.`;
} }
const subtasks = p.activeGoal.openSubtasks.length const subtasks = p.activeGoal.openSubtasks.length
? p.activeGoal.openSubtasks.map((s) => ` - [ ] ${s}`).join("\n") ? p.activeGoal.openSubtasks.map((s) => ` - [ ] ${s}`).join("\n")
: " (no open subtasks)"; : " (no open subtasks)";
const disc = p.activeGoal.discriminator.length ? p.activeGoal.discriminator.join("; ") : "(none set)"; const disc = p.activeGoal.discriminator.length ? p.activeGoal.discriminator.join("; ") : "(none set)";
return `\ return `\
Goals (goals.md): ${p.title} .pi/goals.md: ${p.title}
Active goal: ${p.activeGoal.subject} Active goal: ${p.activeGoal.subject}
discriminator (the success test): ${disc} discriminator (the success test): ${disc}
Open subtasks: Open subtasks: