fix: simplify goal reminders and review lifecycle

This commit is contained in:
wassname2
2026-09-14 13:54:31 +08:00
parent 52d7c463d9
commit acbe21fc93
6 changed files with 227 additions and 151 deletions
+64 -41
View File
@@ -1,7 +1,7 @@
// Pi/OpenAI: Plan and supervise in the main chat; delegate implementation to a visible worker.
import { createHash, randomUUID } from "node:crypto";
import { existsSync, type FSWatcher, mkdirSync, readFileSync, watch, writeFileSync } from "node:fs";
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
import { createHash } from "node:crypto";
import { type FSWatcher, mkdirSync, readdirSync, readFileSync, watch, writeFileSync } from "node:fs";
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
import { type ExtensionAPI, type ExtensionContext, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
import { CronStorage } from "pi-schedule-prompt/src/storage.js";
import { Type } from "typebox";
@@ -70,6 +70,7 @@ function goals(text: string) {
return [{ subject: match[2].trim(), status: (box === "x" ? "done" : box === "/" ? "active" : box === "-" ? "cancelled" : "open") as GoalStatus, index }];
});
}
const requirements = (text: string) => goals(text).map(g => goalAcceptanceSignature(text, g.subject)).join("\n");
const result = (text: string) => ({ content: [{ type: "text" as const, text }], details: {} });
export default function mainSupervisor(pi: ExtensionAPI) {
@@ -103,13 +104,18 @@ export default function mainSupervisor(pi: ExtensionAPI) {
const clearChangedFinalReview = (text: string) => {
if (!state.finalReview || state.finalReview.planDigest === digest(text)) return false;
state.finalReview = undefined;
finalReviewTurnDigest = undefined;
save();
return true;
};
let turnsStale = 0;
let upkeepRound = 0;
let lastWorkingSet = "";
let pendingPlanNotice: string | undefined;
let pendingUpkeep: { generation: number; workingSet: string } | undefined;
const unfinishedGoals = (text: string) => foldPlan(text).split("\n").filter(line => {
const match = GOAL_LINE.exec(line);
return match && match[1] !== "-" && !(match[1].toLowerCase() === "x" && state.signoffs[key(match[2])] && state.signoffs[key(match[2])].signature === goalAcceptanceSignature(text, match[2]));
}).join("\n");
const checkIn = (ctx: ExtensionContext) => scheduleCheckIn(ctx.sessionManager.getSessionId(), state.plan ?? "");
const hasScheduleTool = () => pi.getAllTools().some((tool) => tool.name === "schedule_prompt");
const notedPlanValue = (prefix: string) => {
@@ -134,7 +140,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
}
const accepted = items.filter((g) => g.status === "done" && state.signoffs[key(g.subject)]).length;
ctx.ui.setStatus("goals", `👀 ${accepted}/${items.length} goals`);
const mark = (status: GoalStatus) => status === "done" ? "✔" : status === "active" ? "◼" : status === "cancelled" ? "✗" : "◻";
const mark = (status: GoalStatus) => status === "done" ? "✓" : status === "active" ? "◼" : status === "cancelled" ? "✗" : "◻";
const priority: Record<GoalStatus, number> = { active: 0, open: 1, done: 2, cancelled: 3 };
const sorted = [...items].sort((a, b) => priority[a.status] - priority[b.status]);
const visible = sorted.slice(0, WIDGET_GOAL_LIMIT);
@@ -147,9 +153,11 @@ export default function mainSupervisor(pi: ExtensionAPI) {
}).filter(Boolean);
lines.push(`… ${counts.join(", ")}`);
}
lines.unshift(relative(ctx.cwd, state.plan!)); // Readable path fallback; terminal link activation is not verified.
ctx.ui.setWidget("goals", lines);
}
function watchPlan(ctx: ExtensionContext) {
pendingPlanNotice = undefined;
planWatcher?.close();
planWatcher = undefined;
clearTimeout(planEditTimer);
@@ -160,8 +168,8 @@ export default function mainSupervisor(pi: ExtensionAPI) {
const stamp = generation;
// Watch the directory so atomic plan replacement remains observable. This is an event hook:
// plan-change reviews, not another scheduled loop (the hourly job is schedule_prompt's). A
// short debounce coalesces bursts. Existing high-level plan views exclude maintenance
// (tasks/evidence/Log) while preserving requirement wording and goal checkbox claims.
// short debounce coalesces bursts. The notification view excludes Log and worker identity;
// requirement changes additionally request active-plan context.
try {
planWatcher = watch(dirname(state.plan), { persistent: false }, () => {
if (stamp !== generation) return;
@@ -177,7 +185,8 @@ export default function mainSupervisor(pi: ExtensionAPI) {
if (hash === planHash) return;
planHash = hash;
notice = true;
send(planChangedReview(state.plan!, snapshot.text));
fullPlanContextDue ||= requirements(snapshot.text) !== requirements(lastWorkingSet);
if (!pendingPlanNotice) { pendingPlanNotice = planChangedReview(state.plan!); send(pendingPlanNotice); }
}, 150);
});
planWatcher.on("error", (error) => { planWatcher?.close(); planWatcher = undefined; ctx.ui.notify(`Plan monitoring failed: ${error.message}`, "error"); });
@@ -199,7 +208,6 @@ export default function mainSupervisor(pi: ExtensionAPI) {
state.helpers ??= []; // sessions persisted before helper bookkeeping
notice = true;
turnsStale = 0;
upkeepRound = 0;
lastWorkingSet = "";
pendingUpkeep = undefined;
finalReviewTurnDigest = undefined;
@@ -234,10 +242,10 @@ export default function mainSupervisor(pi: ExtensionAPI) {
}
function enterSolo(ctx: ExtensionContext) {
state.mode = "solo"; state.workerStopped = true;
generation++; notice = true; save(); refresh(ctx); watchPlan(ctx);
generation++; notice = true; fullPlanContextDue = true; save(); refresh(ctx); watchPlan(ctx);
send(`${removeGoalSchedule(ctx.sessionManager.getSessionId())}\n\n${soloNotice(state.plan!)}`);
}
const help = "/goals new [initial idea] | edit | discuss | review | ready | status | stop | resume | solo | attach <plan.md> [solo] | model <model> | quit (exit/clear)\n/subagents opens the worker controls. Stop pauses work. Quit/exit/clear backs up the plan and clears goal state without a model call; worker processes are unchanged. No forced compaction or model switch; the worker pane's own model is chosen with /model in that pane. Hourly check-ins are one session-bound schedule_prompt job; plan-change reviews are the plan-watcher event hook.";
const help = "/goals new [initial idea] | edit | discuss | review | ready | status | stop | resume | solo | attach <plan.md> [solo] | model <model> | quit (exit/clear)\n/subagents opens the worker controls. Stop pauses work. Quit/exit/clear preserves the plan and clears goal state without a model call; worker processes are unchanged. No forced compaction or model switch; the worker pane's own model is chosen with /model in that pane. Hourly check-ins are one session-bound schedule_prompt job; plan-change reviews are the plan-watcher event hook.";
async function ready(ctx: ExtensionContext, menu: boolean, edit = false) {
if (state.mode !== "planning") { ctx.ui.notify("Ready applies to a draft; use status or resume.", "warning"); return; }
const text = planText();
@@ -249,7 +257,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
if (menu || edit) {
const choice = edit ? "Edit" : await ctx.ui.select(`Review ${state.plan}`, ["Ready", "Discuss", "Edit", "Cancel"]);
if (stamp !== generation || digest(planText()) !== digest(text)) { ctx.ui.notify("Plan changed during review. Review it again.", "warning"); return; }
if (choice === "Discuss") { send(discuss); return; }
if (choice === "Discuss") { ctx.ui.notify(discuss, "info"); return; }
if (choice === "Edit") {
const edited = await ctx.ui.editor("Edit goal plan", text);
if (edited !== undefined && stamp === generation && planText() === text && state.plan) { writeFileSync(state.plan, edited); refresh(ctx); }
@@ -258,7 +266,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
if (choice !== "Ready") return;
}
if (!compatible()) { ctx.ui.notify("Requires edxeth/pi-subagents 2.9.x, not nicobailon/pi-subagents. Draft preserved; /goals solo is available.", "error"); return; }
state.mode = "supervising"; generation++; notice = true; save(); refresh(ctx); watchPlan(ctx);
state.mode = "supervising"; generation++; notice = true; fullPlanContextDue = true; save(); refresh(ctx); watchPlan(ctx);
send(`${checkIn(ctx)}\n\n${readyApproved(WORKER, state.plan!, state.worker?.sessionFile, text, ctx.sessionManager.getSessionId())}`);
}
@@ -269,21 +277,31 @@ export default function mainSupervisor(pi: ExtensionAPI) {
// Defer to prompt preparation: same-run continuation retains Pi's current role/context.
pi.on("session_compact", () => { notice = true; fullPlanContextDue = true; });
pi.on("turn_end", (_event, ctx) => {
finalReviewTurnDigest = undefined;
if (!["supervising", "solo"].includes(state.mode)) return;
const snapshot = readPlan();
if (snapshot.text === undefined) { notice = true; return; }
const workingSet = foldPlan(snapshot.text);
fullPlanContextDue ||= requirements(workingSet) !== requirements(lastWorkingSet);
turnsStale = workingSet === lastWorkingSet ? turnsStale + 1 : 0;
lastWorkingSet = workingSet;
refresh(ctx);
if (turnsStale === 8 && goals(snapshot.text).some(g => g.status === "open" || g.status === "active")) {
if (turnsStale === 8 && unfinishedGoals(snapshot.text)) {
// In Pi 0.85.1 triggerTurn:false updates saved history, not the live loop snapshot.
// Queue intent locally until ordinary prompt preparation, never force another turn.
pendingUpkeep = { generation, workingSet };
}
});
pi.on("agent_end", (_e, ctx) => { refresh(ctx); if (!planWatcher && state.mode === "supervising") watchPlan(ctx); });
// Queued follow-ups can be consumed inside the same run, without before_agent_start.
pi.on("message_end", (event) => {
if (event.message.role !== "user") return;
const content = typeof event.message.content === "string" ? event.message.content : event.message.content.filter(part => part.type === "text").map(part => part.text).join("\n");
if (pendingPlanNotice && content === `[pi-goals]\n${pendingPlanNotice}`) pendingPlanNotice = undefined;
if (!state.finalReview || !["supervising", "solo"].includes(state.mode)) return;
const snapshot = readPlan();
if (snapshot.text === undefined || state.finalReview.planDigest !== digest(snapshot.text)) return;
if (content === `[pi-goals]\n${finalReview(state.plan!, snapshot.text)}`) finalReviewTurnDigest = state.finalReview.planDigest;
});
pi.on("agent_end", (_e, ctx) => { finalReviewTurnDigest = undefined; refresh(ctx); if (!planWatcher && state.mode === "supervising") watchPlan(ctx); });
let proposedDraft = "";
let proposing = false;
pi.on("agent_settled", async (_e, ctx) => {
@@ -310,22 +328,23 @@ export default function mainSupervisor(pi: ExtensionAPI) {
const role = state.child ? childPlanRole : state.mode === "supervising"
? supervisor(WORKER, state.plan!, ctx.sessionManager.getSessionId())
: state.mode === "planning" ? planning(state.plan!) : state.mode === "paused" ? pausedRole : soloRole;
const pendingFinalReview = state.finalReview;
fullPlanContextDue ||= requirements(snapshot.text) !== requirements(lastWorkingSet);
const pendingFinalReview = ["supervising", "solo"].includes(state.mode) ? state.finalReview : undefined;
if (pendingFinalReview) finalReviewTurnDigest = pendingFinalReview.planDigest;
// Returned messages enter both Pi's prompt snapshot and saved history together.
// Unlike nextTurn, retaining intent here lets a fresh plan resync supersede upkeep,
// and drops obsolete reminders after edits, takeover, pause or session navigation.
const message = pendingFinalReview
? { customType: "pi-goals-final-review", content: finalReview(state.plan!, snapshot.text), display: false }
: notice
? { customType: "pi-goals-plan", content: planContext(state.child ? "worker" : state.mode, state.plan, snapshot.text, fullPlanContextDue ? "full" : "short"), display: false }
: notice || fullPlanContextDue
? { customType: "pi-goals-plan", content: planContext(state.child ? "worker" : state.mode, state.plan, fullPlanContextDue ? snapshot.text : unfinishedGoals(snapshot.text), fullPlanContextDue ? "full" : "short"), display: false }
: pendingUpkeep?.generation === generation && pendingUpkeep.workingSet === foldPlan(snapshot.text)
&& ["supervising", "solo"].includes(state.mode) && goals(snapshot.text).some(g => g.status === "open" || g.status === "active")
? { customType: "pi-goals-upkeep", content: upkeep(state.plan!, snapshot.text, state.mode === "supervising" ? upkeepRound : undefined), display: false } : undefined;
if (message?.customType === "pi-goals-upkeep" && state.mode === "supervising") upkeepRound++;
&& ["supervising", "solo"].includes(state.mode) && unfinishedGoals(snapshot.text)
? { customType: "pi-goals-upkeep", content: upkeep(state.plan!, unfinishedGoals(snapshot.text)), display: false } : undefined;
if (message) turnsStale = 0;
notice = false;
fullPlanContextDue = false;
lastWorkingSet = foldPlan(snapshot.text);
pendingUpkeep = undefined;
return { systemPrompt: `${event.systemPrompt}\n\n${role}`, ...(message ? { message } : {}) };
});
@@ -375,7 +394,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
if (!command) {
const actions = [
...({
chat: ["new — New plan", "attach — Open plan…"],
chat: ["new — New plan…", "attach — Open plan…"],
planning: ["edit — Edit plan…", "discuss — Discuss changes to the plan", "ready — Approve draft"],
supervising: ["review — Check progress", "stop — Pause work"],
paused: ["resume — Resume work"],
@@ -388,6 +407,11 @@ export default function mainSupervisor(pi: ExtensionAPI) {
const choice = await ctx.ui.select("Goal plan actions", actions);
if (!choice || before !== generation) return;
command = choice.split(" — ")[0];
if (command === "new") {
const value = await ctx.ui.editor("Planning instructions (optional; blank uses this conversation)", "");
if (value === undefined || before !== generation) return;
command += ` ${value.trim()}`;
}
if (["attach", "model"].includes(command)) {
const value = await ctx.ui.editor(command === "attach" ? "Plan path (optional: solo)" : "Worker model (provider/model)", "");
if (!value?.trim() || before !== generation) return;
@@ -412,9 +436,9 @@ export default function mainSupervisor(pi: ExtensionAPI) {
}
if (command === "discuss") {
if (state.mode !== "planning") { ctx.ui.notify("Discuss applies to a draft.", "warning"); return; }
send(discuss); return;
ctx.ui.notify(discuss, "info"); return;
}
if (command === "review" && state.mode === "supervising") { notice = true; send(manualReview(state.plan ?? "", planText())); return; }
if (command === "review" && state.mode === "supervising") { send(manualReview(state.plan ?? "", unfinishedGoals(planText()))); return; }
if (command === "edit" || command === "review" || command === "ready") { await ready(ctx, command === "review", command === "edit"); return; }
if (command === "model" || command.startsWith("model ")) {
if (!state.plan || !goals(planText()).length) { ctx.ui.notify("Register a goal plan first.", "warning"); return; }
@@ -447,30 +471,31 @@ export default function mainSupervisor(pi: ExtensionAPI) {
const retained = target === state.plan ? state.signoffs : {};
const worker = noted ? { sessionFile: resolve(ctx.cwd, noted) } : state.workerStopped ? state.worker : undefined;
state = { mode: solo ? "solo" : "planning", plan: target, signoffs: retained, worker, helpers: [], workerStopped: solo || (!noted && state.workerStopped) };
generation++; notice = true; save(); refresh(ctx); watchPlan(ctx);
generation++; notice = true; fullPlanContextDue = true; save(); refresh(ctx); watchPlan(ctx);
if (solo) enterSolo(ctx);
else send(attachNotice(target, false, noted));
return;
}
if (command === "exit") {
const backup = state.plan && existsSync(state.plan) ? `${state.plan}.${randomUUID()}.bak` : undefined;
if (backup) writeFileSync(backup, readFileSync(state.plan!), { flag: "wx" });
const storage = new CronStorage(ctx.cwd);
const session = ctx.sessionManager.getSessionId();
for (const job of storage.getAllJobs().filter(j => j.name === `goals-${session}` && j.session === session)) {
const matching = storage.getAllJobs().filter(j => j.name === `goals-${session}`);
const skipped = matching.filter(j => j.session !== session);
if (skipped.length) ctx.ui.notify(`Goal check-ins left unchanged (session binding missing or different): ${skipped.map(j => j.id).join(", ")}. Inspect /schedule-prompt.`, "warning");
for (const job of matching.filter(j => j.session === session)) {
storage.removeJob(job.id); // Scheduler re-reads storage before firing; removed jobs cannot prompt.
pi.events.emit("cron:change", { type: "remove", jobId: job.id });
}
state = initial(); generation++; workerRevision++; pendingLaunches.clear(); pendingUpkeep = undefined; notice = true;
save(); refresh(ctx); watchPlan(ctx);
ctx.ui.notify(`Goals cleared.${backup ? ` Plan backed up to ${backup}.` : ""}`, "info");
ctx.ui.notify("Goals cleared; original plan file unchanged.", "info");
return;
}
if (command === "stop") {
if (state.mode === "planning") { ctx.ui.notify("A draft cannot pause; use /goals quit to back up and clear it.", "warning"); return; }
if (state.mode === "planning") { ctx.ui.notify("A draft cannot pause; use /goals quit to clear goal state and preserve the draft.", "warning"); return; }
if (state.mode !== "solo" && state.mode !== "supervising") return;
state.pausedFrom = state.mode;
state.mode = "paused"; generation++; notice = true; save(); refresh(ctx); watchPlan(ctx);
state.mode = "paused"; generation++; notice = true; fullPlanContextDue = true; save(); refresh(ctx); watchPlan(ctx);
const pause = pauseExitNotice(state.worker, false);
const requestCleanup = Boolean(state.worker) || hasScheduleTool();
if (!requestCleanup) ctx.ui.notify(pause, "info"); // Visible now; passive model context waits for a prompt.
@@ -481,7 +506,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
if (state.mode !== "paused" || !state.plan) { ctx.ui.notify("Only a paused approved plan can resume. A draft needs Ready.", "warning"); return; }
if (state.pausedFrom === "solo") { enterSolo(ctx); return; }
if (!compatible()) { ctx.ui.notify("edxeth tools unavailable; plan remains paused.", "error"); return; }
state.mode = "supervising"; generation++; notice = true; save(); refresh(ctx); watchPlan(ctx);
state.mode = "supervising"; generation++; notice = true; fullPlanContextDue = true; save(); refresh(ctx); watchPlan(ctx);
send(`${checkIn(ctx)}\n\n${resumeNotice(WORKER, state.plan, state.worker)}`);
return;
}
@@ -496,18 +521,18 @@ export default function mainSupervisor(pi: ExtensionAPI) {
if ((state.worker && !state.workerStopped) || state.mode === "supervising") { ctx.ui.notify("Exit and resolve the existing worker before replacing the plan. The current plan is preserved.", "warning"); return; }
const planDir = join(ctx.cwd, ".pi", "plan");
mkdirSync(planDir, { recursive: true });
const timestamp = new Date().toISOString().replace("T", "-").replace(/:/g, "").replace(/\.\d{3}Z$/, "Z");
const slug = (objective.toLowerCase().normalize("NFKD").replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 48) || "goal-plan");
let version = 1;
const suffix = ctx.sessionManager.getSessionId().slice(-6);
const pattern = new RegExp(`^${suffix}-v(\\d+)\\.md$`);
let version = 1 + Math.max(0, ...readdirSync(planDir).map(name => Number(pattern.exec(name)?.[1] ?? 0)));
let path: string;
for (;;) {
path = join(planDir, `${timestamp}-${slug}-v${version}.md`);
path = join(planDir, `${suffix}-v${version}.md`);
try { writeFileSync(path, planDocument(objective), { flag: "wx" }); break; } catch (error) {
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
version++;
}
}
state = { mode: "planning", plan: path, signoffs: {}, worker: state.worker, helpers: state.helpers, workerStopped: state.workerStopped }; generation++; notice = true; save(); refresh(ctx); watchPlan(ctx);
state = { mode: "planning", plan: path, signoffs: {}, worker: state.worker, helpers: state.helpers, workerStopped: state.workerStopped }; generation++; notice = true; fullPlanContextDue = true; save(); refresh(ctx); watchPlan(ctx);
send(planningSeed(objective, path));
} catch (error) { ctx.ui.notify(String(error), "error"); }
},
@@ -522,7 +547,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
const items = goals(readFileSync(params.path, "utf8"));
if (!items.length || items.some(g => !g.subject)) return result(messages.invalidAttachment);
} catch { return result(messages.invalidAttachment); }
state.plan = params.path; generation++; notice = true; save(); refresh(ctx);
state.plan = params.path; generation++; notice = true; fullPlanContextDue = true; save(); refresh(ctx);
return result(childPlanAttached(params.path));
},
});
@@ -553,8 +578,6 @@ export default function mainSupervisor(pi: ExtensionAPI) {
if (finalReviewTurnDigest !== digest(text)) {
if (!state.finalReview) {
state.finalReview = { planDigest: digest(text) };
notice = true;
fullPlanContextDue = true;
save();
send(finalReview(path, text));
}
+24 -39
View File
@@ -1,5 +1,13 @@
// Pi/OpenAI: Planning, approval, supervision, reminders, completion and recovery.
import { foldPlan, planContextView } from "./plan.js";
import { createHash } from "node:crypto";
import { foldPlan, GOAL_LINE } from "./plan.js";
// Quote the existing selection verbatim; a longer fence also contains nested Markdown fences.
function quotedPlan(path: string | undefined, text: string, selection: string): string {
const fence = "`".repeat(Math.max(3, ...Array.from(text.matchAll(/`+/g), match => match[0].length + 1)));
const label = selection === "full" ? "Full plan snapshot" : `Plan excerpt (${selection})`;
return `${label} from ${JSON.stringify(path ?? "not attached")}:\n${fence}md\n${text}\n${fence}`;
}
export const planDrafting = `\
You are in plan mode. Help the user express what they want this project to achieve in a short judgeable plan. Seek to understand their underlying goals, infer ordinary details, and use their applicable AGENTS.md instructions, relevant skills, and project context to interpret the request correctly. Do not silently substitute your own goals or expand the agreed scope.
@@ -138,10 +146,10 @@ export function planning(planPath: string): string {
return `Plan only in ${planPath}; do not implement or launch workers before Ready. Ask material unresolved questions, not a quota or confirmation of ordinary details. Record unknowns and present Ready when the outcome, scope and spending are settled. Preserve the user's exact deliverable, preferences and voice. Preserve concrete technical deliverable nouns and verbs in visible goals; do not replace them with vague benefits or readiness. Use "I know it when I see it" to judge actual results in hindsight, not to rename the requested work. Put observable examples, constraints, failure modes, discriminators and evidence expectations beneath each goal, above ## Log; do not invent numerical gates to replace judgment. Record the requested worker model in preferences. When your drafted plan is ready for human review, finish your turn; the interface displays the draft and approval choices automatically. Do not ask the user to type a command to see the proposal. /goals review reopens it on request; /goals exit preserves the draft.`;
}
export function planningSeed(objective: string, planPath: string): string {
return `Enter a planning conversation focused on the user's goals. ${objective ? `Initial idea: ${objective}.` : "Ask what the user wants to achieve; they do not need to supply a finished objective."} Read any existing plan at ${planPath} first, then discuss and draft it with the user. Do not infer approval to implement from starting this conversation. ${planning(planPath)}\n\n${planDrafting}`;
return `Enter a planning conversation focused on the user's goals. ${objective ? `Initial idea: ${objective}.` : "Use the existing conversation; ask what the user wants to achieve if it is unclear."} Read any existing plan at ${planPath} first, then discuss and draft it with the user. Do not infer approval to implement from starting this conversation. ${planning(planPath)}\n\n${planDrafting}`;
}
export const planDocument = (objective: string) => `# Goal plan\n\n## Objective\n${objective}\n\n## Goals\n\n## Log\n`;
export const discuss = "Discuss the current draft in ordinary chat. Do not launch a worker. An unchanged draft does not reopen the review menu; a changed settled draft does.";
export const planDocument = (objective: string) => `# ${objective.split("\n")[0] || "Goal plan"}\n\n## Objective\n${objective}\n\n## Goals\n\n## Log\n`;
export const discuss = "Type your changes in chat; the draft stays open.";
// Ready and explicit child attachment: stock lineage-only sessions do not inherit the shared plan.
export const attachGoalPlanDescription = "Delegated goals-worker only: attach the absolute plan path explicitly supplied in your task. Read it without rewriting it. Restores plan context; grants no parent completion authority. No discovery or worker launch.";
@@ -150,7 +158,7 @@ export function readyApproved(workerName: string, planPath: string, notedWorker:
const launch = notedWorker
? `Inspect the recorded worker session ${notedWorker}; if still live, let it continue or message it. Only after confirming it stopped use subagent_resume with that sessionFile. Never restart completed work.`
: `Delegate the first unfinished goal to agent '${workerName}' with subagent; provide name, title and a bounded task.`;
return `Ready approved this plan: ${planPath}. Stay here as supervisor. ${launch} Include the absolute plan path, require AttachGoalPlan, and first use intercom status/list to discover and confirm your own actual Intercom UUID, then give that address to the child. Your Pi session ID is ${supervisorId}; it is not necessarily your Intercom UUID. The child sends its completion report there and stays open. Require an initial worker report with its actual Intercom UUID, saved-session path and current provider/model; the async launch may return only a runtime ID. Record each distinct identity in plan preferences, marking child-reported fields as such until verified. Do not start a second writer. Inspect actual outputs when the child reports.\n\n${foldPlan(plan)}`;
return `[pi-goals: approval — Ready]\nReady approved this plan: ${planPath}. Stay here as supervisor. ${launch} Include the absolute plan path, require AttachGoalPlan, and first use intercom status/list to discover and confirm your own actual Intercom UUID, then give that address to the child. Your Pi session ID is ${supervisorId}; it is not necessarily your Intercom UUID. The child sends its completion report there and stays open. Require an initial worker report with its actual Intercom UUID, saved-session path and current provider/model; the async launch may return only a runtime ID. Record each distinct identity in plan preferences, marking child-reported fields as such until verified. Do not start a second writer. Inspect actual outputs when the child reports.\n\n${quotedPlan(planPath, foldPlan(plan), "working set before Log")}`;
}
// Supervision and turn-event upkeep (not a scheduled wake-up).
@@ -164,45 +172,22 @@ Take uncertainty as an invitation to investigate, not something to hide. Have ro
Use stock subagent for launch and subagent_resume with the returned sessionFile only after confirming the worker stopped. A stored handle is not proof of liveness; missing runtime state is not proof it stopped. Use pi-intercom list/status to identify the actual live child session before live steering; receipt alone does not prove action. Your Pi session ID is ${supervisorId}, not necessarily your Intercom UUID. Use intercom status/list to discover and confirm your own actual Intercom UUID before supplying the worker's report address. Require its completion report through Intercom while its pane stays open. A recap alone sends no instruction. Record '- worker session:' and '- worker intercom session:' in plan preferences from actual launch results and received-message identity; never confuse the runtime ID with the Intercom ID. Ensure the child calls AttachGoalPlan with the supplied path. Inspect results before CompleteGoal, then continue only unfinished goals.
Use the worker model requested in plan preferences, verify the resolved model, and report unavailable choices instead of silently substituting. Keep normal tools, not edxeth's restricted orchestrator mode. After reload or compaction reread the plan. Failed compaction, exhausted credits or lost connection do not erase progress: diagnose the actual error, restore an available authorized model/credits and resume the same saved session; never restart long work. Stock edxeth can crash the parent when a worker exits after parent reload: preserve drafts and stop workers before /reload. If it already happened, restart the saved parent session; do not repeat completed work.`;
}
// Pi/OpenAI: user nudges plus quotes/attributions from https://github.com/wassname/ml-debug/blob/main/fortune.txt.
export const upkeepNudges = [
"is the worker stuck? (or are you)",
"Insufficient skepticism doesn't feel like insufficient skepticism from the inside. It just feels like doing research. -- Neel Nanda",
"take a breath, use a kamoji, how it going?",
"Don't let your instruments overwhelm your system. -- David J. Agans, *Debugging: The 9 Indispensable Rules*",
"is the worker being cheeky, does it need sheperding",
"The first step is just making time to stop and ask yourself: do I endorse what I'm doing, and could I be doing something better? -- Neel Nanda",
"It seems important to really commit yourself to always investigate whenever you notice confusion. -- Dan Rahtz",
"How reliable is my experiment? Ask yourself: How surprised would I be if it turned out to be complete bullshit due to a bug, error, noise, misunderstanding, etc.? Investigate the most uncertain bits. -- Neel Nanda",
"If it doesn’t work, assume there’s a bug. Spend a lot of effort searching for bugs before you resort to tweaking hyperparameters: usually it’s a bug. Bad hyperparameters can significantly degrade RL performance, but if you’re using hyperparameters similar to the ones in papers and standard implementations, those will probably not be the issue. -- Josh Achiam",
"You can't find typos in your own writing without a great deal of effort because you know what it's supposed to say. -- Gwern Branwen",
"Even a single anomaly, apparently trivial in itself, can indicate the everyday mental model is not just a little bit wrong, but fundamentally wrong. -- Gwern Branwen",
"The default state of the world is that your research is false, because doing research is hard. -- Neel Nanda",
"If you're new to RL, writing things from scratch is the most catastrophically self-sabotaging thing you can do. -- Andy Jones",
"QUIT THINKING AND LOOK. -- David J. Agans, *Debugging: The 9 Indispensable Rules*",
"Excitement is evidence of bullshit: generally, most true results are not exciting, but a fair amount of false results are. -- Neel Nanda",
"Read your data. Often, the quality of the data is a crucial driver of the results of your experiments. Often, it is quite bad. -- Neel Nanda",
"Visualize the model in action. Directly observing the machine learning model performing its task will help determine whether the quantitative performance numbers it achieves seem reasonable. -- Goodfellow, Bengio and Courville",
"The unambiguously correct place to visualize your data is immediately before y_hat = model(x). This is the only source of truth. -- Andrej Karpathy",
"Your misconfigured neural net will throw exceptions only if you're lucky; most of the time it will train but silently work a bit worse. -- Andrej Karpathy",
"The first step to training a neural net is to not touch any neural net code at all and instead begin by thoroughly inspecting your data. -- Andrej Karpathy",
"Write multiple competing hypotheses: consider the most likely failure but also some of: a subtle failure, a perverse failure, a possible bug, and an unknown. Put a rough credence on each. Finally write down what you expect to see differently for success vs each possibility and brainstorm the cheapest tests that may narrow them down. -- wassname",
];
export function upkeep(planPath: string, text: string, supervisorRound?: number): string {
const nudge = supervisorRound === undefined ? "" : `${upkeepNudges[supervisorRound % upkeepNudges.length]}\n\n`;
return `${nudge}Plan upkeep: update task ticks, evidence and Log when you have new progress to record. Preserve agreed goals and discriminators. If already reviewing evidence, finish that review rather than repeat a status recap. This turn-event reminder does not resume paused work.\n\n${planContextView(text, "medium")}\n\nPlan file (audit or edit link): ${planPath}`;
// Routine notices quote only selected goal lines; full context stops at Log.
const goalLines = (text: string) => foldPlan(text).split("\n").filter(line => GOAL_LINE.test(line)).join("\n");
export function upkeep(planPath: string, text: string): string {
return `[pi-goals: reminder — upkeep]\nEight unchanged turns: update task ticks, evidence or Log only for new progress. Finish any evidence review already underway; do not restart completed or paused work.\n\n${quotedPlan(planPath, goalLines(text), "unfinished or unreviewed goal lines")}`;
}
export function planContext(mode: string, path: string | undefined, text: string, tier: "short" | "medium" | "full" = "full"): string {
return `Current goal mode: ${mode}. Earlier role messages are historical; this current role governs.\n${planContextView(text, tier)}\n\nPlan file (audit or edit link): ${path ?? "not attached"}`;
return `[pi-goals: context resync]\nCurrent goal mode: ${mode}. Earlier role messages are historical; this current role governs. Read the plan file for details and earlier evidence; do not restart completed work.\n\n${quotedPlan(path, tier === "full" ? foldPlan(text) : goalLines(text), tier === "full" ? "active plan above Log" : "unfinished or unreviewed goal lines")}`;
}
export function planChangedReview(planPath: string, text: string): string {
return `${supervisorJob}\nPlan changed. Inspect changed requirements, completion claims and evidence. Evidence-only edits do not revoke execution approval. After review, continue unfinished authorized implementation rather than another recap; respect explicit pauses and do not assume approval for changed scope.\n\n${planContextView(text, "short")}\n\nPlan file (audit or edit link): ${planPath}. Manual checkbox edits are claims, not proof. Do not weaken the agreed goal or start a duplicate writer.`;
export function planChangedReview(planPath: string, text = ""): string {
return `[pi-goals: reminder — plan changed]\nPlan changed: inspect current requirements, completion claims and evidence at ${planPath}. Evidence-only edits do not revoke execution approval. Continue only unfinished authorized work; respect pauses and do not assume approval for changed scope. Manual ticks are claims, not sign-off. Do not start a duplicate writer.${text ? `\n\n${quotedPlan(planPath, goalLines(text), "selected goal lines")}` : ""}`;
}
export function manualReview(planPath: string, text: string): string {
return `${supervisorJob}\nReview the current plan, worker progress and actual evidence.\n\n${planContextView(text, "short")}\n\nPlan file (audit or edit link): ${planPath}. Do not launch a duplicate writer.`;
return `[pi-goals: reminder — requested review]\nReview requested: inspect the plan and actual evidence. Do not launch a duplicate writer.\n\n${quotedPlan(planPath, goalLines(text), "unfinished or unreviewed goal lines")}`;
}
export function finalReview(planPath: string, text: string): string {
return `Final completion review. The preceding CompleteGoal request did not record approval. Read the complete embedded plan, including goal requirements, evidence and Log. Inspect the cited artifacts yourself. Only after this review, call CompleteGoal again with the exact remaining goal and evidence; if the plan changed, inspect the changed plan instead.\n\n${planContextView(text, "full")}\n\nPlan file (audit or edit link): ${planPath}.`;
return `[pi-goals: reminder — final completion review]\nFinal completion review: the preceding CompleteGoal request did not record approval. Read the complete file at ${planPath}, including requirements, evidence and Log, and inspect the cited artifacts yourself. Then call CompleteGoal again with the exact remaining goal and evidence. Changed requirements need a new review. Plan revision: ${createHash("sha256").update(text).digest("hex")}.\n\n${quotedPlan(planPath, goalLines(text), "selected goal lines")}`;
}
// Check-ins. The installed scheduler owns storage/timing/UI. Removal guidance must never add jobs.
@@ -210,7 +195,7 @@ export function removeGoalSchedule(sessionId: string): string {
return `With schedule_prompt, list jobs and read .pi/schedule-prompts.json to verify ownership; tool text omits session binding. Remove by jobId only the job named ${JSON.stringify(`goals-${sessionId}`)} bound to session ${JSON.stringify(sessionId)}. Never use cleanup; leave other jobs untouched. Do not add, enable or recreate any job. If unavailable or ownership is ambiguous, report it; /schedule-prompt opens the user controls.`;
}
export function scheduleCheckIn(sessionId: string, planPath: string): string {
return `Hourly check-in is one visible schedule_prompt job; plan-change and upkeep reviews are event hooks, not another timer. List first. If an owned job named ${JSON.stringify(`goals-${sessionId}`)} already exists, retain its human-edited prompt, interval and enabled/disabled state unchanged; never recreate, overwrite or re-enable it. Only while supervising unfinished non-cancelled goals, if missing on this explicit start/resume, add one session-bound interval '1h' job with no model override. Read .pi/schedule-prompts.json and verify that new job's session is ${JSON.stringify(sessionId)}; tool text does not expose binding. If the new job is unbound, remove that job by ID and report the scope error. Do not change other jobs. Its initial prompt: ${supervisorJob} Read ${planPath} and the current goal mode. If paused, exited, solo or all non-cancelled goals reviewed, remove only this owned job without resuming work. Otherwise inspect progress and evidence, give a brief assessment and keep authorized work moving without a duplicate writer. Do not reinstall a missing job from a scheduled check-in. Users inspect/toggle/remove jobs with /schedule-prompt and edit prompt/interval through schedule_prompt update. Never use cleanup. Retain their edits, but warn that this installed scheduler deletes disabled jobs on reload/shutdown; do not promise they persist. If schedule_prompt is unavailable, report hourly check-ins unavailable; do not build a timer.`;
return `Hourly check-in is one visible schedule_prompt job; plan-change and upkeep reviews are event hooks, not another timer. List first. If an owned job named ${JSON.stringify(`goals-${sessionId}`)} already exists, retain its human-edited prompt, interval and enabled/disabled state unchanged; never recreate, overwrite or re-enable it. Only while supervising unfinished non-cancelled goals, if missing on this explicit start/resume, add one session-bound interval '1h' job with no model override. Read .pi/schedule-prompts.json and verify that new job's session is ${JSON.stringify(sessionId)}; tool text does not expose binding. If the new job is unbound, remove that job by ID and report the scope error. Do not change other jobs. Its initial prompt: Hourly goal check-in: read ${planPath} and the current goal mode. If paused, exited, solo or all non-cancelled goals reviewed, remove only this owned job without resuming work. Otherwise inspect progress and evidence, give a brief assessment and keep authorized work moving without a duplicate writer. Do not reinstall a missing job from a scheduled check-in. Users inspect/toggle/remove jobs with /schedule-prompt and edit prompt/interval through schedule_prompt update. Never use cleanup. Retain their edits, but warn that this installed scheduler deletes disabled jobs on reload/shutdown; do not promise they persist. If schedule_prompt is unavailable, report hourly check-ins unavailable; do not build a timer.`;
}
// Completion and runtime errors. Tool returns are model-facing too.
@@ -233,7 +218,7 @@ export function completionLog(goal: string, observation: string, evidence: strin
return `- ${solo ? "Solo self-verification" : "Parent review"}: ${JSON.stringify(goal)}; ${JSON.stringify(observation)}; evidence ${JSON.stringify(evidence)}`;
}
export function finalReviewQueued(goal: string): string {
return `Final review queued for ${goal}; no sign-off recorded. Read the complete embedded plan and actual evidence in that review turn, then call CompleteGoal again with the exact goal and evidence.`;
return `Final review queued for ${goal}; no sign-off recorded. Read the complete plan file and actual evidence in that review run, then call CompleteGoal again with the exact goal and evidence.`;
}
export const finalReviewInvalidated = "The plan changed since the final review was queued; no sign-off recorded. Inspect the current plan and request completion again to queue a new final review.";
export function completionResult(goal: string, sessionId: string, remaining: boolean, solo: boolean): string {