mirror of
https://github.com/wassname/pi-plan.git
synced 2026-09-26 14:10:23 +08:00
Fix goal state recovery, approval invalidation and worker tracking
Unify Log boundaries, serialize completion edits, preserve immutable session state and solo recovery, and match stock worker execution events. Remove unused plan views and brittle prose checks; correct installation and context guidance. Co-Authored-By: Pi/OpenAI <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
+70
-49
@@ -2,10 +2,10 @@
|
||||
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 type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
||||
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";
|
||||
import { foldPlan, GOAL_LINE } from "./plan.js";
|
||||
import { FOLD_LINE, foldPlan, GOAL_LINE, goalAcceptanceSignature } from "./plan.js";
|
||||
import { planViews } from "./plan-view.js";
|
||||
import {
|
||||
attachGoalPlanDescription,
|
||||
@@ -49,7 +49,8 @@ interface State {
|
||||
worker?: { id?: string; sessionFile: string };
|
||||
helpers: { id?: string; sessionFile: string }[];
|
||||
workerStopped?: boolean;
|
||||
signoffs: Record<string, { evidence: string[]; observation: string }>;
|
||||
pausedFrom?: "solo" | "supervising";
|
||||
signoffs: Record<string, { evidence: string[]; observation: string; signature: string }>;
|
||||
child?: boolean;
|
||||
}
|
||||
const initial = (): State => ({ mode: "chat", helpers: [], signoffs: {} });
|
||||
@@ -69,13 +70,13 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
let state = initial();
|
||||
let generation = 0;
|
||||
let workerRevision = 0;
|
||||
let pendingLaunches = 0;
|
||||
const pendingLaunches = new Map<string, { plan: string; generation: number; launches: { agent?: string; sessionFile?: string }[] }>();
|
||||
let notice = true;
|
||||
let planWatcher: FSWatcher | undefined;
|
||||
let planEditTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let planHash = "";
|
||||
const childEnvironment = process.env.PI_SUBAGENT_AGENT === WORKER;
|
||||
const save = () => pi.appendEntry(STATE, state);
|
||||
const save = () => pi.appendEntry(STATE, structuredClone(state));
|
||||
// Missing, empty and failed reads are unavailable snapshots, never an empty authoritative plan.
|
||||
const readPlan = () => {
|
||||
try {
|
||||
@@ -111,10 +112,10 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
return;
|
||||
}
|
||||
const items = goals(snapshot.text);
|
||||
// Reopened/deleted/ambiguous goal identities lose their sign-off. Manual ticks remain claims.
|
||||
// Pi/OpenAI: approval belongs to the reviewed requirements, not only the title.
|
||||
for (const subject of Object.keys(state.signoffs)) {
|
||||
const matches = items.filter((g) => key(g.subject) === subject);
|
||||
if (matches.length !== 1 || matches[0].status !== "done") { delete state.signoffs[subject]; save(); }
|
||||
if (matches.length !== 1 || matches[0].status !== "done" || state.signoffs[subject].signature !== goalAcceptanceSignature(snapshot.text, subject)) { delete state.signoffs[subject]; save(); }
|
||||
}
|
||||
const accepted = items.filter((g) => g.status === "done" && state.signoffs[key(g.subject)]).length;
|
||||
ctx.ui.setStatus("goals", `goals: ${state.child ? "worker" : state.mode} | ${accepted}/${items.length} reviewed`);
|
||||
@@ -189,7 +190,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
else pi.sendMessage({ customType: "pi-goals-supervision", content, display: true }, { deliverAs: "nextTurn" });
|
||||
}
|
||||
async function confirmOwnership(ctx: ExtensionContext, target: string, text: string, solo = true): Promise<boolean> {
|
||||
if (pendingLaunches > 0) { ctx.ui.notify("A worker launch/resume is still pending; inspect its result before takeover.", "warning"); return false; }
|
||||
if (pendingLaunches.size > 0) { ctx.ui.notify("A worker launch/resume is still pending; inspect its result before takeover.", "warning"); return false; }
|
||||
const stamp = generation;
|
||||
const revision = workerRevision;
|
||||
const confirmation = solo ? "Worker confirmed stopped" : "Previous supervisor confirmed stopped";
|
||||
@@ -209,7 +210,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
if (state.mode !== "planning") { ctx.ui.notify("Ready applies to a draft; use status or resume.", "warning"); return; }
|
||||
const text = planText();
|
||||
const items = goals(text);
|
||||
if (!edit && (!items.length || new Set(items.map((g) => key(g.subject))).size !== items.length)) {
|
||||
if (!edit && (!items.length || items.some(g => !g.subject) || new Set(items.map((g) => key(g.subject))).size !== items.length)) {
|
||||
ctx.ui.notify("Write a plan with distinct '- [ ] goal: ...' subjects before Ready.", "warning"); return;
|
||||
}
|
||||
const stamp = generation;
|
||||
@@ -297,23 +298,32 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
if (launch && typeof launch.title === "string" && !launch.title.startsWith(prefix)) launch.title = prefix + launch.title;
|
||||
}
|
||||
}
|
||||
if (state.child || !["subagent", "subagent_resume"].includes(event.toolName)) return;
|
||||
if (state.child || (event.toolName !== "subagent" && event.toolName !== "subagent_resume")) return;
|
||||
// Solo means this chat took over implementation: no concurrent writer may be delegated.
|
||||
if (state.mode === "planning" || state.mode === "paused" || state.mode === "solo") return { block: true, reason: goalToolBlocked(state.mode) };
|
||||
if (state.plan) { pendingLaunches++; state.workerStopped = false; workerRevision++; save(); }
|
||||
if (state.plan) {
|
||||
const input = event.input as { agent?: string; sessionFile?: string; children?: { agent?: string; sessionFile?: string }[] };
|
||||
const launches = input.children ?? [input];
|
||||
pendingLaunches.set(event.toolCallId, { plan: state.plan, generation, launches: launches.map(launch => ({ agent: launch.agent, sessionFile: launch.sessionFile })) });
|
||||
state.workerStopped = false; workerRevision++; save();
|
||||
}
|
||||
});
|
||||
pi.on("tool_result", (event) => {
|
||||
if (state.child || !state.plan || !["subagent", "subagent_resume"].includes(event.toolName)) return;
|
||||
pendingLaunches = Math.max(0, pendingLaunches - 1);
|
||||
if (event.isError) return;
|
||||
const details = event.details as { id?: string; sessionFile?: string } | undefined;
|
||||
if (!details?.id || !details.sessionFile) return;
|
||||
const record = { id: details.id, sessionFile: details.sessionFile };
|
||||
if (state.worker?.sessionFile === record.sessionFile) state.worker = record;
|
||||
else if (!state.worker) state.worker = record;
|
||||
// Extra launches stay recorded as helpers; the implementation binding never moves silently.
|
||||
else state.helpers = [...(state.helpers ?? []).filter((h) => h.sessionFile !== record.sessionFile), record];
|
||||
state.workerStopped = false; workerRevision++; save();
|
||||
pi.on("tool_execution_end", (event) => {
|
||||
const pending = pendingLaunches.get(event.toolCallId);
|
||||
pendingLaunches.delete(event.toolCallId);
|
||||
if (!pending || state.child || pending.plan !== state.plan || pending.generation !== generation || event.isError) return;
|
||||
type ChildResult = { id?: string; sessionFile?: string; agent?: string };
|
||||
const details = (event.result as { details?: ChildResult & { children?: ChildResult[] } }).details;
|
||||
if (!details) return;
|
||||
for (const [index, child] of (details.children ?? [details]).entries()) {
|
||||
if (!child.id || !child.sessionFile) continue;
|
||||
const record = { id: child.id, sessionFile: child.sessionFile };
|
||||
const launch = pending.launches[index];
|
||||
const implementation = (child.agent ?? launch?.agent) === WORKER || launch?.sessionFile === state.worker?.sessionFile && Boolean(state.worker);
|
||||
if (state.worker?.sessionFile === record.sessionFile || !state.worker && implementation) state.worker = record;
|
||||
else state.helpers = [...state.helpers.filter(h => h.sessionFile !== record.sessionFile), record];
|
||||
}
|
||||
workerRevision++; save();
|
||||
});
|
||||
|
||||
pi.registerCommand("goals", {
|
||||
@@ -391,7 +401,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
const target = isAbsolute(raw) ? raw : resolve(ctx.cwd, raw);
|
||||
let text: string;
|
||||
try { text = readFileSync(target, "utf8"); } catch { ctx.ui.notify(`Cannot read plan at ${target}.`, "error"); return; }
|
||||
if (!goals(text).length) { ctx.ui.notify(`${target} has no '- [ ] goal:' lines; attach a judgeable plan.`, "warning"); return; }
|
||||
if (!goals(text).length || goals(text).some(g => !g.subject)) { ctx.ui.notify(`${target} has no '- [ ] goal:' lines with valid subjects; attach a judgeable plan.`, "warning"); return; }
|
||||
if (!solo && ((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 noted = /^-\s*worker session:\s*(\S+)/im.exec(foldPlan(text))?.[1];
|
||||
if (!(await confirmOwnership(ctx, target, text, solo))) return;
|
||||
@@ -412,13 +422,15 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
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 = 0; pendingUpkeep = undefined; notice = true;
|
||||
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");
|
||||
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 !== "solo" && state.mode !== "supervising") return;
|
||||
state.pausedFrom = state.mode;
|
||||
state.mode = "paused"; generation++; notice = true; save(); refresh(ctx); watchPlan(ctx);
|
||||
const pause = pauseExitNotice(state.worker, false);
|
||||
const requestCleanup = Boolean(state.worker) || hasScheduleTool();
|
||||
@@ -428,13 +440,14 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
}
|
||||
if (command === "resume") {
|
||||
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);
|
||||
send(`${checkIn(ctx)}\n\n${resumeNotice(WORKER, state.plan, state.worker)}`);
|
||||
return;
|
||||
}
|
||||
if (command === "solo") {
|
||||
if (!state.plan || !goals(planText()).length) { ctx.ui.notify("Register a goal plan first.", "warning"); return; }
|
||||
if (!state.plan || !goals(planText()).length || goals(planText()).some(g => !g.subject)) { ctx.ui.notify("Register a goal plan first.", "warning"); return; }
|
||||
if (!(await confirmOwnership(ctx, state.plan, planText()))) return;
|
||||
enterSolo(ctx);
|
||||
return;
|
||||
@@ -460,7 +473,9 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
async execute(_id, params, _signal, _update, ctx) {
|
||||
if (!state.child) return result(messages.childAttachOnly);
|
||||
try {
|
||||
if (!isAbsolute(params.path) || !goals(readFileSync(params.path, "utf8")).length) return result(messages.invalidAttachment);
|
||||
if (!isAbsolute(params.path)) return result(messages.invalidAttachment);
|
||||
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);
|
||||
return result(childPlanAttached(params.path));
|
||||
@@ -469,29 +484,35 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
pi.registerTool({
|
||||
name: "CompleteGoal", label: "Review goal evidence",
|
||||
description: completeGoalDescription,
|
||||
parameters: Type.Object({ goal: Type.String(), evidence: Type.Array(Type.String(), { minItems: 1 }), observation: Type.String({ minLength: 1 }) }),
|
||||
parameters: Type.Object({ goal: Type.String({ minLength: 1 }), evidence: Type.Array(Type.String(), { minItems: 1 }), observation: Type.String({ minLength: 1 }) }),
|
||||
async execute(_id, params, signal, _update, ctx) {
|
||||
if (state.child || !["supervising", "solo"].includes(state.mode)) return result(messages.completionUnavailable);
|
||||
if (signal?.aborted) return result(messages.cancelled);
|
||||
const snapshot = readPlan();
|
||||
if (snapshot.text === undefined) return result(snapshot.error!);
|
||||
const text = snapshot.text;
|
||||
const matches = goals(text).filter((g) => g.status !== "cancelled" && key(g.subject) === key(params.goal));
|
||||
if (matches.length !== 1 || !state.plan) return result(messages.uniqueGoal);
|
||||
const evidence = params.evidence.map((file) => isAbsolute(file) ? file : resolve(ctx.cwd, file));
|
||||
try { for (const file of evidence) if (!readFileSync(file).length) throw new Error(emptyEvidence(file)); }
|
||||
catch (error) { return result(evidenceUnavailable(error)); }
|
||||
const lines = text.split("\n");
|
||||
lines[matches[0].index] = lines[matches[0].index].replace(/\[[ xX/-]\]/, "[x]");
|
||||
let log = lines.findIndex(line => /^##\s+Log\s*$/i.test(line));
|
||||
if (log === -1) { lines.push("", "## Log"); log = lines.length - 1; }
|
||||
lines.splice(log + 1, 0, "", completionLog(params.goal, params.observation, evidence, state.mode === "solo"));
|
||||
writeFileSync(state.plan, `${lines.join("\n").trimEnd()}\n`);
|
||||
state.signoffs[key(matches[0].subject)] = { evidence, observation: params.observation };
|
||||
planHash = digest(planViews(planText()).notify);
|
||||
save(); refresh(ctx);
|
||||
const remaining = goals(planText()).some((goal) => goal.status !== "cancelled" && (goal.status !== "done" || !state.signoffs[key(goal.subject)]));
|
||||
return result(completionResult(matches[0].subject, ctx.sessionManager.getSessionId(), remaining, state.mode === "solo"));
|
||||
if (state.child || !state.plan || !["supervising", "solo"].includes(state.mode)) return result(messages.completionUnavailable);
|
||||
const path = state.plan;
|
||||
const stamp = generation;
|
||||
return withFileMutationQueue(path, async () => {
|
||||
if (stamp !== generation || path !== state.plan || state.child || !["supervising", "solo"].includes(state.mode)) return result(messages.completionUnavailable);
|
||||
if (signal?.aborted) return result(messages.cancelled);
|
||||
if (!params.goal.trim()) return result(messages.uniqueGoal);
|
||||
const snapshot = readPlan();
|
||||
if (snapshot.text === undefined) return result(snapshot.error!);
|
||||
const text = snapshot.text;
|
||||
const matches = goals(text).filter((g) => g.status !== "cancelled" && key(g.subject) === key(params.goal));
|
||||
if (matches.length !== 1 || !state.plan) return result(messages.uniqueGoal);
|
||||
const evidence = params.evidence.map((file) => isAbsolute(file) ? file : resolve(ctx.cwd, file));
|
||||
try { for (const file of evidence) if (!readFileSync(file).length) throw new Error(emptyEvidence(file)); }
|
||||
catch (error) { return result(evidenceUnavailable(error)); }
|
||||
const lines = text.split("\n");
|
||||
lines[matches[0].index] = lines[matches[0].index].replace(/\[[ xX/-]\]/, "[x]");
|
||||
let log = lines.findIndex(line => FOLD_LINE.test(line));
|
||||
if (log === -1) { lines.push("", "## Log"); log = lines.length - 1; }
|
||||
lines.splice(log + 1, 0, "", completionLog(params.goal, params.observation, evidence, state.mode === "solo"));
|
||||
writeFileSync(path, `${lines.join("\n").trimEnd()}\n`);
|
||||
state.signoffs[key(matches[0].subject)] = { evidence, observation: params.observation, signature: goalAcceptanceSignature(text, matches[0].subject)! };
|
||||
planHash = digest(planViews(planText()).notify);
|
||||
save(); refresh(ctx);
|
||||
const remaining = goals(planText()).some((goal) => goal.status !== "cancelled" && (goal.status !== "done" || !state.signoffs[key(goal.subject)]));
|
||||
return result(completionResult(matches[0].subject, ctx.sessionManager.getSessionId(), remaining, state.mode === "solo"));
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
+6
-32
@@ -1,33 +1,7 @@
|
||||
// Pi/OpenAI: Preserve plan wording; omit history and, in the short view, task/evidence details.
|
||||
// The notify view governs plan-change events: goals, tasks, evidence and inferences are
|
||||
// content worth a supervisor review; worker identity bookkeeping is not (field report,
|
||||
// LUCID3 supervisor 2026-09-10: two identical review events for a session-path edit).
|
||||
export function planViews(plan: string): { short: string; notify: string; long: string } {
|
||||
const long = plan.split(/^#{1,6}\s+(?:Log|Appendix|Appendices|Appendixes|Interview|Learnings|Papercuts)\b.*$/mi)[0].trim();
|
||||
const identity = /^-\s*(?:active worker|worker session|worker intercom session):/i;
|
||||
const notify = long.split("\n").filter((line) => !identity.test(line)).join("\n").trim();
|
||||
const kept: string[] = [];
|
||||
let omittedIndent: number | null = null;
|
||||
let omittedHeading: number | null = null;
|
||||
for (const line of long.split("\n")) {
|
||||
// Pi/OpenAI: Worker identity bookkeeping is not a change to agreed requirements.
|
||||
if (/^-\s*(?:active worker|worker session|worker intercom session):/i.test(line)) continue;
|
||||
const heading = /^(#{1,6})\s+(.+)$/.exec(line);
|
||||
if (heading) {
|
||||
if (omittedHeading !== null && heading[1].length <= omittedHeading) omittedHeading = null;
|
||||
if (/^(?:Tasks?|Task list|Subtasks?|Evidence)\b/i.test(heading[2])) omittedHeading = heading[1].length;
|
||||
}
|
||||
if (omittedHeading !== null) continue;
|
||||
const indent = line.match(/^\s*/)?.[0].length ?? 0;
|
||||
if (omittedIndent !== null) {
|
||||
if (!line.trim() || indent > omittedIndent) continue;
|
||||
omittedIndent = null;
|
||||
}
|
||||
if (/^\s*[-*]\s+(?:tasks?|subtasks?|evidence):/i.test(line) || /^\s*(?:\d+[.)]|[-*])\s+\[[ x/~-]\]\s+(?!goal:)/i.test(line)) {
|
||||
omittedIndent = indent;
|
||||
continue;
|
||||
}
|
||||
kept.push(line);
|
||||
}
|
||||
return { short: kept.join("\n").trim(), notify, long };
|
||||
// Pi/OpenAI: Review task/evidence changes, but omit history and worker identity bookkeeping.
|
||||
import { foldPlan } from "./plan.js";
|
||||
|
||||
export function planViews(plan: string): { notify: string } {
|
||||
const identity = /^[ \t]*[-*]\s*(?:active worker|worker session|worker intercom session):/i;
|
||||
return { notify: foldPlan(plan).split("\n").filter(line => !identity.test(line)).join("\n").trim() };
|
||||
}
|
||||
|
||||
+40
-2
@@ -1,8 +1,46 @@
|
||||
// Shared plan syntax: only the section above the Log contains current goals.
|
||||
// Pi/OpenAI: Log, at any heading level, is the single boundary between current work and history.
|
||||
export const GOAL_LINE = /^\s*(?:\d+\.|[-*])\s*\[([ xX/-])\]\s*goal:\s*(.*)$/i;
|
||||
export const FOLD_LINE = /^##\s+Log\s*$/im;
|
||||
export const FOLD_LINE = /^#{1,6}[ \t]+Log[ \t]*\r?$/im;
|
||||
const identity = /^[ \t]*[-*]\s*(?:active worker|worker session|worker intercom session|preferred worker model):/i;
|
||||
|
||||
export function foldPlan(plan: string): string {
|
||||
const match = FOLD_LINE.exec(plan);
|
||||
return (match ? plan.slice(0, match.index) : plan).trimEnd();
|
||||
}
|
||||
|
||||
// Pi/OpenAI: Approval covers shared requirements and this goal, not checkbox/task/evidence maintenance.
|
||||
export function goalAcceptanceSignature(plan: string, goal: string): string | undefined {
|
||||
const lines = foldPlan(plan).split("\n");
|
||||
const goals = lines.flatMap((line, index) => {
|
||||
const match = GOAL_LINE.exec(line);
|
||||
return match ? [{ index, subject: match[2].trim().toLowerCase() }] : [];
|
||||
});
|
||||
const matches = goals.filter(item => item.subject === goal.trim().toLowerCase());
|
||||
if (matches.length !== 1) return undefined;
|
||||
const selected = matches[0];
|
||||
const end = goals.find(item => item.index > selected.index)?.index ?? lines.length;
|
||||
const content = [...lines.slice(0, goals[0].index), `goal: ${selected.subject}`, ...lines.slice(selected.index + 1, end)];
|
||||
const kept: string[] = [];
|
||||
let omittedIndent: number | undefined;
|
||||
let omittedHeading: number | undefined;
|
||||
for (const line of content) {
|
||||
if (identity.test(line)) continue;
|
||||
const heading = /^(#{1,6})\s+(.+)$/.exec(line);
|
||||
if (heading) {
|
||||
if (omittedHeading !== undefined && heading[1].length <= omittedHeading) omittedHeading = undefined;
|
||||
if (/^(?:Tasks?|Task list|Subtasks?|Evidence)\b/i.test(heading[2])) omittedHeading = heading[1].length;
|
||||
}
|
||||
if (omittedHeading !== undefined) continue;
|
||||
const indent = line.length - line.trimStart().length;
|
||||
if (omittedIndent !== undefined) {
|
||||
if (!line.trim() || indent > omittedIndent) continue;
|
||||
omittedIndent = undefined;
|
||||
}
|
||||
if (/^\s*[-*]\s+(?:tasks?|subtasks?|evidence):/i.test(line) || /^\s*(?:\d+[.)]|[-*])\s+\[[ xX/~-]\]/.test(line)) {
|
||||
omittedIndent = indent;
|
||||
continue;
|
||||
}
|
||||
if (line.trim()) kept.push(line.trim());
|
||||
}
|
||||
return kept.join("\n");
|
||||
}
|
||||
|
||||
+13
-10
@@ -1,10 +1,12 @@
|
||||
// Pi/OpenAI: Planning, approval, supervision, reminders, completion and recovery.
|
||||
import { foldPlan } from "./plan.js";
|
||||
|
||||
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.
|
||||
|
||||
1. Reduce technical uncertainty first. Use read-only repository tools or web search when either can
|
||||
resolve a fact. Do not write or run code in this phase (edit/write are blocked except for the plan
|
||||
file; don't mutate state via bash either).
|
||||
resolve a fact. Only edit the plan in this phase; do not implement or mutate project state via bash.
|
||||
This is an instruction, not a filesystem restriction.
|
||||
2. Before you draft a goal, identify its object, observable result, scope, and any decision that the
|
||||
human would need to approve later. Briefly reframe the request in your own words to check comprehension
|
||||
and make your understanding visible: the intended outcome, boundary, and success check. Invite correction,
|
||||
@@ -31,8 +33,8 @@ not replace, defer, or contradict it; ask the human if an inference would change
|
||||
5. When every goal has an object, observable result, settled scope, and required approval, draft the
|
||||
plan file and present it. It should be safe to work overnight and present the requested outcome.
|
||||
|
||||
How this mode ends: after each settled draft the human gets a menu (Ready / Refine / Edit / Cancel).
|
||||
Plan mode ends only when they pick Ready. Refine collects short revision notes. Edit opens the full
|
||||
How this mode ends: after each changed settled draft the human gets a menu (Ready / Discuss / Edit / Cancel).
|
||||
Plan mode ends only when they pick Ready. Discuss continues ordinary chat. Edit opens the full
|
||||
plan. When a new requirement arrives, fold it in, say what changed, and present the plan again.
|
||||
Detail that doesn't change a goal or a discriminator belongs in the appendix, not in the goals.
|
||||
|
||||
@@ -131,16 +133,16 @@ 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}`;
|
||||
}
|
||||
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 or reopen the review menu until requested.";
|
||||
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.";
|
||||
|
||||
// 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 the worker widget and plan context; grants no parent completion authority. No discovery or worker launch.";
|
||||
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.";
|
||||
export const childPlanRole = "You are the delegated implementation worker. Maintain task ticks, evidence and Log entries for your delegated work in the supplied plan. Preserve agreed goals, requirements and discriminators; the supervisor owns goal-status changes and completion approval. Do not launch a second writer. Call AttachGoalPlan with the explicit plan path in your task before implementation (also after reconnect if unbound). Immediately report your actual Intercom UUID, saved-session path and current provider/model to the supplied supervisor ID. Identify unavailable fields as unknown; do not equate runtime IDs, session filenames and Intercom IDs. Send progress, completion and blocker reports there with artifact paths, then stay open for live messages. Do not exit or use caller_ping; unsent editor drafts are not visible in model context.";
|
||||
export function readyApproved(workerName: string, planPath: string, notedWorker: string | undefined, plan: string, supervisorId: string): string {
|
||||
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 give the child supervisor Intercom session ${supervisorId}. 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${plan}`;
|
||||
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)}`;
|
||||
}
|
||||
|
||||
// Supervision and turn-event upkeep (not a scheduled wake-up).
|
||||
@@ -151,7 +153,7 @@ You can be playful: let the humor come from what actually happened. Avoid repeat
|
||||
You can speculate and brainstorm around uncertainty or unexpected results. Label guesses as guesses, consider alternative explanations, and look for a useful way to tell them apart. Keep exploration brief, open-minded and fun: take a step back, play with surprising ideas, question the current framing, and enjoy exploring the broader perspective while staying connected to the agreed goal.
|
||||
(b •_•)b -- wassname
|
||||
Take uncertainty as an invitation to investigate, not something to hide. Have room to play with ideas, question yourself and the worker, and appreciate a good surprise. Investigate surprising results, find mistaken assumptions, make complicated ideas simpler, and disagree usefully rather than agree politely. Keep the work moving without turning supervision into paperwork. A little affectionate teasing is welcome when it fits, and workers can push back too. Keep the humor friendly and the criticism specific. -- Pi/Astra
|
||||
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. Give each worker your Intercom session ID ${supervisorId}; 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 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.
|
||||
@@ -176,13 +178,14 @@ export const upkeepNudges = [
|
||||
"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, supervisorRound?: number): string {
|
||||
const nudge = supervisorRound === undefined ? "" : `${upkeepNudges[supervisorRound % upkeepNudges.length]}\n\n`;
|
||||
return `${nudge}Plan upkeep: update task ticks, evidence and Log in ${planPath} 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.`;
|
||||
}
|
||||
export function planContext(mode: string, path: string | undefined, text: string): string {
|
||||
return `Current goal mode: ${mode}. Earlier role messages are historical; this current role governs.\nPlan: ${path ?? "not attached"}\n${text}`;
|
||||
return `Current goal mode: ${mode}. Earlier role messages are historical; this current role governs.\nPlan: ${path ?? "not attached"}\n${foldPlan(text)}\n\nRead historical Log entries from the plan file when needed.`;
|
||||
}
|
||||
export function planChangedReview(planPath: string): string {
|
||||
return `${supervisorJob}\nPlan changed: ${planPath}. Read the current working set and inspect changed requirements, completion claims and evidence. Manual checkbox edits are claims, not proof. Do not weaken the agreed goal or start a duplicate writer.`;
|
||||
@@ -214,7 +217,7 @@ export const goalToolBlocked = (mode: string) => `Goals are ${mode}; no worker l
|
||||
export const emptyEvidence = (path: string) => `Empty evidence: ${path}`;
|
||||
export const evidenceUnavailable = (error: unknown) => `Evidence unavailable: ${String(error)}. No sign-off recorded.`;
|
||||
export const planUnavailable = (path: string | undefined, error: unknown) => `Goal plan ${path ?? "not attached"} unavailable: ${String(error)}. Do not implement or sign off until it is restored or explicitly attached. Retain all progress and signoffs; do not restart completed work.`;
|
||||
export const childPlanAttached = (path: string) => `Attached worker plan ${path}; widget and plan context restored without altering the file. Parent retains completion authority.`;
|
||||
export const childPlanAttached = (path: string) => `Attached worker plan ${path}; plan context restored without altering the file. Parent retains completion authority.`;
|
||||
export function completionLog(goal: string, observation: string, evidence: string[], solo: boolean): string {
|
||||
return `- ${solo ? "Solo self-verification" : "Parent review"}: ${JSON.stringify(goal)}; ${JSON.stringify(observation)}; evidence ${JSON.stringify(evidence)}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user